Initial pi-devin package: Devin Local models inside Pi.

Auth and catalog come from the local Devin CLI. Pi stays the harness.
This commit is contained in:
Kashyab Ambarani
2026-08-16 20:29:56 -07:00
commit d36c0b3705
15 changed files with 1658 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules/
dist/
package-lock.json
.DS_Store
*.log
.env
+24
View File
@@ -0,0 +1,24 @@
# AGENTS.md — pi-devin
Pi package that registers the `devin` provider. Auth and the model catalog come from the local Devin CLI. Pi remains the harness.
## Layout
```
extensions/index.ts # registerProvider("devin"), /login, /devin-status, /devin-refresh
src/cli.ts # locate + spawn `devin`
src/credentials.ts # ~/.local/share/devin/credentials.toml
src/models.ts # `devin models list --format json` → ProviderModelConfig[]
src/stream.ts # streamSimple via GetChatMessage (Connect/protobuf)
src/jwt.ts # GetUserJwt cache
src/metadata.ts # Metadata proto (ide_name=devin)
src/wire.ts # protobuf + Connect framing
src/context-map.ts # Pi Context → Cognition chat history
```
## Contract
- `/login devin` must call `devin auth login` when credentials are missing, not a custom Windsurf paste flow.
- Model IDs must come from `devin models list`, not a hardcoded cloud allowlist.
- Do not spawn `devin acp` as the session owner. Pi keeps tools, permissions, and the session tree.
- Package must stay installable as a Pi package: `keywords: ["pi-package"]` and `pi.extensions`.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kashyab Ambarani
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+84
View File
@@ -0,0 +1,84 @@
# pi-devin
A [Pi](https://pi.dev) package that uses **Devin Local** models inside Pi.
Pi stays the harness. The local [Devin CLI](https://docs.devin.ai/cli) owns login and the live model catalog (`devin auth`, `devin models list`). This package does **not** wrap Devin as an ACP agent and does **not** use the old `pi-devin-auth` cloud-only shim.
## Why this exists
`pi-devin-auth` treated Devin as Cascade cloud chat. Models like Sol High, Opus 5, and Fable 5 then failed with:
```text
This model is only in Devin Local.
```
Zed works because it launches `devin acp` — Devin Local. This package uses that same local CLI for auth + catalog, then streams completions into Pi so Pi's tools, sessions, and UI stay in charge.
## Requirements
- Pi Coding Agent 0.80+
- A signed-in [Devin CLI](https://docs.devin.ai/cli) (`devin auth status`)
- Node 18+
The CLI binary is resolved in this order:
1. `$DEVIN_CLI`
2. `~/.local/bin/devin`, Homebrew, `/usr/local/bin/devin`
3. Devin.app
4. Zed's ACP registry install (`~/Library/Application Support/Zed/external_agents/registry/devin/.../bin/devin`)
5. `which devin`
## Install
Local (this repo):
```bash
pi install /Users/kashyab/pi-devin
```
After you publish to npm:
```bash
pi install npm:pi-devin
```
From git:
```bash
pi install git:github.com/<you>/pi-devin
```
Restart Pi or run `/reload`.
## Usage
```text
/login devin
/model devin/claude-opus-5-high
/model devin/claude-5-fable-high
/model devin/gpt-5-6-sol-high
```
`/login devin` runs `devin auth login` if `~/.local/share/devin/credentials.toml` is missing. If you already signed in through Devin Desktop, the CLI, or Zed, that file is reused.
Commands:
- `/devin-status` — CLI path, version, auth
- `/devin-refresh` — reload `devin models list --format json`
## What this is / is not
| This package | Not this package |
|---|---|
| Pi is the agent | Devin ACP taking over the session |
| Devin CLI for auth + catalog | Fake Windsurf OAuth paste flow |
| Live CLI families (Opus 5, Fable 5, Sol, …) | Hardcoded 11-model cloud allowlist |
| Completions streamed into Pi tools | `devin acp` as the editor host |
## Publish
This is a standard Pi package (`keywords: ["pi-package"]` + `pi.extensions`). After you push to npm with that keyword, it can show up on [pi.dev/packages](https://pi.dev/packages).
## License
MIT. Unofficial. Not affiliated with Cognition.
+117
View File
@@ -0,0 +1,117 @@
import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
import type { Api, Model, OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
import { authStatus, loginWithCli, readCredentials } from "../src/credentials.js";
import { whichDevin, devinVersion } from "../src/cli.js";
import { FALLBACK_MODELS, loadCliCatalog, modelsFromCatalog } from "../src/models.js";
import { streamDevin } from "../src/stream.js";
const PROVIDER_ID = "devin";
const PLACEHOLDER_BASE_URL = "https://server.codeium.com";
let _pi: ExtensionAPI | null = null;
function registerDevinProvider(pi: ExtensionAPI, models: ProviderModelConfig[]): void {
pi.registerProvider(PROVIDER_ID, {
name: "Devin Local",
api: "devin-local",
baseUrl: PLACEHOLDER_BASE_URL,
models,
oauth: {
name: "Devin CLI",
async login(_callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
const creds = await loginWithCli();
if (_pi) {
try {
const catalog = await loadCliCatalog();
registerDevinProvider(_pi, modelsFromCatalog(catalog));
} catch {
// keep current models
}
}
return {
refresh: "",
access: creds.apiKey,
expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
};
},
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
const creds = readCredentials();
if (!creds) return credentials;
return {
refresh: "",
access: creds.apiKey,
expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
};
},
getApiKey(credentials: OAuthCredentials): string {
return readCredentials()?.apiKey || credentials.access;
},
modifyModels(models: Model<Api>[], _credentials: OAuthCredentials): Model<Api>[] {
return models;
},
},
streamSimple: streamDevin,
});
}
export default async function (pi: ExtensionAPI): Promise<void> {
_pi = pi;
registerDevinProvider(pi, FALLBACK_MODELS);
try {
if (readCredentials()) {
const catalog = await loadCliCatalog();
registerDevinProvider(pi, modelsFromCatalog(catalog));
}
} catch {
// fallback models already registered
}
pi.on("session_start", async () => {
try {
if (!_pi || !readCredentials()) return;
const catalog = await loadCliCatalog();
registerDevinProvider(_pi, modelsFromCatalog(catalog));
} catch {
// keep current models
}
});
pi.registerCommand("devin-status", {
description: "Show Devin CLI auth + binary status",
handler: async (_args, ctx) => {
const bin = await whichDevin();
const version = await devinVersion();
const status = await authStatus();
ctx.ui.notify(
[
bin ? `CLI: ${bin}` : "CLI: not found",
version ? `Version: ${version}` : "Version: unknown",
status.loggedIn ? "Auth: signed in via Devin CLI" : "Auth: not signed in. Run /login devin or `devin auth login`",
].join("\n"),
status.loggedIn && bin ? "info" : "warning",
);
},
});
pi.registerCommand("devin-refresh", {
description: "Refresh Devin Local model catalog from `devin models list`",
handler: async (_args, ctx) => {
try {
const catalog = await loadCliCatalog();
const models = modelsFromCatalog(catalog);
registerDevinProvider(pi, models);
ctx.ui.notify(`Devin: loaded ${models.length} families from the local CLI.`, "info");
} catch (error) {
ctx.ui.notify(
`Devin refresh failed: ${error instanceof Error ? error.message : String(error)}`,
"error",
);
}
},
});
pi.on("session_shutdown", async () => {
_pi = null;
});
}
+45
View File
@@ -0,0 +1,45 @@
{
"name": "pi-devin",
"version": "0.1.0",
"description": "Pi provider for Devin Local models. Auth and catalog come from the local Devin CLI; Pi stays the harness.",
"type": "module",
"keywords": [
"pi-package",
"pi-extension",
"devin",
"cognition",
"custom-provider"
],
"author": "Kashyab Ambarani",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/kashyab/pi-devin.git"
},
"pi": {
"extensions": [
"./extensions/index.ts"
]
},
"files": [
"extensions/",
"src/",
"README.md",
"AGENTS.md",
"LICENSE"
],
"scripts": {
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*"
},
"devDependencies": {
"@types/node": "^24.13.3",
"typescript": "^5.9.2"
},
"engines": {
"node": ">=18.0.0"
}
}
+127
View File
@@ -0,0 +1,127 @@
import { existsSync, readdirSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const ZED_DEVIN =
join(
homedir(),
"Library/Application Support/Zed/external_agents/registry/devin",
);
const KNOWN_BINS = [
process.env.DEVIN_CLI,
join(homedir(), ".local/bin/devin"),
join(homedir(), ".devin/bin/devin"),
"/opt/homebrew/bin/devin",
"/usr/local/bin/devin",
"/Applications/Devin.app/Contents/Resources/app/extensions/windsurf/devin/bin/devin",
].filter((p): p is string => Boolean(p));
let cachedBin: string | null | undefined;
function newestZedDevin(): string | null {
try {
if (!existsSync(ZED_DEVIN)) return null;
const versions = readdirSync(ZED_DEVIN)
.map((name) => {
const bin = join(ZED_DEVIN, name, "bin/devin");
try {
return { bin, mtime: statSync(bin).mtimeMs };
} catch {
return null;
}
})
.filter((row): row is { bin: string; mtime: number } => Boolean(row))
.sort((a, b) => b.mtime - a.mtime);
return versions[0]?.bin ?? null;
} catch {
return null;
}
}
export function findDevinBin(): string | null {
if (cachedBin !== undefined) return cachedBin;
for (const bin of KNOWN_BINS) {
if (existsSync(bin)) {
cachedBin = bin;
return bin;
}
}
const zed = newestZedDevin();
if (zed) {
cachedBin = zed;
return zed;
}
cachedBin = null;
return null;
}
export function clearDevinBinCache(): void {
cachedBin = undefined;
}
export async function whichDevin(): Promise<string | null> {
const known = findDevinBin();
if (known) return known;
try {
const { stdout } = await execFileAsync("/usr/bin/which", ["devin"], { timeout: 5_000 });
const path = stdout.trim();
if (path && existsSync(path)) {
cachedBin = path;
return path;
}
} catch {
// not on PATH
}
return null;
}
export async function runDevin(
args: string[],
opts: { timeoutMs?: number; inheritStdio?: boolean } = {},
): Promise<{ stdout: string; stderr: string; code: number }> {
const bin = await whichDevin();
if (!bin) {
throw new Error(
"Devin CLI not found. Install Devin, or set DEVIN_CLI to the `devin` binary (Zed ACP registry and Devin.app are also checked).",
);
}
if (opts.inheritStdio) {
return await new Promise((resolve, reject) => {
const child = spawn(bin, args, { stdio: "inherit" });
child.on("error", reject);
child.on("close", (code) => {
resolve({ stdout: "", stderr: "", code: code ?? 1 });
});
});
}
try {
const { stdout, stderr } = await execFileAsync(bin, args, {
timeout: opts.timeoutMs ?? 30_000,
maxBuffer: 8 * 1024 * 1024,
});
return { stdout, stderr, code: 0 };
} catch (error) {
const err = error as { stdout?: string; stderr?: string; code?: number; message?: string };
if (typeof err.code === "number") {
return { stdout: err.stdout ?? "", stderr: err.stderr ?? "", code: err.code };
}
throw error;
}
}
export async function devinVersion(): Promise<string | null> {
try {
const { stdout, code } = await runDevin(["version"], { timeoutMs: 8_000 });
if (code !== 0) return null;
return stdout.trim() || null;
} catch {
return null;
}
}
+96
View File
@@ -0,0 +1,96 @@
import type { Context, Message, Tool } from "@earendil-works/pi-ai";
export interface ContentPart {
type: "text" | "image";
text?: string;
mimeType?: string;
base64Data?: string;
}
export interface ChatHistoryItem {
role: "user" | "assistant" | "system" | "tool";
content: string | ContentPart[];
tool_call_id?: string;
tool_calls?: Array<{ id: string; name: string; arguments: string }>;
}
export interface ToolDef {
name: string;
description: string;
parameters: unknown;
}
export interface MappedChat {
messages: ChatHistoryItem[];
tools: ToolDef[];
}
function userContent(content: Message["content"]): string | ContentPart[] {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
const parts: ContentPart[] = [];
for (const part of content) {
if (!part || typeof part !== "object") continue;
if (part.type === "text") parts.push({ type: "text", text: part.text });
if (part.type === "image") {
parts.push({ type: "image", mimeType: part.mimeType, base64Data: part.data });
}
}
return parts;
}
export function mapContextToChat(context: Context): MappedChat {
const messages: ChatHistoryItem[] = [];
if (context.systemPrompt) {
messages.push({ role: "system", content: context.systemPrompt });
}
for (const message of context.messages) {
if (message.role === "user") {
messages.push({ role: "user", content: userContent(message.content) });
continue;
}
if (message.role === "assistant") {
const texts: string[] = [];
const toolCalls: Array<{ id: string; name: string; arguments: string }> = [];
for (const part of message.content) {
if (part.type === "text") texts.push(part.text);
if (part.type === "toolCall") {
toolCalls.push({
id: part.id,
name: part.name,
arguments: JSON.stringify(part.arguments ?? {}),
});
}
}
messages.push({
role: "assistant",
content: texts.join("\n"),
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
});
continue;
}
if (message.role === "toolResult") {
const text =
typeof message.content === "string"
? message.content
: message.content
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("\n");
messages.push({
role: "tool",
content: text,
tool_call_id: message.toolCallId,
});
}
}
const tools: ToolDef[] = (context.tools ?? []).map((tool: Tool) => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
}));
return { messages, tools };
}
+77
View File
@@ -0,0 +1,77 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { runDevin } from "./cli.js";
export interface DevinCredentials {
apiKey: string;
apiServerUrl: string;
webappHost: string;
apiUrl: string;
path: string;
}
const CREDENTIALS_PATH = join(homedir(), ".local/share/devin/credentials.toml");
function parseTomlStrings(text: string): Record<string, string> {
const out: Record<string, string> = {};
for (const line of text.split("\n")) {
const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*"(.*)"\s*$/);
if (match) out[match[1]] = match[2];
}
return out;
}
export function credentialsPath(): string {
return CREDENTIALS_PATH;
}
export function readCredentials(): DevinCredentials | null {
if (!existsSync(CREDENTIALS_PATH)) return null;
const raw = parseTomlStrings(readFileSync(CREDENTIALS_PATH, "utf8"));
const apiKey = raw.windsurf_api_key || raw.api_key;
if (!apiKey) return null;
return {
apiKey,
apiServerUrl: (raw.api_server_url || "https://server.codeium.com").replace(/\/$/, ""),
webappHost: raw.devin_webapp_host || "app.devin.ai",
apiUrl: raw.devin_api_url || "https://api.devin.ai",
path: CREDENTIALS_PATH,
};
}
export async function authStatus(): Promise<{
loggedIn: boolean;
summary: string;
}> {
const creds = readCredentials();
try {
const { stdout, stderr, code } = await runDevin(["auth", "status"], { timeoutMs: 15_000 });
const text = `${stdout}\n${stderr}`.trim();
const loggedIn = code === 0 && /logged in/i.test(text);
return { loggedIn: loggedIn || Boolean(creds), summary: text || (creds ? "credentials.toml present" : "not signed in") };
} catch (error) {
if (creds) {
return { loggedIn: true, summary: `Devin credentials present at ${CREDENTIALS_PATH}` };
}
return {
loggedIn: false,
summary: error instanceof Error ? error.message : String(error),
};
}
}
export async function loginWithCli(): Promise<DevinCredentials> {
const already = readCredentials();
const status = await authStatus();
if (already && status.loggedIn) return already;
const { code } = await runDevin(["auth", "login"], { inheritStdio: true });
const creds = readCredentials();
if (!creds) {
throw new Error(
`\`devin auth login\` ${code === 0 ? "finished" : `exited ${code}`} but ${CREDENTIALS_PATH} is missing. Run \`devin auth login\` yourself, then /login devin again.`,
);
}
return creds;
}
+103
View File
@@ -0,0 +1,103 @@
import { randomUUID } from "node:crypto";
import { buildMetadata } from "./metadata.js";
import { encodeMessage, iterFields } from "./wire.js";
export interface MintedUserJwt {
jwt: string;
expiresAt: number;
}
function anySignal(signals: AbortSignal[]): AbortSignal {
const builtin = (AbortSignal as unknown as { any?: (s: AbortSignal[]) => AbortSignal }).any;
if (typeof builtin === "function") return builtin(signals);
const controller = new AbortController();
const onAbort = (reason: unknown): void => {
if (!controller.signal.aborted) controller.abort(reason);
};
for (const signal of signals) {
if (signal.aborted) {
onAbort(signal.reason);
break;
}
signal.addEventListener("abort", () => onAbort(signal.reason), { once: true });
}
return controller.signal;
}
export async function mintUserJwt(
apiKey: string,
host: string,
signal?: AbortSignal,
): Promise<MintedUserJwt> {
const metadata = buildMetadata({
apiKey,
sessionId: randomUUID(),
requestId: BigInt(Date.now()),
triggerId: randomUUID(),
});
const timeout = AbortSignal.timeout(30_000);
const combined = signal ? anySignal([signal, timeout]) : timeout;
const resp = await fetch(`${host.replace(/\/$/, "")}/exa.auth_pb.AuthService/GetUserJwt`, {
method: "POST",
headers: {
"Content-Type": "application/proto",
"Connect-Protocol-Version": "1",
},
body: new Uint8Array(encodeMessage(1, metadata)),
signal: combined,
});
const buf = Buffer.from(await resp.arrayBuffer());
if (!resp.ok) {
throw new Error(`GetUserJwt HTTP ${resp.status}: ${buf.toString("utf8").slice(0, 240)}`);
}
let jwt: string | null = null;
for (const field of iterFields(buf)) {
if (field.num === 1 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const value = field.value.toString("utf8");
if (value.startsWith("eyJ")) {
jwt = value;
break;
}
}
}
if (!jwt) throw new Error("GetUserJwt returned no JWT");
let expiresAt = Math.floor(Date.now() / 1000) + 600;
try {
const payload = jwt.split(".")[1] ?? "";
const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
const parsed = JSON.parse(Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"));
if (typeof parsed.exp === "number") expiresAt = parsed.exp;
} catch {
// keep fallback
}
return { jwt, expiresAt };
}
let cache: { jwt: string; expiresAt: number; apiKey: string; host: string } | null = null;
const inFlight = new Map<string, Promise<MintedUserJwt>>();
export async function getCachedUserJwt(apiKey: string, host: string, signal?: AbortSignal): Promise<string> {
const now = Math.floor(Date.now() / 1000);
if (cache && cache.apiKey === apiKey && cache.host === host && cache.expiresAt > now + 60) {
return cache.jwt;
}
const key = `${host}\x1f${apiKey}`;
const existing = inFlight.get(key);
if (existing) return (await existing).jwt;
const promise = mintUserJwt(apiKey, host, signal);
inFlight.set(key, promise);
try {
const minted = await promise;
cache = { jwt: minted.jwt, expiresAt: minted.expiresAt, apiKey, host };
return minted.jwt;
} finally {
inFlight.delete(key);
}
}
export function clearCachedUserJwt(): void {
cache = null;
inFlight.clear();
}
+44
View File
@@ -0,0 +1,44 @@
import {
encodeMessage,
encodeString,
encodeTimestampBody,
encodeVarintField,
} from "./wire.js";
const DEFAULT_VERSION = "3000.4.25";
export interface MetadataInput {
apiKey: string;
userJwt?: string;
sessionId: string;
requestId: bigint;
triggerId: string;
version?: string;
}
export function buildMetadata(input: MetadataInput): Buffer {
const version = input.version ?? DEFAULT_VERSION;
const os =
process.platform === "darwin"
? "darwin"
: process.platform === "win32"
? "windows"
: "linux";
const parts: Buffer[] = [
encodeString(1, "devin"),
encodeString(2, version),
encodeString(3, input.apiKey),
encodeString(4, "en"),
encodeString(5, os),
encodeString(7, version),
encodeVarintField(9, input.requestId),
encodeString(10, input.sessionId),
encodeString(12, "devin"),
encodeMessage(16, encodeTimestampBody()),
encodeString(25, input.triggerId),
encodeString(26, "Unset"),
encodeString(28, "devin"),
];
if (input.userJwt) parts.push(encodeString(21, input.userJwt));
return Buffer.concat(parts);
}
+223
View File
@@ -0,0 +1,223 @@
import type { ThinkingLevelMap } from "@earendil-works/pi-ai";
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
import { runDevin } from "./cli.js";
export interface DevinVariant {
model_uid: string;
label: string;
max_context_tokens?: number;
max_output_tokens?: number;
cost_tier?: string;
cost_summary?: string;
is_new?: boolean;
is_beta?: boolean;
}
export interface DevinFamily {
family_label: string;
family_uid: string;
slug: string;
aliases?: string[];
variants: DevinVariant[];
}
export interface DevinCatalog {
families: DevinFamily[];
}
const THINKING_ORDER = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
function parseCost(summary?: string): ProviderModelConfig["cost"] {
const empty = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
if (!summary) return empty;
const input = summary.match(/\$([0-9.]+)\s*\/\s*MTok In/i);
const output = summary.match(/\$([0-9.]+)\s*\/\s*MTok Out/i);
const inCost = input ? Number(input[1]) : 0;
const outCost = output ? Number(output[1]) : 0;
return {
input: inCost,
output: outCost,
cacheRead: Number((inCost * 0.1).toFixed(4)),
cacheWrite: Number((inCost * 1.25).toFixed(4)),
};
}
function variantKey(uid: string): string | null {
const suffixes = [
"none-priority",
"low-priority",
"medium-priority",
"high-priority",
"xhigh-priority",
"max-priority",
"low-fast",
"medium-fast",
"high-fast",
"xhigh-fast",
"max-fast",
"thinking-1m",
"thinking",
"none",
"low",
"medium",
"high",
"xhigh",
"max",
"minimal",
];
for (const suffix of suffixes) {
if (uid === suffix || uid.endsWith(`-${suffix}`)) return suffix;
}
return null;
}
function thinkingFromSuffix(suffix: string | null): keyof ThinkingLevelMap | null {
if (!suffix) return "high";
if (suffix === "none" || suffix === "none-priority") return "off";
if (suffix === "minimal") return "minimal";
if (suffix.startsWith("low")) return "low";
if (suffix.startsWith("medium")) return "medium";
if (suffix.startsWith("high") && !suffix.startsWith("xhigh")) return "high";
if (suffix.startsWith("xhigh")) return "xhigh";
if (suffix.startsWith("max")) return "max";
if (suffix.includes("thinking")) return "high";
return null;
}
function preferredDefault(map: ThinkingLevelMap): string | undefined {
for (const level of ["high", "medium", "max", "xhigh", "low", "minimal", "off"] as const) {
const value = map[level];
if (typeof value === "string") return value;
}
return undefined;
}
function familyToModels(family: DevinFamily): ProviderModelConfig[] {
const usable = family.variants.filter((variant) => {
const key = variantKey(variant.model_uid);
return !key || (!key.includes("priority") && !key.includes("fast") && key !== "thinking-1m");
});
const source = usable.length > 0 ? usable : family.variants;
const thinkingLevelMap: ThinkingLevelMap = {};
for (const variant of source) {
const level = thinkingFromSuffix(variantKey(variant.model_uid));
if (level && thinkingLevelMap[level] === undefined) {
thinkingLevelMap[level] = variant.model_uid;
}
}
const defaultUid = preferredDefault(thinkingLevelMap) ?? source[0]?.model_uid ?? family.family_uid;
const sample = source.find((variant) => variant.model_uid === defaultUid) ?? source[0];
if (!sample) return [];
const mappedLevels = THINKING_ORDER.filter((level) => typeof thinkingLevelMap[level] === "string");
const reasoning = mappedLevels.length > 1;
return [
{
id: defaultUid,
name: family.family_label || family.slug || defaultUid,
reasoning,
thinkingLevelMap: reasoning ? thinkingLevelMap : undefined,
input: ["text", "image"],
cost: parseCost(sample.cost_summary),
contextWindow: sample.max_context_tokens ?? 256_000,
maxTokens: sample.max_output_tokens ?? 128_000,
},
];
}
export const FALLBACK_MODELS: ProviderModelConfig[] = [
{
id: "claude-opus-5-high",
name: "Claude Opus 5",
reasoning: true,
thinkingLevelMap: {
low: "claude-opus-5-low",
medium: "claude-opus-5-medium",
high: "claude-opus-5-high",
xhigh: "claude-opus-5-xhigh",
max: "claude-opus-5-max",
},
input: ["text", "image"],
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "claude-5-fable-high",
name: "Claude Fable 5",
reasoning: true,
thinkingLevelMap: {
low: "claude-5-fable-low",
medium: "claude-5-fable-medium",
high: "claude-5-fable-high",
xhigh: "claude-5-fable-xhigh",
max: "claude-5-fable-max",
},
input: ["text", "image"],
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "gpt-5-6-sol-high",
name: "GPT-5.6 Sol",
reasoning: true,
thinkingLevelMap: {
off: "gpt-5-6-sol-none",
low: "gpt-5-6-sol-low",
medium: "gpt-5-6-sol-medium",
high: "gpt-5-6-sol-high",
xhigh: "gpt-5-6-sol-xhigh",
max: "gpt-5-6-sol-max",
},
input: ["text", "image"],
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "swe-1-7",
name: "SWE-1.7",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 262_000,
maxTokens: 128_000,
},
];
export function modelsFromCatalog(catalog: DevinCatalog | null): ProviderModelConfig[] {
if (!catalog?.families?.length) return FALLBACK_MODELS;
const models = catalog.families.flatMap(familyToModels);
return models.length > 0 ? models : FALLBACK_MODELS;
}
export async function loadCliCatalog(): Promise<DevinCatalog | null> {
const { stdout, code, stderr } = await runDevin(["models", "list", "--format", "json"], {
timeoutMs: 20_000,
});
if (code !== 0) {
throw new Error(stderr.trim() || `devin models list exited ${code}`);
}
const parsed = JSON.parse(stdout) as DevinCatalog;
if (!parsed?.families) return null;
return parsed;
}
export function resolveModelUid(
modelId: string,
thinkingLevelMap: ThinkingLevelMap | undefined,
reasoning?: string,
): string {
if (reasoning && thinkingLevelMap) {
const mapped = thinkingLevelMap[reasoning as keyof ThinkingLevelMap];
if (typeof mapped === "string") return mapped;
}
if (thinkingLevelMap) {
const fallback = preferredDefault(thinkingLevelMap);
if (fallback) return fallback;
}
return modelId;
}
+568
View File
@@ -0,0 +1,568 @@
import { randomUUID } from "node:crypto";
import * as zlib from "node:zlib";
import {
type Api,
type AssistantMessage,
type AssistantMessageEventStream,
type Context,
type Model,
type SimpleStreamOptions,
calculateCost,
createAssistantMessageEventStream,
} from "@earendil-works/pi-ai";
import { mapContextToChat, type ChatHistoryItem, type ContentPart, type ToolDef } from "./context-map.js";
import { getCachedUserJwt } from "./jwt.js";
import { buildMetadata } from "./metadata.js";
import { resolveModelUid } from "./models.js";
import {
encodeFixed64Field,
encodeMessage,
encodeString,
encodeVarintField,
frameConnectStream,
iterFields,
} from "./wire.js";
const SOURCE_BY_ROLE: Record<string, number> = {
user: 1,
assistant: 2,
system: 1,
tool: 4,
};
export type CloudChatEvent =
| { kind: "text"; text: string }
| { kind: "reasoning"; text: string }
| { kind: "tool_call_start"; id: string; name: string }
| { kind: "tool_call_args"; argsDelta: string; id?: string }
| { kind: "finish"; reason: "stop" | "tool_calls" | "length" | "content_filter" }
| {
kind: "usage";
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
cachedInputTokens?: number;
cacheCreationInputTokens?: number;
};
function normalizeContent(content: string | ContentPart[]): ContentPart[] {
if (typeof content === "string") return [{ type: "text", text: content }];
return content;
}
function collapseSystemIntoUser(messages: ChatHistoryItem[]): ChatHistoryItem[] {
const out: ChatHistoryItem[] = [];
let pending: string[] = [];
const textOf = (content: string | ContentPart[]) =>
normalizeContent(content)
.filter((part) => part.type === "text" && part.text)
.map((part) => part.text as string)
.join("\n");
for (const message of messages) {
if (message.role === "system") {
const text = textOf(message.content);
if (text) pending.push(text);
continue;
}
if (message.role === "user" && pending.length > 0) {
const userParts = normalizeContent(message.content);
const userText = textOf(userParts);
const images = userParts.filter((part) => part.type === "image");
out.push({
role: "user",
content: [{ type: "text", text: `<system>\n${pending.join("\n\n")}\n</system>\n${userText}` }, ...images],
});
pending = [];
continue;
}
out.push(message);
}
if (pending.length > 0) {
out.push({ role: "user", content: `<system>\n${pending.join("\n\n")}\n</system>` });
}
return out;
}
function encodeImageData(img: { mimeType?: string; base64Data?: string }): Buffer {
return Buffer.concat([
encodeString(1, img.base64Data ?? ""),
encodeString(2, img.mimeType ?? "image/png"),
]);
}
function encodeChatToolCall(tc: { id: string; name: string; arguments: string }): Buffer {
return Buffer.concat([encodeString(1, tc.id), encodeString(2, tc.name), encodeString(3, tc.arguments)]);
}
function encodeChatMessagePrompt(
content: ContentPart[],
source: number,
opts?: { toolCallId?: string; toolCalls?: Array<{ id: string; name: string; arguments: string }> },
): Buffer {
const text = content
.filter((part) => part.type === "text")
.map((part) => part.text ?? "")
.join("\n");
const parts: Buffer[] = [
encodeVarintField(2, source),
encodeString(3, text),
encodeVarintField(4, Math.max(1, Math.floor(text.length / 4))),
encodeVarintField(5, 1),
];
if (opts?.toolCallId) parts.push(encodeString(7, opts.toolCallId));
for (const tc of opts?.toolCalls ?? []) parts.push(encodeMessage(6, encodeChatToolCall(tc)));
for (const img of content.filter((part) => part.type === "image")) {
parts.push(encodeMessage(10, encodeImageData(img)));
}
return Buffer.concat(parts);
}
function encodeCompletionConfiguration(maxOutputTokens?: number): Buffer {
return Buffer.concat([
encodeVarintField(1, 1),
encodeVarintField(2, 64_000),
encodeVarintField(3, maxOutputTokens ?? 128_000),
encodeFixed64Field(5, 0.7),
encodeFixed64Field(6, 0.95),
encodeVarintField(7, 50),
encodeFixed64Field(8, 1.0),
encodeFixed64Field(11, 1.0),
]);
}
function encodeToolDef(tool: ToolDef): Buffer {
const description = tool.description.length > 6_998 ? `${tool.description.slice(0, 6_995)}...` : tool.description;
return Buffer.concat([
encodeString(1, tool.name),
encodeString(2, description),
encodeString(3, JSON.stringify(tool.parameters ?? {})),
]);
}
function buildGetChatMessageRequest(args: {
apiKey: string;
userJwt: string;
modelUid: string;
messages: ChatHistoryItem[];
tools?: ToolDef[];
cascadeId: string;
promptId: string;
sessionId: string;
requestId: bigint;
triggerId: string;
maxOutputTokens?: number;
}): Buffer {
const metadata = buildMetadata({
apiKey: args.apiKey,
userJwt: args.userJwt,
sessionId: args.sessionId,
requestId: args.requestId,
triggerId: args.triggerId,
});
const prompts = collapseSystemIntoUser(args.messages).map((message) =>
encodeMessage(
3,
encodeChatMessagePrompt(normalizeContent(message.content), SOURCE_BY_ROLE[message.role] ?? 1, {
toolCallId: message.role === "tool" ? message.tool_call_id : undefined,
toolCalls: message.role === "assistant" ? message.tool_calls : undefined,
}),
),
);
return Buffer.concat([
encodeMessage(1, metadata),
...prompts,
encodeVarintField(7, 5),
encodeMessage(8, encodeCompletionConfiguration(args.maxOutputTokens)),
...(args.tools ?? []).map((tool) => encodeMessage(10, encodeToolDef(tool))),
encodeString(16, args.cascadeId),
encodeString(21, args.modelUid),
encodeString(22, args.promptId),
]);
}
function* decodeChatFrame(proto: Buffer): Generator<CloudChatEvent> {
for (const field of iterFields(proto)) {
if (field.num === 3 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const text = field.value.toString("utf8");
if (text) yield { kind: "text", text };
} else if (field.num === 9 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const text = field.value.toString("utf8");
if (text) yield { kind: "reasoning", text };
} else if (field.num === 6 && field.wire === 2 && Buffer.isBuffer(field.value)) {
let id: string | undefined;
let name: string | undefined;
let argsDelta: string | undefined;
for (const inner of iterFields(field.value)) {
if (inner.wire === 2 && Buffer.isBuffer(inner.value)) {
const text = inner.value.toString("utf8");
if (inner.num === 1) id = text;
else if (inner.num === 2) name = text;
else if (inner.num === 3) argsDelta = text;
}
}
if (id !== undefined && name !== undefined) yield { kind: "tool_call_start", id, name };
if (argsDelta !== undefined) yield { kind: "tool_call_args", argsDelta, ...(id ? { id } : {}) };
} else if (field.num === 5 && field.wire === 0) {
const value = Number(field.value);
let reason: Extract<CloudChatEvent, { kind: "finish" }>['reason'] = "stop";
if (value === 10) reason = "tool_calls";
else if (value === 11) reason = "content_filter";
else if (value === 1 || value === 3) reason = "length";
yield { kind: "finish", reason };
} else if (field.num === 28 && field.wire === 2 && Buffer.isBuffer(field.value)) {
const usage = decodeUsage(field.value);
if (usage) yield usage;
}
}
}
function decodeUsage(buf: Buffer): CloudChatEvent | null {
let promptTokens: number | undefined;
let completionTokens: number | undefined;
let cachedInputTokens: number | undefined;
let cacheCreationInputTokens: number | undefined;
for (const field of iterFields(buf)) {
if (field.num !== 2 || field.wire !== 2 || !Buffer.isBuffer(field.value)) continue;
let metric: string | undefined;
let value: number | undefined;
for (const inner of iterFields(field.value)) {
if (inner.num === 5 && inner.wire === 2 && Buffer.isBuffer(inner.value)) {
metric = inner.value.toString("utf8");
} else if (inner.num === 4 && inner.wire === 2 && Buffer.isBuffer(inner.value)) {
for (const dim of iterFields(inner.value)) {
if (dim.num === 2 && dim.wire === 5 && Buffer.isBuffer(dim.value)) {
value = dim.value.readFloatLE(0);
}
}
}
}
if (!metric || value === undefined || !Number.isFinite(value)) continue;
const n = Math.round(value);
if (metric === "input_tokens") promptTokens = n;
else if (metric === "output_tokens") completionTokens = n;
else if (metric.includes("cached") || metric.includes("cache_read")) cachedInputTokens = n;
else if (metric.includes("cache_creation")) cacheCreationInputTokens = n;
}
if (promptTokens === undefined && completionTokens === undefined) return null;
return {
kind: "usage",
promptTokens,
completionTokens,
totalTokens: (promptTokens ?? 0) + (completionTokens ?? 0),
cachedInputTokens,
cacheCreationInputTokens,
};
}
const sessionCache = new Map<string, { sessionId: string; cascadeId: string }>();
function sessionIds(apiKey: string, host: string) {
const key = `${host}\x1f${apiKey}`;
let ids = sessionCache.get(key);
if (!ids) {
ids = { sessionId: randomUUID(), cascadeId: randomUUID() };
sessionCache.set(key, ids);
}
return ids;
}
async function* streamChatEvents(args: {
apiKey: string;
host: string;
modelUid: string;
messages: ChatHistoryItem[];
tools?: ToolDef[];
maxOutputTokens?: number;
signal?: AbortSignal;
}): AsyncGenerator<CloudChatEvent> {
const host = args.host.replace(/\/$/, "");
const userJwt = await getCachedUserJwt(args.apiKey, host, args.signal);
const ids = sessionIds(args.apiKey, host);
const proto = buildGetChatMessageRequest({
apiKey: args.apiKey,
userJwt,
modelUid: args.modelUid,
messages: args.messages,
tools: args.tools,
cascadeId: ids.cascadeId,
promptId: randomUUID(),
sessionId: ids.sessionId,
requestId: BigInt(Date.now()),
triggerId: randomUUID(),
maxOutputTokens: args.maxOutputTokens,
});
const resp = await fetch(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
method: "POST",
headers: {
"Content-Type": "application/connect+proto",
"Connect-Protocol-Version": "1",
"Connect-Content-Encoding": "gzip",
"Connect-Accept-Encoding": "gzip",
},
body: new Uint8Array(frameConnectStream(proto, true)),
signal: args.signal,
});
if (!resp.ok) {
throw new Error(`GetChatMessage HTTP ${resp.status}: ${(await resp.text()).slice(0, 300)}`);
}
if (!resp.body) throw new Error("GetChatMessage returned an empty body");
const reader = resp.body.getReader();
const queue: Buffer[] = [];
let queued = 0;
let sawEos = false;
let trailerError: string | null = null;
const peek = (n: number): Buffer | null => {
if (queued < n) return null;
if (queue.length === 1 && queue[0].length >= n) return queue[0].subarray(0, n);
const parts: Buffer[] = [];
let remaining = n;
for (const chunk of queue) {
if (remaining <= 0) break;
if (chunk.length <= remaining) {
parts.push(chunk);
remaining -= chunk.length;
} else {
parts.push(chunk.subarray(0, remaining));
remaining = 0;
}
}
return Buffer.concat(parts, n);
};
const drop = (n: number): void => {
queued -= n;
let remaining = n;
while (remaining > 0 && queue.length > 0) {
const head = queue[0];
if (head.length <= remaining) {
queue.shift();
remaining -= head.length;
} else {
queue[0] = head.subarray(remaining);
remaining = 0;
}
}
};
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (value) {
queue.push(Buffer.from(value));
queued += value.length;
}
while (queued >= 5) {
const header = peek(5);
if (!header) break;
const flags = header[0];
const len = header.readUInt32BE(1);
if (queued < 5 + len) break;
drop(5);
const raw = peek(len) ?? Buffer.alloc(0);
drop(len);
let payload = raw;
if (flags & 0x01) payload = zlib.gunzipSync(raw);
if (flags & 0x02) {
sawEos = true;
const text = payload.toString("utf8");
if (text.includes('"error"')) {
try {
const parsed = JSON.parse(text) as { error?: { message?: string } };
trailerError = parsed.error?.message ?? text;
} catch {
trailerError = text;
}
}
continue;
}
yield* decodeChatFrame(payload);
}
}
} finally {
try {
reader.releaseLock();
} catch {
// ignore
}
try {
void resp.body?.cancel();
} catch {
// ignore
}
}
if (trailerError) throw new Error(trailerError);
if (!sawEos) throw new Error("Devin stream ended without an EOS trailer");
}
export function streamDevin(
model: Model<Api>,
context: Context,
options?: SimpleStreamOptions,
): AssistantMessageEventStream {
const stream = createAssistantMessageEventStream();
void (async () => {
const output: AssistantMessage = {
role: "assistant",
content: [],
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
};
let textOpen = false;
let thinkingOpen = false;
let toolIndex = -1;
let partialJson = "";
let toolId = "";
let toolName = "";
const closeText = () => {
if (!textOpen) return;
const idx = output.content.length - 1;
const block = output.content[idx];
if (block.type === "text") {
stream.push({ type: "text_end", contentIndex: idx, content: block.text, partial: output });
}
textOpen = false;
};
const closeThinking = () => {
if (!thinkingOpen) return;
const idx = output.content.length - 1;
const block = output.content[idx];
if (block.type === "thinking") {
stream.push({ type: "thinking_end", contentIndex: idx, content: block.thinking, partial: output });
}
thinkingOpen = false;
};
const closeTool = () => {
if (toolIndex < 0) return;
const block = output.content[toolIndex];
if (block.type === "toolCall") {
try {
block.arguments = JSON.parse(partialJson);
} catch {
// keep last parsed object
}
stream.push({
type: "toolcall_end",
contentIndex: toolIndex,
toolCall: { type: "toolCall", id: toolId, name: toolName, arguments: block.arguments },
partial: output,
});
}
toolIndex = -1;
};
try {
const apiKey = options?.apiKey;
if (!apiKey) throw new Error("No Devin credentials. Run /login devin (uses the local Devin CLI).");
const host = (options?.env?.DEVIN_API_SERVER_URL || "https://server.codeium.com").replace(/\/$/, "");
const modelUid = resolveModelUid(model.id, model.thinkingLevelMap, options?.reasoning);
const mapped = mapContextToChat(context);
stream.push({ type: "start", partial: output });
for await (const event of streamChatEvents({
apiKey,
host,
modelUid,
messages: mapped.messages,
tools: mapped.tools.length > 0 ? mapped.tools : undefined,
maxOutputTokens: options?.maxTokens,
signal: options?.signal,
})) {
if (event.kind === "text") {
closeThinking();
if (!textOpen) {
output.content.push({ type: "text", text: "" });
stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
textOpen = true;
}
const idx = output.content.length - 1;
const block = output.content[idx];
if (block.type === "text") {
block.text += event.text;
stream.push({ type: "text_delta", contentIndex: idx, delta: event.text, partial: output });
}
} else if (event.kind === "reasoning") {
closeText();
if (!thinkingOpen) {
output.content.push({ type: "thinking", thinking: "" });
stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
thinkingOpen = true;
}
const idx = output.content.length - 1;
const block = output.content[idx];
if (block.type === "thinking") {
block.thinking += event.text;
stream.push({ type: "thinking_delta", contentIndex: idx, delta: event.text, partial: output });
}
} else if (event.kind === "tool_call_start") {
closeText();
closeThinking();
closeTool();
toolId = event.id;
toolName = event.name;
partialJson = "";
output.content.push({ type: "toolCall", id: event.id, name: event.name, arguments: {} });
toolIndex = output.content.length - 1;
stream.push({ type: "toolcall_start", contentIndex: toolIndex, partial: output });
} else if (event.kind === "tool_call_args") {
if (toolIndex < 0) continue;
partialJson += event.argsDelta;
const block = output.content[toolIndex];
if (block.type === "toolCall") {
try {
block.arguments = JSON.parse(partialJson);
} catch {
// incomplete json
}
}
stream.push({ type: "toolcall_delta", contentIndex: toolIndex, delta: event.argsDelta, partial: output });
} else if (event.kind === "finish") {
closeText();
closeThinking();
closeTool();
output.stopReason =
event.reason === "tool_calls" ? "toolUse" : event.reason === "length" ? "length" : "stop";
} else if (event.kind === "usage") {
output.usage.input = event.promptTokens ?? 0;
output.usage.output = event.completionTokens ?? 0;
output.usage.cacheRead = event.cachedInputTokens ?? 0;
output.usage.cacheWrite = event.cacheCreationInputTokens ?? 0;
output.usage.totalTokens = event.totalTokens ?? output.usage.input + output.usage.output;
calculateCost(model, output.usage);
}
}
closeText();
closeThinking();
closeTool();
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
stream.end();
} catch (error) {
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = error instanceof Error ? error.message : String(error);
stream.push({ type: "error", reason: output.stopReason as "aborted" | "error", error: output });
stream.end();
}
})();
return stream;
}
+111
View File
@@ -0,0 +1,111 @@
import * as zlib from "node:zlib";
export function encodeVarint(value: number | bigint): Buffer {
const bytes: number[] = [];
let v = BigInt(value);
if (v < 0n) throw new RangeError(`encodeVarint: negative input (${value})`);
while (v > 127n) {
bytes.push(Number(v & 0x7fn) | 0x80);
v >>= 7n;
}
bytes.push(Number(v));
return Buffer.from(bytes);
}
export function encodeTag(fieldNum: number, wire: number): Buffer {
return encodeVarint((fieldNum << 3) | wire);
}
export function encodeString(fieldNum: number, s: string): Buffer {
const buf = Buffer.from(s, "utf8");
return Buffer.concat([encodeTag(fieldNum, 2), encodeVarint(buf.length), buf]);
}
export function encodeMessage(fieldNum: number, body: Buffer): Buffer {
return Buffer.concat([encodeTag(fieldNum, 2), encodeVarint(body.length), body]);
}
export function encodeVarintField(fieldNum: number, v: number | bigint): Buffer {
return Buffer.concat([encodeTag(fieldNum, 0), encodeVarint(v)]);
}
export function encodeFixed64Field(fieldNum: number, v: number): Buffer {
const b = Buffer.alloc(8);
b.writeDoubleLE(v, 0);
return Buffer.concat([encodeTag(fieldNum, 1), b]);
}
export function encodeTimestampBody(): Buffer {
const now = Date.now();
const seconds = Math.floor(now / 1000);
const nanos = (now % 1000) * 1_000_000;
return Buffer.concat([
encodeVarintField(1, seconds),
nanos > 0 ? encodeVarintField(2, nanos) : Buffer.alloc(0),
]);
}
export function decodeVarint(buf: Buffer, offset: number): [bigint, number] {
let res = 0n;
let shift = 0n;
let i = offset;
while (i < buf.length) {
const b = buf[i++];
res |= BigInt(b & 0x7f) << shift;
if (!(b & 0x80)) return [res, i];
shift += 7n;
}
throw new Error("truncated varint");
}
export interface ProtoField {
num: number;
wire: number;
value: bigint | Buffer;
}
export function* iterFields(buf: Buffer): Generator<ProtoField> {
let i = 0;
while (i < buf.length) {
const [tagBig, next] = decodeVarint(buf, i);
i = next;
const tag = Number(tagBig);
const num = tag >> 3;
const wire = tag & 0x7;
if (wire === 0) {
const [v, after] = decodeVarint(buf, i);
i = after;
yield { num, wire, value: v };
} else if (wire === 1) {
if (i + 8 > buf.length) return;
yield { num, wire, value: buf.subarray(i, i + 8) };
i += 8;
} else if (wire === 2) {
const [n, after] = decodeVarint(buf, i);
i = after;
const len = Number(n);
if (len < 0 || i + len > buf.length) return;
yield { num, wire, value: buf.subarray(i, i + len) };
i += len;
} else if (wire === 5) {
if (i + 4 > buf.length) return;
yield { num, wire, value: buf.subarray(i, i + 4) };
i += 4;
} else {
return;
}
}
}
export function frameConnectStream(body: Buffer, compress = true): Buffer {
let payload = body;
let flags = 0;
if (compress) {
payload = zlib.gzipSync(body);
flags |= 0x01;
}
const header = Buffer.alloc(5);
header[0] = flags;
header.writeUInt32BE(payload.length, 1);
return Buffer.concat([header, payload]);
}
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["extensions/**/*.ts", "src/**/*.ts"]
}