billion-context 0.1.89 → 0.1.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -24,6 +24,9 @@ function stateDir() {
24
24
  }
25
25
 
26
26
  // src/instance.ts
27
+ function isProxyInstanceFile(v) {
28
+ return v !== void 0 && "instanceId" in v;
29
+ }
27
30
  function readProxyInstanceFile(file) {
28
31
  let raw;
29
32
  try {
@@ -64,6 +67,15 @@ function readProxyInstanceFile(file) {
64
67
  function instanceFilePath() {
65
68
  return path2.join(stateDir(), "proxy-origin");
66
69
  }
70
+ function isPidAlive(pid) {
71
+ if (!Number.isInteger(pid) || pid <= 0) return false;
72
+ try {
73
+ process.kill(pid, 0);
74
+ return true;
75
+ } catch (err) {
76
+ return err.code === "EPERM";
77
+ }
78
+ }
67
79
 
68
80
  // src/mcp.ts
69
81
  var VERSION = (() => {
@@ -76,11 +88,22 @@ var VERSION = (() => {
76
88
  }
77
89
  })();
78
90
  var DEFAULT_PROXY_ORIGIN = "http://127.0.0.1:8787";
91
+ var warnedStaleRecord = false;
79
92
  function resolveProxyOrigin() {
80
93
  const fromEnv = process.env.BILI_MCP_PROXY?.trim();
81
94
  if (fromEnv && fromEnv.length > 0) return fromEnv;
82
95
  const discovered = readProxyInstanceFile();
83
- if (discovered && /^https?:\/\/\S+$/.test(discovered.origin)) return discovered.origin;
96
+ if (discovered && /^https?:\/\/\S+$/.test(discovered.origin)) {
97
+ if (isProxyInstanceFile(discovered) && discovered.pid > 0 && !isPidAlive(discovered.pid)) {
98
+ if (!warnedStaleRecord) {
99
+ warnedStaleRecord = true;
100
+ process.stderr.write(`[bili-mcp] recorded bili proxy (${discovered.origin}, pid ${discovered.pid}) is not running \u2014 falling back to ${DEFAULT_PROXY_ORIGIN}; set BILI_MCP_PROXY if your proxy listens elsewhere
101
+ `);
102
+ }
103
+ return DEFAULT_PROXY_ORIGIN;
104
+ }
105
+ return discovered.origin;
106
+ }
84
107
  return DEFAULT_PROXY_ORIGIN;
85
108
  }
86
109
  var TOOL_TIMEOUT_MS = 6e4;
package/dist/mcp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp.ts","../src/instance.ts","../src/paths.ts"],"sourcesContent":["// MCP stdio thin shell for launcher mode (#162): a single \"bili\" MCP server\n// the hosts load via --mcp-config / -c mcp_servers.bili. It fetches the\n// proxy's plugin manifest (single source of truth — zero schema drift),\n// exposes the 4 ACP tools over stdio JSON-RPC, and forwards executes to\n// POST /__bili/plugin/tool. Claude Code passes its session id via the MCP\n// initialize request's _meta.ui.sessionId (documented SessionStart context);\n// we also accept BILI_CONVERSATION_ID env (codex spawn-time registration).\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { readProxyInstanceFile } from \"./instance.js\";\n\nconst VERSION = (() => {\n try {\n const here = fileURLToPath(import.meta.url);\n const pkg = path.join(path.dirname(here), \"..\", \"package.json\");\n return (JSON.parse(fs.readFileSync(pkg, \"utf8\")).version as string) ?? \"dev\";\n } catch {\n return \"dev\";\n }\n})();\ntype JsonRpcId = string | number | null;\n\ntype McpToolDef = {\n name: string;\n description?: string;\n inputSchema: unknown;\n};\n// Claude Code passes the session id as an env var to MCP children (verified\n// against claude 2.1.227: CLAUDE_CODE_SESSION_ID) — and puts the SAME id on\n// every model request (x-claude-code-session-id), so binding is by identity.\n// BILI_CONVERSATION_ID (launcher-spawned hosts like codex) has no matching\n// request id — binding is headless (next NEW session).\nconst DEFAULT_PROXY_ORIGIN = \"http://127.0.0.1:8787\";\n\nexport function resolveProxyOrigin(): string {\n const fromEnv = process.env.BILI_MCP_PROXY?.trim();\n if (fromEnv && fromEnv.length > 0) return fromEnv;\n const discovered = readProxyInstanceFile();\n if (discovered && /^https?:\\/\\/\\S+$/.test(discovered.origin)) return discovered.origin;\n return DEFAULT_PROXY_ORIGIN;\n}\n\nconst TOOL_TIMEOUT_MS = 60_000;\nconst CONVERSATION_FROM_ENV = process.env.CLAUDE_CODE_SESSION_ID?.trim() || process.env.BILI_CONVERSATION_ID?.trim() || undefined;\nconst IDENTITY_BINDING = Boolean(process.env.CLAUDE_CODE_SESSION_ID?.trim());\nlet manifestTools: McpToolDef[] = [];\nlet conversationId = CONVERSATION_FROM_ENV;\nlet registered = false;\nlet initialized = false;\nfunction send(msg: unknown): void {\n process.stdout.write(JSON.stringify(msg) + \"\\n\");\n}\n\nfunction sendResult(id: JsonRpcId, result: unknown): void {\n if (id === null) return; // notification — no response expected\n send({ jsonrpc: \"2.0\", id, result });\n}\n\nfunction sendError(id: JsonRpcId, code: number, message: string): void {\n if (id === null) return;\n send({ jsonrpc: \"2.0\", id, error: { code, message } });\n}\n\nasync function fetchManifest(): Promise<void> {\n const res = await fetch(`${resolveProxyOrigin()}/__bili/plugin/manifest`, { signal: AbortSignal.timeout(5000) });\n if (!res.ok) throw new Error(`manifest fetch failed: ${res.status}`);\n const data = (await res.json()) as { tools?: Record<string, { name: string; description?: string; input_schema?: unknown }[]> };\n // Anthropic wire shape is the canonical MCP-compatible schema source.\n const anthropic = data.tools?.anthropic ?? [];\n manifestTools = anthropic.map((t) => ({ name: t.name, description: t.description, inputSchema: t.input_schema }));\n if (manifestTools.length === 0) throw new Error(\"manifest served no anthropic tools\");\n}\n\nlet manifestPromise: Promise<void> | null = null;\nfunction ensureManifest(): Promise<void> {\n manifestPromise ??= fetchManifest().catch((err) => {\n manifestPromise = null;\n throw err;\n });\n return manifestPromise;\n}\n\nexport async function forwardTool(tool: string, args: unknown, timeoutMs: number = TOOL_TIMEOUT_MS): Promise<string> {\n let res: Response;\n try {\n res = await fetch(`${resolveProxyOrigin()}/__bili/plugin/tool`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ conversationId, tool, args }),\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (err) {\n if (err instanceof Error && err.name === \"TimeoutError\") throw new Error(`tool forward timed out after ${timeoutMs}ms: ${tool}`);\n throw err;\n }\n const data = (await res.json()) as { ok?: boolean; result?: string; error?: string };\n if (!res.ok || !data.ok) throw new Error(data.error ?? `tool forward failed: ${res.status}`);\n return data.result ?? \"\";\n}\n\nconst ERR_TOOL = -32602;\n\nasync function handleMessage(msg: {\n id?: JsonRpcId;\n method?: string;\n params?: {\n _meta?: { ui?: { sessionId?: string } };\n [k: string]: unknown;\n };\n}): Promise<void> {\n const { id = null, method } = msg;\n const params = typeof msg.params === \"object\" && msg.params !== null ? msg.params : {};\n switch (method) {\n case \"initialize\": {\n // Session id arrives as an env var on the child process (claude)\n // or is injected at spawn time (launcher hosts). The MCP spec\n // guarantees the host waits for this response before calling\n // tools, and claude -p fires its first model request around the\n // same time — registering here is still the earliest we can be.\n // Identity-mode registrations bind on any later request, so the\n // race only matters for headless mode.\n const fromMeta = params._meta?.ui?.sessionId?.trim();\n if (fromMeta) conversationId ??= fromMeta;\n initialized = true;\n if (conversationId && !registered) {\n const registerFetch = fetch(`${resolveProxyOrigin()}/__bili/plugin/register`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ conversationId, agent: \"mcp\", identity: IDENTITY_BINDING }),\n signal: AbortSignal.timeout(5000),\n });\n registered = true; // issue-once: a repeated initialize must not re-register\n if (IDENTITY_BINDING) {\n // Identity-mode binding survives any arrival order —\n // respond immediately so pipelined hosts are not stuck\n // behind the register round-trip.\n void registerFetch.catch(() => {});\n } else {\n // Headless binding is order-sensitive: the register MUST\n // land before the host's first model request, and hosts\n // that pipeline tools/list would otherwise race past it.\n await registerFetch.catch(() => {});\n }\n }\n sendResult(id, {\n protocolVersion: \"2025-06-18\",\n serverInfo: { name: \"bili\", version: VERSION },\n capabilities: { tools: {} },\n });\n return;\n }\n case \"notifications/initialized\":\n return;\n case \"tools/list\": {\n if (!initialized) {\n sendError(id, -32002, \"server not initialized\");\n return;\n }\n try {\n await ensureManifest();\n sendResult(id, { tools: manifestTools });\n } catch (err) {\n sendError(id, -32003, `bili proxy unreachable at ${resolveProxyOrigin()} (${err instanceof Error ? err.message : String(err)}) — start bili or set BILI_MCP_PROXY`);\n }\n return;\n }\n case \"tools/call\": {\n const tool = typeof params.name === \"string\" ? params.name : \"\";\n const args = params.arguments && typeof params.arguments === \"object\" ? params.arguments : {};\n if (!tool) {\n sendError(id, ERR_TOOL, \"params.name is required\");\n return;\n }\n if (!conversationId) {\n sendError(id, ERR_TOOL, \"no conversation id (set BILI_CONVERSATION_ID or connect via Claude Code MCP session meta)\");\n return;\n }\n try {\n const text = await forwardTool(tool, args);\n sendResult(id, { content: [{ type: \"text\", text }], isError: false });\n } catch (err) {\n // Tool-level failures are results (isError), not JSON-RPC\n // errors, so the host surfaces them to the model.\n sendResult(id, { content: [{ type: \"text\", text: `bili tool error: ${err instanceof Error ? err.message : String(err)}` }], isError: true });\n }\n return;\n }\n case \"ping\":\n sendResult(id, {});\n return;\n default:\n if (method?.startsWith(\"notifications/\")) return;\n sendError(id, -32601, `method not found: ${method ?? \"(none)\"}`);\n }\n}\nasync function mcpMain(): Promise<void> {\n let buf = \"\";\n process.stdin.setEncoding(\"utf8\");\n process.stdin.on(\"data\", (chunk: string) => {\n buf += chunk;\n let nl: number;\n while ((nl = buf.indexOf(\"\\n\")) >= 0) {\n const line = buf.slice(0, nl).trim();\n buf = buf.slice(nl + 1);\n if (!line) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (parsed && typeof parsed === \"object\") {\n void handleMessage(parsed as Parameters<typeof handleMessage>[0]);\n }\n }\n });\n process.stdin.on(\"end\", () => process.exit(0));\n}\n\n/** CLI entry (`bili mcp`): the stdio loop keeps the process alive. */\nexport function runMcpStdio(): void {\n void mcpMain();\n}\n\n// Direct entry (dist/mcp.js spawned by the injected MCP config, or the ts\n// source under tsx in tests): run only when invoked as the script itself,\n// never when imported by the CLI.\nif (process.argv[1] && /(?:^|[\\\\/])mcp\\.(?:ts|js)$/.test(process.argv[1])) {\n void mcpMain();\n}\n","import { createHash, randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { stateDir } from \"./paths.js\";\n\n/** Instance registry (#394/#403/#417): the proxy-origin file is upgraded from\n * a bare URL string to a JSON record identifying the live instance, so\n * readers (launcher attach, plugin install, MCP shells) can verify liveness\n * and config compatibility before trusting it. Readers from older builds\n * treat a JSON body as unreadable and fall back to their default origin; the\n * writer and all in-tree readers ship together, so the skew window is one\n * restart. */\nexport interface ProxyInstanceFile {\n origin: string;\n instanceId: string;\n pid: number;\n startedAt: number;\n host: string;\n port: number;\n passthrough: boolean;\n mitmDomains: string[];\n modelWindows: Record<string, number>;\n launchToken?: string;\n}\n\nexport function isProxyInstanceFile(v: ProxyInstanceFile | { origin: string } | undefined): v is ProxyInstanceFile {\n return v !== undefined && \"instanceId\" in v;\n}\n\nexport function readProxyInstanceFile(file?: string): ProxyInstanceFile | { origin: string } | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(file ?? instanceFilePath(), \"utf8\").trim();\n } catch {\n return undefined;\n }\n if (raw.startsWith(\"{\")) {\n try {\n const parsed = JSON.parse(raw) as Partial<ProxyInstanceFile>;\n if (typeof parsed.origin === \"string\" && /^https?:\\/\\/\\S+$/.test(parsed.origin)) {\n const windows: Record<string, number> = {};\n if (parsed.modelWindows && typeof parsed.modelWindows === \"object\") {\n for (const [k, v] of Object.entries(parsed.modelWindows)) {\n const n = Number(v);\n if (Number.isFinite(n) && n > 0) windows[k] = n;\n }\n }\n return {\n origin: parsed.origin,\n instanceId: typeof parsed.instanceId === \"string\" ? parsed.instanceId : \"\",\n pid: typeof parsed.pid === \"number\" ? parsed.pid : 0,\n startedAt: typeof parsed.startedAt === \"number\" ? parsed.startedAt : 0,\n host: typeof parsed.host === \"string\" ? parsed.host : \"\",\n port: typeof parsed.port === \"number\" ? parsed.port : 0,\n passthrough: Boolean(parsed.passthrough),\n mitmDomains: Array.isArray(parsed.mitmDomains) ? parsed.mitmDomains.map(String) : [],\n modelWindows: windows,\n launchToken: typeof parsed.launchToken === \"string\" ? parsed.launchToken : undefined,\n };\n }\n } catch {\n // fall through to legacy plain-string parsing\n }\n }\n if (/^https?:\\/\\/\\S+$/.test(raw)) return { origin: raw };\n return undefined;\n}\n\nexport function instanceFilePath(): string {\n return path.join(stateDir(), \"proxy-origin\");\n}\n\n/** tmp+fsync+rename (same shape as web/api.ts atomicWriteConfig) — a torn\n * write must never leave a half-file behind (#406 family). Shared by the\n * proxy-origin record and every registry marker. */\nfunction atomicWriteJson(obj: unknown, filePath: string): void {\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n let descriptor: number | undefined;\n try {\n descriptor = fs.openSync(tempPath, \"wx\", 0o644);\n fs.writeSync(descriptor, JSON.stringify(obj) + \"\\n\", null, \"utf8\");\n fs.fsyncSync(descriptor);\n fs.closeSync(descriptor);\n descriptor = undefined;\n fs.renameSync(tempPath, filePath);\n } catch (error) {\n if (descriptor !== undefined) {\n try {\n fs.closeSync(descriptor);\n } catch {}\n }\n try {\n fs.unlinkSync(tempPath);\n } catch {}\n throw error;\n }\n}\n\nexport function atomicWriteInstanceFile(info: ProxyInstanceFile, file?: string): void {\n atomicWriteJson(info, file ?? instanceFilePath());\n}\n\n/** Remove our record on shutdown — but only if the file still carries OUR\n * instanceId (a newer instance may have legitimately taken it over). */\nexport function clearProxyInstanceFile(instanceId: string, file?: string): void {\n const filePath = file ?? instanceFilePath();\n const current = readProxyInstanceFile(filePath);\n if (isProxyInstanceFile(current) && current.instanceId === instanceId) {\n try {\n fs.unlinkSync(filePath);\n } catch {}\n }\n}\n\n/** pid liveness (kill-0). EPERM means the process exists but is owned by\n * another user — still alive. */\nexport function isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\ninterface RegistryEntry {\n instanceId: string;\n pid: number;\n port: number;\n origin: string;\n startedAt: number;\n}\n\n/** Cross-instance liveness registry (#394/#527): one marker file per instance\n * under <state>/instances/<id>.json, so the registry IS the directory listing\n * and removal unlinks exactly one file — no shared read-modify-write, hence no\n * cross-process lost update. Dead owners are reaped on the next registration;\n * legacy instances.json is folded in read-only. Best-effort. */\nfunction registryDirPath(): string {\n return path.join(stateDir(), \"instances\");\n}\n\nfunction legacyRegistryFilePath(): string {\n return path.join(stateDir(), \"instances.json\");\n}\n\nconst SAFE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nfunction safeRegistryName(instanceId: string): string {\n if (instanceId !== \".\" && instanceId !== \"..\" && instanceId.length <= 128 && SAFE_NAME_RE.test(instanceId)) {\n return instanceId;\n }\n return createHash(\"sha256\").update(instanceId).digest(\"hex\");\n}\n\nfunction registryEntryFile(instanceId: string): string {\n return path.join(registryDirPath(), `${safeRegistryName(instanceId)}.json`);\n}\n\nfunction safeReadJson(file: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch {\n return undefined;\n }\n}\n\nfunction coerceEntry(value: unknown): RegistryEntry | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const o = value as Record<string, unknown>;\n if (typeof o.instanceId !== \"string\" || o.instanceId === \"\") return undefined;\n return {\n instanceId: o.instanceId,\n pid: typeof o.pid === \"number\" ? o.pid : 0,\n port: typeof o.port === \"number\" ? o.port : 0,\n origin: typeof o.origin === \"string\" ? o.origin : \"\",\n startedAt: typeof o.startedAt === \"number\" ? o.startedAt : 0,\n };\n}\n\nfunction readMarkerNames(): string[] {\n try {\n return fs.readdirSync(registryDirPath());\n } catch {\n return [];\n }\n}\n\nfunction readAllRegistryEntries(): RegistryEntry[] {\n const seen = new Set<string>();\n const out: RegistryEntry[] = [];\n for (const name of readMarkerNames()) {\n if (!name.endsWith(\".json\")) continue;\n const entry = coerceEntry(safeReadJson(path.join(registryDirPath(), name)));\n if (entry && !seen.has(entry.instanceId)) {\n seen.add(entry.instanceId);\n out.push(entry);\n }\n }\n const legacy = safeReadJson(legacyRegistryFilePath()) as { instances?: unknown } | undefined;\n if (legacy && Array.isArray(legacy.instances)) {\n for (const raw of legacy.instances) {\n const entry = coerceEntry(raw);\n if (entry && !seen.has(entry.instanceId)) {\n seen.add(entry.instanceId);\n out.push(entry);\n }\n }\n }\n return out;\n}\n\nfunction reapDeadMarkers(ours: string): void {\n for (const name of readMarkerNames()) {\n if (!name.endsWith(\".json\")) continue;\n const file = path.join(registryDirPath(), name);\n const entry = coerceEntry(safeReadJson(file));\n if (!entry || entry.instanceId === ours || isPidAlive(entry.pid)) continue;\n try {\n fs.unlinkSync(file);\n } catch {}\n }\n}\n\nexport function registerInstanceAndWarn(entry: RegistryEntry, warn: (msg: string) => void): void {\n const others = readAllRegistryEntries().filter((e) => e.instanceId !== entry.instanceId && isPidAlive(e.pid));\n for (const other of others) {\n warn(\n `another bili instance is running (pid ${other.pid}, ${other.origin}) — both processes will write the same sessions directory; stop one to avoid state pollution (#394)`,\n );\n }\n reapDeadMarkers(entry.instanceId);\n try {\n atomicWriteJson(entry, registryEntryFile(entry.instanceId));\n } catch {}\n}\n\n/** Unlinks only our own marker; cannot clobber another instance's entry (#527). */\nexport function unregisterInstance(instanceId: string): void {\n try {\n fs.unlinkSync(registryEntryFile(instanceId));\n } catch {}\n}\n","import { homedir } from \"node:os\";\nimport path from \"node:path\";\n\n/** XDG base-directory paths for billion-context.\n *\n * Follows the XDG Base Directory Specification so the proxy lands in the\n * conventional Linux/macOS locations instead of a bespoke ~/.bili/:\n *\n * config (user-edited, dotfile-managed):\n * $XDG_CONFIG_HOME/billion-context/billion-context.json\n * default: ~/.config/billion-context/billion-context.json\n *\n * data (persisted session state, grows over time):\n * $XDG_DATA_HOME/billion-context/sessions/\n * default: ~/.local/share/billion-context/sessions/\n *\n * Env overrides (highest priority) are kept so test runners and container\n * setups can relocate everything without touching the config file. */\n\nfunction xdg(envVar: string, fallback: string): string {\n const v = process.env[envVar];\n if (v && v.length > 0) return path.resolve(v);\n return path.join(homedir(), fallback);\n}\n\n/** Root config dir: user-editable configuration lives here. */\nexport function configDir(): string {\n return path.join(xdg(\"XDG_CONFIG_HOME\", \".config\"), \"billion-context\");\n}\n\n/** Main config file path. */\nexport function configFile(): string {\n const env = process.env.BILI_CONFIG_FILE;\n if (env && env.length > 0) return path.resolve(env);\n return path.join(configDir(), \"billion-context.json\");\n}\n\n/** Root data dir: persistent session state lives here. */\nexport function dataDir(): string {\n return path.join(xdg(\"XDG_DATA_HOME\", \".local/share\"), \"billion-context\");\n}\n\n/** Sessions dir: one JSON file per session. */\nexport function sessionsDir(): string {\n const env = process.env.BILI_SESSIONS_DIR;\n if (env && env.length > 0) return path.resolve(env);\n return path.join(dataDir(), \"sessions\");\n}\n\n/** Root cache dir: transient/ephemeral data (update-check throttle, etc.). */\nexport function cacheDir(): string {\n return path.join(xdg(\"XDG_CACHE_HOME\", \".cache\"), \"billion-context\");\n}\n\n/** Root state dir: log files and other per-host state. */\nexport function stateDir(): string {\n return path.join(xdg(\"XDG_STATE_HOME\", \".local/state\"), \"billion-context\");\n}\n\n/** Default log file path. */\nexport function defaultLogFile(): string {\n return path.join(stateDir(), \"bili.log\");\n}\n\n/** Origin of the most recently started proxy (best-effort discovery file for\n * host-spawned MCP shells that have no env passthrough: opencode/claude/codex). */\nexport function proxyOriginFile(): string {\n return path.join(stateDir(), \"proxy-origin\");\n}\n\n/** CA dir: root CA + dynamically-signed host certificates for MITM mode.\n * Lives under the data dir (the private key is sensitive but regenerable).\n * Created lazily by ca.ts on first MITM use. */\nexport function caDir(): string {\n return path.join(dataDir(), \"ca\");\n}\n"],"mappings":"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","names":["fs","path","path","path","path","fs"]}
1
+ {"version":3,"sources":["../src/mcp.ts","../src/instance.ts","../src/paths.ts"],"sourcesContent":["// MCP stdio thin shell for launcher mode (#162): a single \"bili\" MCP server\n// the hosts load via --mcp-config / -c mcp_servers.bili. It fetches the\n// proxy's plugin manifest (single source of truth — zero schema drift),\n// exposes the 4 ACP tools over stdio JSON-RPC, and forwards executes to\n// POST /__bili/plugin/tool. Claude Code passes its session id via the MCP\n// initialize request's _meta.ui.sessionId (documented SessionStart context);\n// we also accept BILI_CONVERSATION_ID env (codex spawn-time registration).\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isPidAlive, isProxyInstanceFile, readProxyInstanceFile } from \"./instance.js\";\n\nconst VERSION = (() => {\n try {\n const here = fileURLToPath(import.meta.url);\n const pkg = path.join(path.dirname(here), \"..\", \"package.json\");\n return (JSON.parse(fs.readFileSync(pkg, \"utf8\")).version as string) ?? \"dev\";\n } catch {\n return \"dev\";\n }\n})();\ntype JsonRpcId = string | number | null;\n\ntype McpToolDef = {\n name: string;\n description?: string;\n inputSchema: unknown;\n};\n// Claude Code passes the session id as an env var to MCP children (verified\n// against claude 2.1.227: CLAUDE_CODE_SESSION_ID) — and puts the SAME id on\n// every model request (x-claude-code-session-id), so binding is by identity.\n// BILI_CONVERSATION_ID (launcher-spawned hosts like codex) has no matching\n// request id — binding is headless (next NEW session).\nconst DEFAULT_PROXY_ORIGIN = \"http://127.0.0.1:8787\";\nlet warnedStaleRecord = false;\n\nexport function resolveProxyOrigin(): string {\n const fromEnv = process.env.BILI_MCP_PROXY?.trim();\n if (fromEnv && fromEnv.length > 0) return fromEnv;\n const discovered = readProxyInstanceFile();\n if (discovered && /^https?:\\/\\/\\S+$/.test(discovered.origin)) {\n // #405: a JSON record naming a dead writer pid points at a port nothing\n // listens on — skip it for the default origin instead of dialing it\n // forever. The `pid > 0` guard is deliberate: pid 0 (foreign writer)\n // and legacy plain-URL pointers make no liveness claim and keep the\n // old trust semantics (unlike plugin-install, which refuses them).\n if (isProxyInstanceFile(discovered) && discovered.pid > 0 && !isPidAlive(discovered.pid)) {\n if (!warnedStaleRecord) {\n warnedStaleRecord = true;\n process.stderr.write(`[bili-mcp] recorded bili proxy (${discovered.origin}, pid ${discovered.pid}) is not running — falling back to ${DEFAULT_PROXY_ORIGIN}; set BILI_MCP_PROXY if your proxy listens elsewhere\\n`);\n }\n return DEFAULT_PROXY_ORIGIN;\n }\n return discovered.origin;\n }\n return DEFAULT_PROXY_ORIGIN;\n}\n\nconst TOOL_TIMEOUT_MS = 60_000;\nconst CONVERSATION_FROM_ENV = process.env.CLAUDE_CODE_SESSION_ID?.trim() || process.env.BILI_CONVERSATION_ID?.trim() || undefined;\nconst IDENTITY_BINDING = Boolean(process.env.CLAUDE_CODE_SESSION_ID?.trim());\nlet manifestTools: McpToolDef[] = [];\nlet conversationId = CONVERSATION_FROM_ENV;\nlet registered = false;\nlet initialized = false;\nfunction send(msg: unknown): void {\n process.stdout.write(JSON.stringify(msg) + \"\\n\");\n}\n\nfunction sendResult(id: JsonRpcId, result: unknown): void {\n if (id === null) return; // notification — no response expected\n send({ jsonrpc: \"2.0\", id, result });\n}\n\nfunction sendError(id: JsonRpcId, code: number, message: string): void {\n if (id === null) return;\n send({ jsonrpc: \"2.0\", id, error: { code, message } });\n}\n\nasync function fetchManifest(): Promise<void> {\n const res = await fetch(`${resolveProxyOrigin()}/__bili/plugin/manifest`, { signal: AbortSignal.timeout(5000) });\n if (!res.ok) throw new Error(`manifest fetch failed: ${res.status}`);\n const data = (await res.json()) as { tools?: Record<string, { name: string; description?: string; input_schema?: unknown }[]> };\n // Anthropic wire shape is the canonical MCP-compatible schema source.\n const anthropic = data.tools?.anthropic ?? [];\n manifestTools = anthropic.map((t) => ({ name: t.name, description: t.description, inputSchema: t.input_schema }));\n if (manifestTools.length === 0) throw new Error(\"manifest served no anthropic tools\");\n}\n\nlet manifestPromise: Promise<void> | null = null;\nfunction ensureManifest(): Promise<void> {\n manifestPromise ??= fetchManifest().catch((err) => {\n manifestPromise = null;\n throw err;\n });\n return manifestPromise;\n}\n\nexport async function forwardTool(tool: string, args: unknown, timeoutMs: number = TOOL_TIMEOUT_MS): Promise<string> {\n let res: Response;\n try {\n res = await fetch(`${resolveProxyOrigin()}/__bili/plugin/tool`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ conversationId, tool, args }),\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (err) {\n if (err instanceof Error && err.name === \"TimeoutError\") throw new Error(`tool forward timed out after ${timeoutMs}ms: ${tool}`);\n throw err;\n }\n const data = (await res.json()) as { ok?: boolean; result?: string; error?: string };\n if (!res.ok || !data.ok) throw new Error(data.error ?? `tool forward failed: ${res.status}`);\n return data.result ?? \"\";\n}\n\nconst ERR_TOOL = -32602;\n\nasync function handleMessage(msg: {\n id?: JsonRpcId;\n method?: string;\n params?: {\n _meta?: { ui?: { sessionId?: string } };\n [k: string]: unknown;\n };\n}): Promise<void> {\n const { id = null, method } = msg;\n const params = typeof msg.params === \"object\" && msg.params !== null ? msg.params : {};\n switch (method) {\n case \"initialize\": {\n // Session id arrives as an env var on the child process (claude)\n // or is injected at spawn time (launcher hosts). The MCP spec\n // guarantees the host waits for this response before calling\n // tools, and claude -p fires its first model request around the\n // same time — registering here is still the earliest we can be.\n // Identity-mode registrations bind on any later request, so the\n // race only matters for headless mode.\n const fromMeta = params._meta?.ui?.sessionId?.trim();\n if (fromMeta) conversationId ??= fromMeta;\n initialized = true;\n if (conversationId && !registered) {\n const registerFetch = fetch(`${resolveProxyOrigin()}/__bili/plugin/register`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ conversationId, agent: \"mcp\", identity: IDENTITY_BINDING }),\n signal: AbortSignal.timeout(5000),\n });\n registered = true; // issue-once: a repeated initialize must not re-register\n if (IDENTITY_BINDING) {\n // Identity-mode binding survives any arrival order —\n // respond immediately so pipelined hosts are not stuck\n // behind the register round-trip.\n void registerFetch.catch(() => {});\n } else {\n // Headless binding is order-sensitive: the register MUST\n // land before the host's first model request, and hosts\n // that pipeline tools/list would otherwise race past it.\n await registerFetch.catch(() => {});\n }\n }\n sendResult(id, {\n protocolVersion: \"2025-06-18\",\n serverInfo: { name: \"bili\", version: VERSION },\n capabilities: { tools: {} },\n });\n return;\n }\n case \"notifications/initialized\":\n return;\n case \"tools/list\": {\n if (!initialized) {\n sendError(id, -32002, \"server not initialized\");\n return;\n }\n try {\n await ensureManifest();\n sendResult(id, { tools: manifestTools });\n } catch (err) {\n sendError(id, -32003, `bili proxy unreachable at ${resolveProxyOrigin()} (${err instanceof Error ? err.message : String(err)}) — start bili or set BILI_MCP_PROXY`);\n }\n return;\n }\n case \"tools/call\": {\n const tool = typeof params.name === \"string\" ? params.name : \"\";\n const args = params.arguments && typeof params.arguments === \"object\" ? params.arguments : {};\n if (!tool) {\n sendError(id, ERR_TOOL, \"params.name is required\");\n return;\n }\n if (!conversationId) {\n sendError(id, ERR_TOOL, \"no conversation id (set BILI_CONVERSATION_ID or connect via Claude Code MCP session meta)\");\n return;\n }\n try {\n const text = await forwardTool(tool, args);\n sendResult(id, { content: [{ type: \"text\", text }], isError: false });\n } catch (err) {\n // Tool-level failures are results (isError), not JSON-RPC\n // errors, so the host surfaces them to the model.\n sendResult(id, { content: [{ type: \"text\", text: `bili tool error: ${err instanceof Error ? err.message : String(err)}` }], isError: true });\n }\n return;\n }\n case \"ping\":\n sendResult(id, {});\n return;\n default:\n if (method?.startsWith(\"notifications/\")) return;\n sendError(id, -32601, `method not found: ${method ?? \"(none)\"}`);\n }\n}\nasync function mcpMain(): Promise<void> {\n let buf = \"\";\n process.stdin.setEncoding(\"utf8\");\n process.stdin.on(\"data\", (chunk: string) => {\n buf += chunk;\n let nl: number;\n while ((nl = buf.indexOf(\"\\n\")) >= 0) {\n const line = buf.slice(0, nl).trim();\n buf = buf.slice(nl + 1);\n if (!line) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (parsed && typeof parsed === \"object\") {\n void handleMessage(parsed as Parameters<typeof handleMessage>[0]);\n }\n }\n });\n process.stdin.on(\"end\", () => process.exit(0));\n}\n\n/** CLI entry (`bili mcp`): the stdio loop keeps the process alive. */\nexport function runMcpStdio(): void {\n void mcpMain();\n}\n\n// Direct entry (dist/mcp.js spawned by the injected MCP config, or the ts\n// source under tsx in tests): run only when invoked as the script itself,\n// never when imported by the CLI.\nif (process.argv[1] && /(?:^|[\\\\/])mcp\\.(?:ts|js)$/.test(process.argv[1])) {\n void mcpMain();\n}\n","import { createHash, randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { stateDir } from \"./paths.js\";\n\n/** Instance registry (#394/#403/#417): the proxy-origin file is upgraded from\n * a bare URL string to a JSON record identifying the live instance, so\n * readers (launcher attach, plugin install, MCP shells) can verify liveness\n * and config compatibility before trusting it. Readers from older builds\n * treat a JSON body as unreadable and fall back to their default origin; the\n * writer and all in-tree readers ship together, so the skew window is one\n * restart. */\nexport interface ProxyInstanceFile {\n origin: string;\n instanceId: string;\n pid: number;\n startedAt: number;\n host: string;\n port: number;\n passthrough: boolean;\n mitmDomains: string[];\n modelWindows: Record<string, number>;\n launchToken?: string;\n}\n\nexport function isProxyInstanceFile(v: ProxyInstanceFile | { origin: string } | undefined): v is ProxyInstanceFile {\n return v !== undefined && \"instanceId\" in v;\n}\n\nexport function readProxyInstanceFile(file?: string): ProxyInstanceFile | { origin: string } | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(file ?? instanceFilePath(), \"utf8\").trim();\n } catch {\n return undefined;\n }\n if (raw.startsWith(\"{\")) {\n try {\n const parsed = JSON.parse(raw) as Partial<ProxyInstanceFile>;\n if (typeof parsed.origin === \"string\" && /^https?:\\/\\/\\S+$/.test(parsed.origin)) {\n const windows: Record<string, number> = {};\n if (parsed.modelWindows && typeof parsed.modelWindows === \"object\") {\n for (const [k, v] of Object.entries(parsed.modelWindows)) {\n const n = Number(v);\n if (Number.isFinite(n) && n > 0) windows[k] = n;\n }\n }\n return {\n origin: parsed.origin,\n instanceId: typeof parsed.instanceId === \"string\" ? parsed.instanceId : \"\",\n pid: typeof parsed.pid === \"number\" ? parsed.pid : 0,\n startedAt: typeof parsed.startedAt === \"number\" ? parsed.startedAt : 0,\n host: typeof parsed.host === \"string\" ? parsed.host : \"\",\n port: typeof parsed.port === \"number\" ? parsed.port : 0,\n passthrough: Boolean(parsed.passthrough),\n mitmDomains: Array.isArray(parsed.mitmDomains) ? parsed.mitmDomains.map(String) : [],\n modelWindows: windows,\n launchToken: typeof parsed.launchToken === \"string\" ? parsed.launchToken : undefined,\n };\n }\n } catch {\n // fall through to legacy plain-string parsing\n }\n }\n if (/^https?:\\/\\/\\S+$/.test(raw)) return { origin: raw };\n return undefined;\n}\n\nexport function instanceFilePath(): string {\n return path.join(stateDir(), \"proxy-origin\");\n}\n\n/** tmp+fsync+rename (same shape as web/api.ts atomicWriteConfig) — a torn\n * write must never leave a half-file behind (#406 family). Shared by the\n * proxy-origin record and every registry marker. */\nfunction atomicWriteJson(obj: unknown, filePath: string): void {\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n let descriptor: number | undefined;\n try {\n descriptor = fs.openSync(tempPath, \"wx\", 0o644);\n fs.writeSync(descriptor, JSON.stringify(obj) + \"\\n\", null, \"utf8\");\n fs.fsyncSync(descriptor);\n fs.closeSync(descriptor);\n descriptor = undefined;\n fs.renameSync(tempPath, filePath);\n } catch (error) {\n if (descriptor !== undefined) {\n try {\n fs.closeSync(descriptor);\n } catch {}\n }\n try {\n fs.unlinkSync(tempPath);\n } catch {}\n throw error;\n }\n}\n\nexport function atomicWriteInstanceFile(info: ProxyInstanceFile, file?: string): void {\n atomicWriteJson(info, file ?? instanceFilePath());\n}\n\n/** Remove our record on shutdown — but only if the file still carries OUR\n * instanceId (a newer instance may have legitimately taken it over). */\nexport function clearProxyInstanceFile(instanceId: string, file?: string): void {\n const filePath = file ?? instanceFilePath();\n const current = readProxyInstanceFile(filePath);\n if (isProxyInstanceFile(current) && current.instanceId === instanceId) {\n try {\n fs.unlinkSync(filePath);\n } catch {}\n }\n}\n\n/** pid liveness (kill-0). EPERM means the process exists but is owned by\n * another user — still alive. */\nexport function isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\ninterface RegistryEntry {\n instanceId: string;\n pid: number;\n port: number;\n origin: string;\n startedAt: number;\n}\n\n/** Cross-instance liveness registry (#394/#527): one marker file per instance\n * under <state>/instances/<id>.json, so the registry IS the directory listing\n * and removal unlinks exactly one file — no shared read-modify-write, hence no\n * cross-process lost update. Dead owners are reaped on the next registration;\n * legacy instances.json is folded in read-only. Best-effort. */\nfunction registryDirPath(): string {\n return path.join(stateDir(), \"instances\");\n}\n\nfunction legacyRegistryFilePath(): string {\n return path.join(stateDir(), \"instances.json\");\n}\n\nconst SAFE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nfunction safeRegistryName(instanceId: string): string {\n if (instanceId !== \".\" && instanceId !== \"..\" && instanceId.length <= 128 && SAFE_NAME_RE.test(instanceId)) {\n return instanceId;\n }\n return createHash(\"sha256\").update(instanceId).digest(\"hex\");\n}\n\nfunction registryEntryFile(instanceId: string): string {\n return path.join(registryDirPath(), `${safeRegistryName(instanceId)}.json`);\n}\n\nfunction safeReadJson(file: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch {\n return undefined;\n }\n}\n\nfunction coerceEntry(value: unknown): RegistryEntry | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const o = value as Record<string, unknown>;\n if (typeof o.instanceId !== \"string\" || o.instanceId === \"\") return undefined;\n return {\n instanceId: o.instanceId,\n pid: typeof o.pid === \"number\" ? o.pid : 0,\n port: typeof o.port === \"number\" ? o.port : 0,\n origin: typeof o.origin === \"string\" ? o.origin : \"\",\n startedAt: typeof o.startedAt === \"number\" ? o.startedAt : 0,\n };\n}\n\nfunction readMarkerNames(): string[] {\n try {\n return fs.readdirSync(registryDirPath());\n } catch {\n return [];\n }\n}\n\nfunction readAllRegistryEntries(): RegistryEntry[] {\n const seen = new Set<string>();\n const out: RegistryEntry[] = [];\n for (const name of readMarkerNames()) {\n if (!name.endsWith(\".json\")) continue;\n const entry = coerceEntry(safeReadJson(path.join(registryDirPath(), name)));\n if (entry && !seen.has(entry.instanceId)) {\n seen.add(entry.instanceId);\n out.push(entry);\n }\n }\n const legacy = safeReadJson(legacyRegistryFilePath()) as { instances?: unknown } | undefined;\n if (legacy && Array.isArray(legacy.instances)) {\n for (const raw of legacy.instances) {\n const entry = coerceEntry(raw);\n if (entry && !seen.has(entry.instanceId)) {\n seen.add(entry.instanceId);\n out.push(entry);\n }\n }\n }\n return out;\n}\n\nfunction reapDeadMarkers(ours: string): void {\n for (const name of readMarkerNames()) {\n if (!name.endsWith(\".json\")) continue;\n const file = path.join(registryDirPath(), name);\n const entry = coerceEntry(safeReadJson(file));\n if (!entry || entry.instanceId === ours || isPidAlive(entry.pid)) continue;\n try {\n fs.unlinkSync(file);\n } catch {}\n }\n}\n\nexport function registerInstanceAndWarn(entry: RegistryEntry, warn: (msg: string) => void): void {\n const others = readAllRegistryEntries().filter((e) => e.instanceId !== entry.instanceId && isPidAlive(e.pid));\n for (const other of others) {\n warn(\n `another bili instance is running (pid ${other.pid}, ${other.origin}) — both processes will write the same sessions directory; stop one to avoid state pollution (#394)`,\n );\n }\n reapDeadMarkers(entry.instanceId);\n try {\n atomicWriteJson(entry, registryEntryFile(entry.instanceId));\n } catch {}\n}\n\n/** Unlinks only our own marker; cannot clobber another instance's entry (#527). */\nexport function unregisterInstance(instanceId: string): void {\n try {\n fs.unlinkSync(registryEntryFile(instanceId));\n } catch {}\n}\n","import { homedir } from \"node:os\";\nimport path from \"node:path\";\n\n/** XDG base-directory paths for billion-context.\n *\n * Follows the XDG Base Directory Specification so the proxy lands in the\n * conventional Linux/macOS locations instead of a bespoke ~/.bili/:\n *\n * config (user-edited, dotfile-managed):\n * $XDG_CONFIG_HOME/billion-context/billion-context.json\n * default: ~/.config/billion-context/billion-context.json\n *\n * data (persisted session state, grows over time):\n * $XDG_DATA_HOME/billion-context/sessions/\n * default: ~/.local/share/billion-context/sessions/\n *\n * Env overrides (highest priority) are kept so test runners and container\n * setups can relocate everything without touching the config file. */\n\nfunction xdg(envVar: string, fallback: string): string {\n const v = process.env[envVar];\n if (v && v.length > 0) return path.resolve(v);\n return path.join(homedir(), fallback);\n}\n\n/** Root config dir: user-editable configuration lives here. */\nexport function configDir(): string {\n return path.join(xdg(\"XDG_CONFIG_HOME\", \".config\"), \"billion-context\");\n}\n\n/** Main config file path. */\nexport function configFile(): string {\n const env = process.env.BILI_CONFIG_FILE;\n if (env && env.length > 0) return path.resolve(env);\n return path.join(configDir(), \"billion-context.json\");\n}\n\n/** Root data dir: persistent session state lives here. */\nexport function dataDir(): string {\n return path.join(xdg(\"XDG_DATA_HOME\", \".local/share\"), \"billion-context\");\n}\n\n/** Sessions dir: one JSON file per session. */\nexport function sessionsDir(): string {\n const env = process.env.BILI_SESSIONS_DIR;\n if (env && env.length > 0) return path.resolve(env);\n return path.join(dataDir(), \"sessions\");\n}\n\n/** Root cache dir: transient/ephemeral data (update-check throttle, etc.). */\nexport function cacheDir(): string {\n return path.join(xdg(\"XDG_CACHE_HOME\", \".cache\"), \"billion-context\");\n}\n\n/** Root state dir: log files and other per-host state. */\nexport function stateDir(): string {\n return path.join(xdg(\"XDG_STATE_HOME\", \".local/state\"), \"billion-context\");\n}\n\n/** Default log file path. */\nexport function defaultLogFile(): string {\n return path.join(stateDir(), \"bili.log\");\n}\n\n/** Origin of the most recently started proxy (best-effort discovery file for\n * host-spawned MCP shells that have no env passthrough: opencode/claude/codex). */\nexport function proxyOriginFile(): string {\n return path.join(stateDir(), \"proxy-origin\");\n}\n\n/** CA dir: root CA + dynamically-signed host certificates for MITM mode.\n * Lives under the data dir (the private key is sensitive but regenerable).\n * Created lazily by ca.ts on first MITM use. */\nexport function caDir(): string {\n return path.join(dataDir(), \"ca\");\n}\n"],"mappings":"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","names":["fs","path","path","path","path","fs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "billion-context",
3
- "version": "0.1.89",
3
+ "version": "0.1.90",
4
4
  "description": "Universal context-compression proxy for AI coding agents. Sits between any agent and its model API, rewriting Anthropic/OpenAI streams with acp-kernel compression. Any agent that can set a base URL (Claude Code, Codex, Cursor, Aider) works out of the box.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -45,7 +45,7 @@
45
45
  "devDependencies": {
46
46
  "@types/node": "^22.0.0",
47
47
  "@types/node-forge": "^1.3.14",
48
- "acp-kernel": "0.0.54",
48
+ "acp-kernel": "0.0.56",
49
49
  "fzstd": "0.1.1",
50
50
  "node-forge": "^1.4.0",
51
51
  "tar": "^7.5.22",