billion-context 0.1.116 → 0.1.117
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/agent/omp.js +17 -5
- package/dist/agent/omp.js.map +1 -1
- package/dist/agent/pi.js +17 -5
- package/dist/agent/pi.js.map +1 -1
- package/dist/index.js +360 -266
- package/dist/index.js.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
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 manifest's 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// #760: a fourth channel for hosts that share ONE shim across several\n// concurrent conversations (no env/meta session at all): every tool accepts an\n// optional conversation_id argument the model copies from the proxy's notes,\n// which overrides the default binding for that one call only. No registration\n// is issued for per-call ids — the proxy resolves them directly and flips the\n// target session to plugin mode on successful execution.\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());\n// #656: hosts that resume a session (claude --resume forks a NEW session id)\n// do so after MCP children were spawned — the env-captured id goes stale and\n// every tool call 404s forever. When that exact failure is seen, adopt the\n// proxy's most-recent active conversation (status?fallback=latest) and retry\n// once. Only armed for identity-bound hosts (claude code); opt out with\n// BILI_MCP_NO_ORPHAN_ADOPT=1 when several host sessions share one proxy and\n// the resumed one must not adopt a sibling's conversation.\nconst ORPHAN_ADOPT = IDENTITY_BINDING && process.env.BILI_MCP_NO_ORPHAN_ADOPT !== \"1\";\nlet manifestTools: McpToolDef[] = [];\nlet conversationId = CONVERSATION_FROM_ENV;\n// #760: every conversation this shim has ever registered — the default\n// binding plus any per-call ids seen so far (issue-once each).\nconst registeredConversations = new Set<string>();\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, conversationIdOverride: string | undefined = undefined): Promise<string> {\n const effectiveConversationId = conversationIdOverride ?? conversationId;\n for (let attempt = 0; ; attempt++) {\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: effectiveConversationId, 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) return data.result ?? \"\";\n // #656: the shim's captured id was never registered — the host likely\n // resumed its session and forked a new id after this shim spawned.\n // Adopt the proxy's latest active conversation and retry once. Armed\n // for DEFAULT-bound calls only: a per-call (#760) id naming an unknown\n // session must fail loudly, never adopt a sibling conversation.\n if (\n res.status === 404 && attempt === 0 && ORPHAN_ADOPT && conversationIdOverride === undefined && conversationId &&\n typeof data.error === \"string\" && data.error.includes(\"no model request has arrived\")\n ) {\n if (await adoptLatestActiveConversation()) continue;\n }\n throw new Error(data.error ?? `tool forward failed: ${res.status}`);\n }\n}\n\n/** One-shot recovery for a stale shim id (#656): resolve the proxy's\n * most-recent ACTIVE conversation via the status endpoint's fallback=latest\n * and adopt it for all subsequent tool calls. Returns true when an adoption\n * happened (caller should retry the tool call). */\nasync function adoptLatestActiveConversation(): Promise<boolean> {\n try {\n const res = await fetch(\n `${resolveProxyOrigin()}/__bili/plugin/status?conversationId=${encodeURIComponent(conversationId ?? \"\")}&fallback=latest`,\n { signal: AbortSignal.timeout(5000) },\n );\n if (!res.ok) return false;\n const data = (await res.json()) as { ok?: boolean; conversationId?: string; fallback?: boolean };\n if (data.ok !== true || data.fallback !== true || !data.conversationId || data.conversationId === conversationId) return false;\n process.stderr.write(\n `[bili-mcp] conversation id no longer known by the proxy (host resumed its session?); adopting latest active conversation ${data.conversationId}\\n`,\n );\n conversationId = data.conversationId;\n return true;\n } catch {\n return false;\n }\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 && !registeredConversations.has(conversationId)) {\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 registeredConversations.add(conversationId); // 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 rawArgs: Record<string, unknown> = params.arguments && typeof params.arguments === \"object\" ? (params.arguments as Record<string, unknown>) : {};\n if (!tool) {\n sendError(id, ERR_TOOL, \"params.name is required\");\n return;\n }\n // #760: per-call conversation_id — the model copies the id the\n // proxy printed in its notes (\"your bili conversation id: …\").\n // Overrides the default binding (env/meta); stripped before\n // forwarding since the proxy routes on the body-level field.\n const perCallRaw = rawArgs.conversation_id;\n const perCall = typeof perCallRaw === \"string\" ? perCallRaw.trim() : \"\";\n const args = { ...rawArgs };\n delete args.conversation_id;\n if (!perCall && !conversationId) {\n sendError(id, ERR_TOOL, \"no conversation id (pass the conversation_id argument — see the 'your bili conversation id' line in the proxy notes — or set BILI_CONVERSATION_ID or connect via Claude Code MCP session meta)\");\n return;\n }\n try {\n const text = await forwardTool(tool, args, TOOL_TIMEOUT_MS, perCall || undefined);\n sendResult(id, { content: [{ type: \"text\", text }], isError: false });\n } catch (err) {\n // Protocol 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/** Cross-process starting marker (#707): while a launcher sits between spawn\n * and healthy, no instance record exists yet, so concurrent launches see\n * nothing to attach to and double-spawn — two writers over one sessions dir.\n * The starter claims this file with O_EXCL before spawning; concurrent\n * callers find it and wait for the bring-up instead of spawning their own. */\nexport interface ProxyStartingMarker {\n token: string;\n pid: number;\n host: string;\n port: number;\n startedAt: number;\n}\n\nexport function startingMarkerPath(): string {\n return path.join(stateDir(), \"proxy-starting\");\n}\n\nexport function readStartingMarker(file?: string): ProxyStartingMarker | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(file ?? startingMarkerPath(), \"utf8\");\n } catch {\n return undefined;\n }\n try {\n const parsed = JSON.parse(raw) as Partial<ProxyStartingMarker>;\n if (typeof parsed.token !== \"string\" || parsed.token === \"\") return undefined;\n if (typeof parsed.pid !== \"number\" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return undefined;\n if (typeof parsed.startedAt !== \"number\") return undefined;\n return {\n token: parsed.token,\n pid: parsed.pid,\n host: typeof parsed.host === \"string\" ? parsed.host : \"\",\n port: typeof parsed.port === \"number\" ? parsed.port : 0,\n startedAt: parsed.startedAt,\n };\n } catch {\n return undefined;\n }\n}\n\n/** Claim the marker atomically across processes (O_EXCL create — a rename-based\n * write would leave the same race at the marker level). Returns false when a\n * claimant already holds it or the state dir is unwritable; coordination then\n * degrades to the pre-#707 behavior instead of failing the launch. */\nexport function claimStartingMarker(marker: ProxyStartingMarker, file?: string): boolean {\n const filePath = file ?? startingMarkerPath();\n let descriptor: number | undefined;\n try {\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n descriptor = fs.openSync(filePath, \"wx\", 0o644);\n fs.writeSync(descriptor, JSON.stringify(marker) + \"\\n\", null, \"utf8\");\n fs.fsyncSync(descriptor);\n descriptor = undefined;\n return true;\n } catch {\n if (descriptor !== undefined) {\n try {\n fs.closeSync(descriptor);\n } catch {}\n }\n return false;\n }\n}\n\n/** Remove the marker only if it still carries OUR token (a newer claimant may\n * have taken it over). Mirrors clearProxyInstanceFile. */\nexport function clearStartingMarker(token: string, file?: string): void {\n const filePath = file ?? startingMarkerPath();\n const current = readStartingMarker(filePath);\n if (current && current.token === token) {\n try {\n fs.unlinkSync(filePath);\n } catch {}\n }\n}\n\n/** Unconditional best-effort removal — used only on markers the caller has\n * already verified stale (dead owner / expired). */\nexport function removeStartingMarker(file?: string): void {\n try {\n fs.unlinkSync(file ?? startingMarkerPath());\n } catch {}\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 manifest's 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// #760: a fourth channel for hosts that share ONE shim across several\n// concurrent conversations (no env/meta session at all): every tool accepts an\n// optional conversation_id argument the model copies from the proxy's notes,\n// which overrides the default binding for that one call only. No registration\n// is issued for per-call ids — the proxy resolves them directly and flips the\n// target session to plugin mode on successful execution.\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());\n// #656: hosts that resume a session (claude --resume forks a NEW session id)\n// do so after MCP children were spawned — the env-captured id goes stale and\n// every tool call 404s forever. When that exact failure is seen, adopt the\n// proxy's most-recent active conversation (status?fallback=latest) and retry\n// once. Only armed for identity-bound hosts (claude code); opt out with\n// BILI_MCP_NO_ORPHAN_ADOPT=1 when several host sessions share one proxy and\n// the resumed one must not adopt a sibling's conversation.\nconst ORPHAN_ADOPT = IDENTITY_BINDING && process.env.BILI_MCP_NO_ORPHAN_ADOPT !== \"1\";\nlet manifestTools: McpToolDef[] = [];\nlet conversationId = CONVERSATION_FROM_ENV;\n// #760: every conversation this shim has ever registered — the default\n// binding plus any per-call ids seen so far (issue-once each).\nconst registeredConversations = new Set<string>();\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, conversationIdOverride: string | undefined = undefined): Promise<string> {\n const effectiveConversationId = conversationIdOverride ?? conversationId;\n for (let attempt = 0; ; attempt++) {\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: effectiveConversationId, 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) return data.result ?? \"\";\n // #656: the shim's captured id was never registered — the host likely\n // resumed its session and forked a new id after this shim spawned.\n // Adopt the proxy's latest active conversation and retry once. Armed\n // for DEFAULT-bound calls only: a per-call (#760) id naming an unknown\n // session must fail loudly, never adopt a sibling conversation.\n if (\n res.status === 404 && attempt === 0 && ORPHAN_ADOPT && conversationIdOverride === undefined && conversationId &&\n typeof data.error === \"string\" && data.error.includes(\"no model request has arrived\")\n ) {\n if (await adoptLatestActiveConversation()) continue;\n }\n throw new Error(data.error ?? `tool forward failed: ${res.status}`);\n }\n}\n\n/** One-shot recovery for a stale shim id (#656): resolve the proxy's\n * most-recent ACTIVE conversation via the status endpoint's fallback=latest\n * and adopt it for all subsequent tool calls. Returns true when an adoption\n * happened (caller should retry the tool call). */\nasync function adoptLatestActiveConversation(): Promise<boolean> {\n try {\n const res = await fetch(\n `${resolveProxyOrigin()}/__bili/plugin/status?conversationId=${encodeURIComponent(conversationId ?? \"\")}&fallback=latest`,\n { signal: AbortSignal.timeout(5000) },\n );\n if (!res.ok) return false;\n const data = (await res.json()) as { ok?: boolean; conversationId?: string; fallback?: boolean };\n if (data.ok !== true || data.fallback !== true || !data.conversationId || data.conversationId === conversationId) return false;\n process.stderr.write(\n `[bili-mcp] conversation id no longer known by the proxy (host resumed its session?); adopting latest active conversation ${data.conversationId}\\n`,\n );\n conversationId = data.conversationId;\n return true;\n } catch {\n return false;\n }\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 && !registeredConversations.has(conversationId)) {\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 registeredConversations.add(conversationId); // 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 rawArgs: Record<string, unknown> = params.arguments && typeof params.arguments === \"object\" ? (params.arguments as Record<string, unknown>) : {};\n if (!tool) {\n sendError(id, ERR_TOOL, \"params.name is required\");\n return;\n }\n // #760: per-call conversation_id — the model copies the id the\n // proxy printed in its notes (\"your bili conversation id: …\").\n // Overrides the default binding (env/meta); stripped before\n // forwarding since the proxy routes on the body-level field.\n const perCallRaw = rawArgs.conversation_id;\n const perCall = typeof perCallRaw === \"string\" ? perCallRaw.trim() : \"\";\n const args = { ...rawArgs };\n delete args.conversation_id;\n if (!perCall && !conversationId) {\n sendError(id, ERR_TOOL, \"no conversation id (pass the conversation_id argument — see the 'your bili conversation id' line in the proxy notes — or set BILI_CONVERSATION_ID or connect via Claude Code MCP session meta)\");\n return;\n }\n try {\n const text = await forwardTool(tool, args, TOOL_TIMEOUT_MS, perCall || undefined);\n sendResult(id, { content: [{ type: \"text\", text }], isError: false });\n } catch (err) {\n // Protocol 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/** Cross-process starting marker (#707): while a launcher sits between spawn\n * and healthy, no instance record exists yet, so concurrent launches see\n * nothing to attach to and double-spawn — two writers over one sessions dir.\n * The starter claims this file with O_EXCL before spawning; concurrent\n * callers find it and wait for the bring-up instead of spawning their own. */\nexport interface ProxyStartingMarker {\n token: string;\n pid: number;\n host: string;\n port: number;\n startedAt: number;\n}\n\nexport function startingMarkerPath(): string {\n return path.join(stateDir(), \"proxy-starting\");\n}\n\nexport function readStartingMarker(file?: string): ProxyStartingMarker | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(file ?? startingMarkerPath(), \"utf8\");\n } catch {\n return undefined;\n }\n try {\n const parsed = JSON.parse(raw) as Partial<ProxyStartingMarker>;\n if (typeof parsed.token !== \"string\" || parsed.token === \"\") return undefined;\n if (typeof parsed.pid !== \"number\" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return undefined;\n if (typeof parsed.startedAt !== \"number\") return undefined;\n return {\n token: parsed.token,\n pid: parsed.pid,\n host: typeof parsed.host === \"string\" ? parsed.host : \"\",\n port: typeof parsed.port === \"number\" ? parsed.port : 0,\n startedAt: parsed.startedAt,\n };\n } catch {\n return undefined;\n }\n}\n\n/** Claim the marker atomically across processes (O_EXCL create — a rename-based\n * write would leave the same race at the marker level). Returns false when a\n * claimant already holds it or the state dir is unwritable; coordination then\n * degrades to the pre-#707 behavior instead of failing the launch. */\nexport function claimStartingMarker(marker: ProxyStartingMarker, file?: string): boolean {\n const filePath = file ?? startingMarkerPath();\n let descriptor: number | undefined;\n try {\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n descriptor = fs.openSync(filePath, \"wx\", 0o644);\n fs.writeSync(descriptor, JSON.stringify(marker) + \"\\n\", null, \"utf8\");\n fs.fsyncSync(descriptor);\n descriptor = undefined;\n return true;\n } catch {\n if (descriptor !== undefined) {\n try {\n fs.closeSync(descriptor);\n } catch {}\n }\n return false;\n }\n}\n\n/** Remove the marker only if it still carries OUR token (a newer claimant may\n * have taken it over). Mirrors clearProxyInstanceFile. */\nexport function clearStartingMarker(token: string, file?: string): void {\n const filePath = file ?? startingMarkerPath();\n const current = readStartingMarker(filePath);\n if (current && current.token === token) {\n try {\n fs.unlinkSync(filePath);\n } catch {}\n }\n}\n\n/** Unconditional best-effort removal — used only on markers the caller has\n * already verified stale (dead owner / expired). */\nexport function removeStartingMarker(file?: string): void {\n try {\n fs.unlinkSync(file ?? startingMarkerPath());\n } catch {}\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/** Body-dump dir (ACP_DUMP_BODY / BILI_DUMP_4XX): ACP_DUMP_DIR override first,\n * else the XDG state dir so dumps co-locate with bili.log on every platform. */\nexport function dumpsDir(): string {\n const env = process.env.ACP_DUMP_DIR;\n if (env && env.length > 0) return env;\n return path.join(stateDir(), \"dumps\");\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.
|
|
3
|
+
"version": "0.1.117",
|
|
4
4
|
"description": "Context-compression proxy that lets AI coding agents run for days — billions of tokens through one context window. Sits between any agent (Claude Code, Codex, Cursor, Aider, …) and its model API, folding consumed conversation into reversible, prefix-cache-friendly summaries. Zero per-agent code: just point your base URL at the proxy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|