billion-context 0.1.2 → 0.1.4

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/index.js CHANGED
@@ -28,14 +28,23 @@ function sessionsDir() {
28
28
  if (env && env.length > 0) return path.resolve(env);
29
29
  return path.join(dataDir(), "sessions");
30
30
  }
31
+ function cacheDir() {
32
+ return path.join(xdg("XDG_CACHE_HOME", ".cache"), "billion-context");
33
+ }
34
+ function stateDir() {
35
+ return path.join(xdg("XDG_STATE_HOME", ".local/state"), "billion-context");
36
+ }
37
+ function defaultLogFile() {
38
+ return path.join(stateDir(), "bili.log");
39
+ }
31
40
 
32
41
  // src/config.ts
33
- function safeReadJson(path4) {
42
+ function safeReadJson(path6) {
34
43
  try {
35
- return JSON.parse(readFileSync(path4, "utf8"));
44
+ return JSON.parse(readFileSync(path6, "utf8"));
36
45
  } catch (e) {
37
46
  if (e.code !== "ENOENT") {
38
- console.error(`[acp-config] failed to parse ${path4}: ${String(e)}`);
47
+ console.error(`[acp-config] failed to parse ${path6}: ${String(e)}`);
39
48
  }
40
49
  return void 0;
41
50
  }
@@ -98,10 +107,6 @@ function loadOptions(env = process.env) {
98
107
  }
99
108
  }
100
109
  const modelContextLimit = parseInt(env.ACP_MODEL_CONTEXT_LIMIT ?? `${fileConfig.modelContextLimit ?? 2e5}`, 10);
101
- const enabled = (env.ACP_CONDENSE_ENABLED ?? (fileConfig.condense?.enabled === false ? "0" : "1")) !== "0";
102
- const keepRecentToolResults = parseInt(env.ACP_KEEP_RECENT_TOOL_RESULTS ?? `${fileConfig.condense?.keepRecentToolResults ?? 6}`, 10);
103
- const minCharsToCondense = parseInt(env.ACP_MIN_CHARS_TO_CONDENSE ?? `${fileConfig.condense?.minCharsToCondense ?? 1500}`, 10);
104
- const maxKeptChars = parseInt(env.ACP_MAX_KEPT_CHARS ?? `${fileConfig.condense?.maxKeptChars ?? 400}`, 10);
105
110
  return {
106
111
  port: Number.isFinite(port) ? port : 8787,
107
112
  host,
@@ -109,7 +114,6 @@ function loadOptions(env = process.env) {
109
114
  routes,
110
115
  modelContextLimit,
111
116
  kernelConfig: defaultConfig(modelContextLimit),
112
- condense: { enabled, keepRecentToolResults, minCharsToCondense, maxKeptChars },
113
117
  compress: {
114
118
  injectTool: (env.ACP_COMPRESS_TOOL ?? (fileConfig.compress?.injectTool === false ? "0" : "1")) !== "0",
115
119
  injectNudge: (env.ACP_COMPRESS_NUDGE ?? (fileConfig.compress?.injectNudge === false ? "0" : "1")) !== "0"
@@ -118,7 +122,9 @@ function loadOptions(env = process.env) {
118
122
  log: env.ACP_LOG !== "0" && fileConfig.log !== false,
119
123
  debug: (env.ACP_DEBUG ?? (fileConfig.debug ? "1" : "0")) === "1",
120
124
  dumpSse: env.ACP_DUMP_SSE || fileConfig.dumpSse || void 0,
121
- passthrough: (env.ACP_PASSTHROUGH ?? (fileConfig.passthrough ? "1" : "0")) === "1"
125
+ passthrough: (env.ACP_PASSTHROUGH ?? (fileConfig.passthrough ? "1" : "0")) === "1",
126
+ autoUpdate: (env.ACP_AUTO_UPDATE ?? (fileConfig.autoUpdate === false ? "0" : "1")) !== "0",
127
+ logFile: env.ACP_LOG_FILE !== void 0 ? env.ACP_LOG_FILE || void 0 : fileConfig.logFile
122
128
  };
123
129
  }
124
130
  function loadConfigFile() {
@@ -149,12 +155,12 @@ var MAX_REQUEST_BYTES = 100 * 1024 * 1024;
149
155
  var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
150
156
  async function fetchWithTimeout(url, init, timeoutMs = UPSTREAM_TIMEOUT_MS) {
151
157
  const controller = new AbortController();
152
- const timer = setTimeout(() => controller.abort(), timeoutMs);
158
+ const timer2 = setTimeout(() => controller.abort(), timeoutMs);
153
159
  try {
154
160
  const response = await fetch(url, { ...init, signal: controller.signal });
155
- return { response, clearTimer: () => clearTimeout(timer) };
161
+ return { response, clearTimer: () => clearTimeout(timer2) };
156
162
  } catch (e) {
157
- clearTimeout(timer);
163
+ clearTimeout(timer2);
158
164
  throw e;
159
165
  }
160
166
  }
@@ -187,7 +193,6 @@ var ClusterCounter = class {
187
193
  };
188
194
 
189
195
  // src/anthropic.ts
190
- var CONDENSED_TAG = "[acp-proxy: condensed";
191
196
  function extractSystem(system) {
192
197
  if (!system) return "";
193
198
  if (typeof system === "string") return system;
@@ -195,13 +200,14 @@ function extractSystem(system) {
195
200
  }
196
201
  function buildSystem(text, original) {
197
202
  if (Array.isArray(original) && original.length > 0) {
198
- const cc = original[0]?.cache_control;
199
- return [{ type: "text", text, ...cc ? { cache_control: cc } : {} }];
203
+ const ccBlock = original.find((b) => b.cache_control);
204
+ return [{ type: "text", text, ...ccBlock ? { cache_control: ccBlock.cache_control } : {} }];
200
205
  }
201
206
  return text;
202
207
  }
203
208
  function anthropicToCore(body) {
204
209
  const msgs = [];
210
+ const cacheControls = /* @__PURE__ */ new Map();
205
211
  const clusters = new ClusterCounter();
206
212
  for (const m of body.messages) {
207
213
  const blocks = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
@@ -209,7 +215,9 @@ function anthropicToCore(body) {
209
215
  switch (b.type) {
210
216
  case "text": {
211
217
  const base = deriveMessageId(m.role, "text", b.text);
212
- msgs.push({ id: clusters.next(base), role: m.role, contentType: "text", text: b.text });
218
+ const id = clusters.next(base);
219
+ msgs.push({ id, role: m.role, contentType: "text", text: b.text });
220
+ if (b.cache_control) cacheControls.set(id, b.cache_control);
213
221
  break;
214
222
  }
215
223
  case "tool_use": {
@@ -217,26 +225,30 @@ function anthropicToCore(body) {
217
225
  toolCallId: b.id,
218
226
  toolName: b.name
219
227
  });
228
+ const id = clusters.next(base);
220
229
  msgs.push({
221
- id: clusters.next(base),
230
+ id,
222
231
  role: "assistant",
223
232
  contentType: "tool-call",
224
233
  toolName: b.name,
225
234
  toolCallId: b.id,
226
235
  text: safeStringify(b.input)
227
236
  });
237
+ if (b.cache_control) cacheControls.set(id, b.cache_control);
228
238
  break;
229
239
  }
230
240
  case "tool_result": {
231
241
  const text = typeof b.content === "string" ? b.content : b.content.map((c) => c.text).join("\n");
232
242
  const base = deriveMessageId("tool", "tool-result", text, { toolCallId: b.tool_use_id });
243
+ const id = clusters.next(base);
233
244
  msgs.push({
234
- id: clusters.next(base),
245
+ id,
235
246
  role: "tool",
236
247
  contentType: "tool-result",
237
248
  toolCallId: b.tool_use_id,
238
249
  text
239
250
  });
251
+ if (b.cache_control) cacheControls.set(id, b.cache_control);
240
252
  break;
241
253
  }
242
254
  case "thinking": {
@@ -252,9 +264,9 @@ function anthropicToCore(body) {
252
264
  }
253
265
  }
254
266
  }
255
- return { msgs };
267
+ return { msgs, cacheControls };
256
268
  }
257
- function coreToAnthropic(messages) {
269
+ function coreToAnthropic(messages, cacheControls) {
258
270
  const out = [];
259
271
  let current = null;
260
272
  const flush = () => {
@@ -263,6 +275,10 @@ function coreToAnthropic(messages) {
263
275
  }
264
276
  current = null;
265
277
  };
278
+ const cc = (id) => {
279
+ const v = cacheControls?.get(id);
280
+ return v ? { cache_control: v } : {};
281
+ };
266
282
  for (const m of messages) {
267
283
  const target = m.role === "assistant" ? "assistant" : "user";
268
284
  if (!current || current.role !== target) {
@@ -271,21 +287,23 @@ function coreToAnthropic(messages) {
271
287
  }
272
288
  switch (m.contentType) {
273
289
  case "text":
274
- current.blocks.push({ type: "text", text: m.text ?? "" });
290
+ current.blocks.push({ type: "text", text: m.text ?? "", ...cc(m.id) });
275
291
  break;
276
292
  case "tool-call":
277
293
  current.blocks.push({
278
294
  type: "tool_use",
279
295
  id: m.toolCallId ?? `call_${m.id}`,
280
296
  name: m.toolName ?? "unknown",
281
- input: safeParse(m.text)
297
+ input: safeParse(m.text),
298
+ ...cc(m.id)
282
299
  });
283
300
  break;
284
301
  case "tool-result":
285
302
  current.blocks.push({
286
303
  type: "tool_result",
287
304
  tool_use_id: m.toolCallId ?? "",
288
- content: m.text ?? ""
305
+ content: m.text ?? "",
306
+ ...cc(m.id)
289
307
  });
290
308
  break;
291
309
  case "reasoning":
@@ -296,33 +314,6 @@ function coreToAnthropic(messages) {
296
314
  flush();
297
315
  return out;
298
316
  }
299
- function condenseOldToolResults(messages, opts) {
300
- if (!opts.enabled) return { messages, condensedCount: 0, charsSaved: 0 };
301
- const toolResultIndices = [];
302
- for (let i = 0; i < messages.length; i++) {
303
- const m = messages[i];
304
- if (m && m.contentType === "tool-result") toolResultIndices.push(i);
305
- }
306
- if (toolResultIndices.length <= opts.keepRecent) {
307
- return { messages, condensedCount: 0, charsSaved: 0 };
308
- }
309
- const toCondense = new Set(toolResultIndices.slice(0, toolResultIndices.length - opts.keepRecent));
310
- let condensedCount = 0;
311
- let charsSaved = 0;
312
- const out = messages.map((m, i) => {
313
- if (!toCondense.has(i)) return m;
314
- const text = m.text ?? "";
315
- if (text.length < opts.minChars) return m;
316
- const head = text.slice(0, opts.maxKeptChars);
317
- const stub = `${CONDENSED_TAG} ${text.length.toLocaleString()} chars]
318
- ${head}
319
- [/acp-proxy]`;
320
- charsSaved += text.length - stub.length;
321
- condensedCount++;
322
- return { ...m, text: stub };
323
- });
324
- return { messages: out, condensedCount, charsSaved };
325
- }
326
317
  function conversationSignalAnthropic(body, headerValue2) {
327
318
  if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
328
319
  const firstUser = body.messages.find((m) => m.role === "user");
@@ -824,14 +815,14 @@ var SessionStore = class {
824
815
  if (!this.enabled) return;
825
816
  const existing = this.timers.get(session.id);
826
817
  if (existing) clearTimeout(existing);
827
- const timer = setTimeout(() => {
818
+ const timer2 = setTimeout(() => {
828
819
  this.timers.delete(session.id);
829
820
  void this.writeNow(session).catch((e) => {
830
821
  this.log("error", `[persist] debounced write failed for ${session.id}: ${msg(e)}`);
831
822
  });
832
823
  }, this.debounceMs);
833
- timer.unref?.();
834
- this.timers.set(session.id, timer);
824
+ timer2.unref?.();
825
+ this.timers.set(session.id, timer2);
835
826
  }
836
827
  /** Asynchronously persist a session right now (skips the debounce). Throws
837
828
  * on write failure so callers can react (e.g. avoid evicting). */
@@ -888,7 +879,7 @@ var SessionStore = class {
888
879
  async flushAll(sessions2) {
889
880
  if (!this.enabled) return;
890
881
  const dirty = new Set(this.timers.keys());
891
- for (const timer of this.timers.values()) clearTimeout(timer);
882
+ for (const timer2 of this.timers.values()) clearTimeout(timer2);
892
883
  this.timers.clear();
893
884
  const pending = [];
894
885
  for (const s of sessions2) {
@@ -907,7 +898,7 @@ var SessionStore = class {
907
898
  }
908
899
  /** Cancel all pending writes without flushing (e.g. for tests). */
909
900
  cancelAll() {
910
- for (const timer of this.timers.values()) clearTimeout(timer);
901
+ for (const timer2 of this.timers.values()) clearTimeout(timer2);
911
902
  this.timers.clear();
912
903
  }
913
904
  };
@@ -920,7 +911,6 @@ function buildRecord(session) {
920
911
  upstreamOrigin: session.upstreamOrigin,
921
912
  createdAt: session.createdAt,
922
913
  requests: session.requests,
923
- condensedToolResults: session.condensedToolResults,
924
914
  tokensSaved: session.tokensSaved,
925
915
  state: session.state,
926
916
  blockContents: Object.fromEntries(session.blockContents)
@@ -939,7 +929,6 @@ function buildSession(parsed) {
939
929
  createdAt: parsed.createdAt ?? Date.now(),
940
930
  lastSeen: Date.now(),
941
931
  requests: parsed.requests ?? 0,
942
- condensedToolResults: parsed.condensedToolResults ?? 0,
943
932
  tokensSaved: parsed.tokensSaved ?? 0,
944
933
  blockContents,
945
934
  inFlight: 0,
@@ -1026,7 +1015,6 @@ function getSession(id, meta) {
1026
1015
  createdAt: Date.now(),
1027
1016
  lastSeen: Date.now(),
1028
1017
  requests: 0,
1029
- condensedToolResults: 0,
1030
1018
  tokensSaved: 0,
1031
1019
  blockContents: /* @__PURE__ */ new Map(),
1032
1020
  inFlight: 0,
@@ -1597,6 +1585,46 @@ ${body.slice(0, 4e3)}...`;
1597
1585
  ${body}`;
1598
1586
  }
1599
1587
 
1588
+ // src/logger.ts
1589
+ import { createWriteStream, mkdirSync as mkdirSync3, statSync, renameSync as renameSync2 } from "fs";
1590
+ import path3 from "path";
1591
+ var MAX_BYTES = 10 * 1024 * 1024;
1592
+ var stream;
1593
+ var logPath;
1594
+ function open(pathStr) {
1595
+ mkdirSync3(path3.dirname(pathStr), { recursive: true });
1596
+ try {
1597
+ if (statSync(pathStr).size >= MAX_BYTES) {
1598
+ renameSync2(pathStr, pathStr + ".old");
1599
+ }
1600
+ } catch {
1601
+ }
1602
+ return createWriteStream(pathStr, { flags: "a" });
1603
+ }
1604
+ function configureLogger(file) {
1605
+ if (!file || file === "off") {
1606
+ logPath = void 0;
1607
+ stream = void 0;
1608
+ return void 0;
1609
+ }
1610
+ logPath = file;
1611
+ stream = open(file);
1612
+ return file;
1613
+ }
1614
+ var log = (level, msg2) => {
1615
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1616
+ const line = `${ts} [${level}] ${msg2}
1617
+ `;
1618
+ process.stderr.write(line);
1619
+ if (stream) {
1620
+ stream.write(line);
1621
+ }
1622
+ };
1623
+ function closeLogger() {
1624
+ stream?.end();
1625
+ stream = void 0;
1626
+ }
1627
+
1600
1628
  // src/compress-loop.ts
1601
1629
  function executeProxyTool(toolName, args, ctx) {
1602
1630
  if (toolName === "compress") {
@@ -1858,6 +1886,16 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
1858
1886
  const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
1859
1887
  const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
1860
1888
  const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
1889
+ if (usage) {
1890
+ const prompt = usage.prompt_tokens ?? usage.input_tokens;
1891
+ const det = usage.prompt_tokens_details ?? usage.prompt_cache_hit_tokens;
1892
+ const cached = det?.cached_tokens ?? usage.prompt_cache_hit_tokens;
1893
+ const out = usage.completion_tokens ?? usage.output_tokens;
1894
+ if (typeof prompt === "number") {
1895
+ const ch = typeof cached === "number" ? cached : 0;
1896
+ log("info", `[acp-usage] round ${loopCount} input=${prompt} cached=${typeof cached === "number" ? cached : "?"} output=${out ?? "?"}${ch > 0 ? ` (cache hit ${Math.round(ch / prompt * 100)}%)` : ""}`);
1897
+ }
1898
+ }
1861
1899
  if (!hasOnlyProxy) {
1862
1900
  for (const tc of realCalls) {
1863
1901
  yield Buffer.from(buildToolCallSse(makeBase(), tc), "utf8");
@@ -1951,16 +1989,16 @@ function extractTextTriggers(text) {
1951
1989
  let i = 0;
1952
1990
  let n = 0;
1953
1991
  while (i < text.length) {
1954
- const open = text.indexOf(ACP_TEXT_OPEN, i);
1955
- if (open === -1) {
1992
+ const open2 = text.indexOf(ACP_TEXT_OPEN, i);
1993
+ if (open2 === -1) {
1956
1994
  clean += text.slice(i);
1957
1995
  break;
1958
1996
  }
1959
- clean += text.slice(i, open);
1960
- const after = open + ACP_TEXT_OPEN.length;
1997
+ clean += text.slice(i, open2);
1998
+ const after = open2 + ACP_TEXT_OPEN.length;
1961
1999
  const close = text.indexOf(ACP_TEXT_CLOSE, after);
1962
2000
  if (close === -1) {
1963
- clean += text.slice(open);
2001
+ clean += text.slice(open2);
1964
2002
  break;
1965
2003
  }
1966
2004
  const payload = text.slice(after, close).trim();
@@ -2255,7 +2293,7 @@ async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, r
2255
2293
  const prDet = usage.prompt_tokens_details;
2256
2294
  const cached = inDet?.cached_tokens ?? prDet?.cached_tokens ?? "?";
2257
2295
  const out = usage.output_tokens ?? "?";
2258
- console.error(`[acp-usage] round ${loopCount} input=${prompt} cached=${cached} output=${out}${cached !== "?" && cached !== 0 && prompt !== "?" ? ` (cache hit ${Math.round(Number(cached) / Number(prompt) * 100)}%)` : ""}`);
2296
+ log("info", `[acp-usage] round ${loopCount} input=${prompt} cached=${cached} output=${out}${cached !== "?" && cached !== 0 && prompt !== "?" ? ` (cache hit ${Math.round(Number(cached) / Number(prompt) * 100)}%)` : ""}`);
2259
2297
  }
2260
2298
  }
2261
2299
  }
@@ -2453,10 +2491,10 @@ function safeWrite(res, chunk) {
2453
2491
  } catch {
2454
2492
  }
2455
2493
  }
2456
- function emitStreamError(res, protocol, message, log) {
2494
+ function emitStreamError(res, protocol, message, log2) {
2457
2495
  const visible = `
2458
2496
  \u274C [ACP] stream error: ${message}`;
2459
- log?.(`[acp-proxy: stream aborted mid-response: ${message}]`);
2497
+ log2?.(`[acp-proxy: stream aborted mid-response: ${message}]`);
2460
2498
  try {
2461
2499
  if (protocol === "openai") {
2462
2500
  safeWrite(res, `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: visible }, finish_reason: null }] })}
@@ -2556,17 +2594,21 @@ function resolveUpstream(opts, reqUrl) {
2556
2594
  return void 0;
2557
2595
  }
2558
2596
  async function startServer(opts) {
2597
+ const filePath = configureLogger(opts.logFile ?? defaultLogFile());
2559
2598
  const core = createCore();
2560
2599
  const config = opts.kernelConfig;
2561
- const log = (level, msg2) => logMsg(opts, level, msg2);
2600
+ const log2 = (level, msg2) => logMsg(opts, level, msg2);
2562
2601
  await initSessions();
2563
- log("info", `[persist] ${getStore().enabled ? "enabled" : "disabled"}`);
2602
+ log2("info", `[persist] ${getStore().enabled ? "enabled" : "disabled"}`);
2603
+ if (filePath) {
2604
+ log2("info", `[log] writing to ${filePath}`);
2605
+ }
2564
2606
  const server = http.createServer(async (req, res) => {
2565
2607
  try {
2566
- await handle(req, res, opts, core, config, log);
2608
+ await handle(req, res, opts, core, config, log2);
2567
2609
  } catch (err) {
2568
2610
  const msg2 = String(err);
2569
- log("error", msg2);
2611
+ log2("error", msg2);
2570
2612
  if (!res.headersSent) {
2571
2613
  const status = msg2.includes("exceeds") ? 413 : 502;
2572
2614
  res.writeHead(status, { "content-type": "application/json" });
@@ -2577,7 +2619,7 @@ async function startServer(opts) {
2577
2619
  }
2578
2620
  });
2579
2621
  server.listen(opts.port, opts.host, () => {
2580
- log(
2622
+ log2(
2581
2623
  "info",
2582
2624
  `acp-proxy listening on http://${opts.host}:${opts.port}` + (Object.keys(opts.routes).length ? ` \u2014 routes: ${Object.entries(opts.routes).map(([n, u]) => `${n}=${typeof u === "string" ? u : u.url}`).join(", ")}` : ` \u2192 ${opts.upstream}`)
2583
2625
  );
@@ -2586,9 +2628,10 @@ async function startServer(opts) {
2586
2628
  const shutdown = (sig) => {
2587
2629
  if (shuttingDown) return;
2588
2630
  shuttingDown = true;
2589
- log("info", `${sig} received \u2014 flushing sessions\u2026`);
2631
+ log2("info", `${sig} received \u2014 flushing sessions\u2026`);
2590
2632
  server.close();
2591
2633
  void flushAllSessions().finally(() => {
2634
+ closeLogger();
2592
2635
  process.exit(0);
2593
2636
  });
2594
2637
  };
@@ -2596,21 +2639,7 @@ async function startServer(opts) {
2596
2639
  process.on("SIGINT", () => shutdown("SIGINT"));
2597
2640
  return server;
2598
2641
  }
2599
- function applyCondense(messages, opts, session) {
2600
- const condenseOpts = {
2601
- enabled: opts.condense.enabled,
2602
- keepRecent: opts.condense.keepRecentToolResults,
2603
- minChars: opts.condense.minCharsToCondense,
2604
- maxKeptChars: opts.condense.maxKeptChars
2605
- };
2606
- const { messages: out, condensedCount, charsSaved } = condenseOldToolResults(messages, condenseOpts);
2607
- if (condensedCount > 0) {
2608
- session.condensedToolResults += condensedCount;
2609
- session.tokensSaved += Math.ceil(charsSaved / 4);
2610
- }
2611
- return out;
2612
- }
2613
- async function handle(req, res, opts, core, config, log) {
2642
+ async function handle(req, res, opts, core, config, log2) {
2614
2643
  if (req.method === "GET" && req.url === "/__acp/stats") return sendStats(res);
2615
2644
  if (req.method === "GET" && (req.url === "/" || req.url === "/__acp/health")) {
2616
2645
  res.writeHead(200, { "content-type": "application/json" });
@@ -2622,12 +2651,12 @@ async function handle(req, res, opts, core, config, log) {
2622
2651
  bodyBuffer = await readBody(req);
2623
2652
  } catch (err) {
2624
2653
  if (err instanceof BodyTooLargeError) {
2625
- log("warn", `413: request body exceeds ${err.limit} bytes`);
2654
+ log2("warn", `413: request body exceeds ${err.limit} bytes`);
2626
2655
  res.writeHead(413, { "content-type": "application/json" });
2627
2656
  res.end(JSON.stringify({ error: { type: "request_too_large", message: err.message } }));
2628
2657
  return;
2629
2658
  }
2630
- log("warn", `read body failed: ${String(err)}`);
2659
+ log2("warn", `read body failed: ${String(err)}`);
2631
2660
  res.writeHead(400, { "content-type": "application/json" });
2632
2661
  res.end(JSON.stringify({ error: { type: "invalid_request", message: String(err) } }));
2633
2662
  return;
@@ -2663,10 +2692,10 @@ async function handle(req, res, opts, core, config, log) {
2663
2692
  const session = getSession(sessionId, { protocol, upstreamOrigin });
2664
2693
  const affinity = affinityToken(req.headers, conversation);
2665
2694
  await withSessionLock(session, async () => {
2666
- prepared = protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig, log, session) : protocol === "openai" ? prepareOpenai(parsed, req, opts, core, reqConfig, log, session) : prepareResponses(parsed, req, opts, core, reqConfig, log, session);
2695
+ prepared = protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig, log2, session) : protocol === "openai" ? prepareOpenai(parsed, req, opts, core, reqConfig, log2, session) : prepareResponses(parsed, req, opts, core, reqConfig, log2, session);
2667
2696
  acquireInFlight(session);
2668
2697
  try {
2669
- await forward(req, res, opts, prepared.body, prepared, core, reqConfig, log, route, affinity);
2698
+ await forward(req, res, opts, prepared.body, prepared, core, reqConfig, log2, route, affinity);
2670
2699
  } finally {
2671
2700
  releaseInFlight(session);
2672
2701
  }
@@ -2674,9 +2703,9 @@ async function handle(req, res, opts, core, config, log) {
2674
2703
  }
2675
2704
  if (!prepared) {
2676
2705
  if (protocol === null && !opts.passthrough) {
2677
- log("warn", `unrecognized path ${url} \u2014 not a known protocol (/chat/completions, /v1/messages, /responses); forwarding unchanged`);
2706
+ log2("warn", `unrecognized path ${url} \u2014 not a known protocol (/chat/completions, /v1/messages, /responses); forwarding unchanged`);
2678
2707
  }
2679
- await forward(req, res, opts, bodyBuffer, null, core, reqConfig, log, route, void 0);
2708
+ await forward(req, res, opts, bodyBuffer, null, core, reqConfig, log2, route, void 0);
2680
2709
  }
2681
2710
  }
2682
2711
  var ACP_TAG_MARK = "<acp ";
@@ -2704,24 +2733,24 @@ function diagNudge(turn, sessionId, tokenCount, limit) {
2704
2733
  const inject = n.shouldInject ? `INJECT T${n.tier ?? "?"}` : "idle";
2705
2734
  return `[${sessionId}] nudge ${inject}: usage=${pct} (${tokenCount}/${limit}), growth=${growth}/${floor} (ref=${ref}, interval=${interval}), pendingT1=${pendingT1}/${interval}, reason="${n.reason.slice(0, 120)}"`;
2706
2735
  }
2707
- function prepareAnthropic(parsed, req, opts, core, config, log, session) {
2736
+ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
2708
2737
  const sessionId = session.id;
2709
- const stream = parsed.stream === true;
2738
+ const stream2 = parsed.stream === true;
2710
2739
  session.requests++;
2711
2740
  let processedMessages = [];
2712
2741
  let rebuiltMessages = parsed.messages;
2713
2742
  let systemOut = parsed.system;
2714
2743
  let toolsOut = parsed.tools;
2715
2744
  try {
2716
- const { msgs } = anthropicToCore(parsed);
2745
+ const { msgs, cacheControls } = anthropicToCore(parsed);
2717
2746
  const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
2718
2747
  const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
2719
2748
  session.state = turn.state;
2720
- log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2721
- log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2722
- processedMessages = applyCondense(turn.messages, opts, session);
2749
+ log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2750
+ log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2751
+ processedMessages = turn.messages;
2723
2752
  reapOrphanBlocks(session, msgs, deactivateBlock4);
2724
- rebuiltMessages = coreToAnthropic(processedMessages);
2753
+ rebuiltMessages = coreToAnthropic(processedMessages, cacheControls);
2725
2754
  systemOut = injectSystem(parsed, opts);
2726
2755
  if (opts.compress.injectTool) {
2727
2756
  toolsOut = injectTool(parsed.tools);
@@ -2736,31 +2765,31 @@ function prepareAnthropic(parsed, req, opts, core, config, log, session) {
2736
2765
  }
2737
2766
  }
2738
2767
  } catch (err) {
2739
- log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
2768
+ log2("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
2740
2769
  processedMessages = [];
2741
2770
  }
2742
2771
  const rebuilt = { ...parsed, messages: rebuiltMessages, system: systemOut, tools: toolsOut };
2743
2772
  markDirty(session);
2744
- return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "anthropic", stream, compressInjected: opts.compress.injectTool };
2773
+ return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "anthropic", stream: stream2, compressInjected: opts.compress.injectTool };
2745
2774
  }
2746
- function prepareOpenai(parsed, req, opts, core, config, log, session) {
2775
+ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
2747
2776
  const sessionId = session.id;
2748
- const stream = parsed.stream === true;
2777
+ const stream2 = parsed.stream === true;
2749
2778
  session.requests++;
2750
2779
  let processedMessages = [];
2751
2780
  let rebuiltMessages = parsed.messages;
2752
2781
  let toolsOut = parsed.tools;
2753
2782
  const maxTokens = typeof parsed.max_tokens === "number" ? parsed.max_tokens : 8192;
2754
- const isTitleGen = maxTokens <= 200 || parsed.messages.length <= 2;
2783
+ const isTitleGen = maxTokens <= 200;
2755
2784
  const shouldInject = opts.compress.injectTool && !isTitleGen;
2756
2785
  try {
2757
2786
  const { msgs } = openaiToCore(parsed);
2758
2787
  const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
2759
2788
  const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
2760
2789
  session.state = turn.state;
2761
- log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2762
- log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2763
- processedMessages = applyCondense(turn.messages, opts, session);
2790
+ log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2791
+ log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2792
+ processedMessages = turn.messages;
2764
2793
  reapOrphanBlocks(session, msgs, deactivateBlock4);
2765
2794
  rebuiltMessages = coreToOpenai(processedMessages);
2766
2795
  const sysParts = [];
@@ -2779,16 +2808,16 @@ function prepareOpenai(parsed, req, opts, core, config, log, session) {
2779
2808
  }
2780
2809
  }
2781
2810
  } catch (err) {
2782
- log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
2811
+ log2("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
2783
2812
  processedMessages = [];
2784
2813
  }
2785
2814
  const rebuilt = { ...parsed, messages: rebuiltMessages, tools: toolsOut };
2786
2815
  markDirty(session);
2787
- return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "openai", stream, compressInjected: shouldInject };
2816
+ return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "openai", stream: stream2, compressInjected: shouldInject };
2788
2817
  }
2789
- function prepareResponses(parsed, req, opts, core, config, log, session) {
2818
+ function prepareResponses(parsed, req, opts, core, config, log2, session) {
2790
2819
  const sessionId = session.id;
2791
- const stream = parsed.stream === true;
2820
+ const stream2 = parsed.stream === true;
2792
2821
  session.requests++;
2793
2822
  let processedMessages = [];
2794
2823
  let rebuiltInput = parsed.input;
@@ -2797,18 +2826,18 @@ function prepareResponses(parsed, req, opts, core, config, log, session) {
2797
2826
  try {
2798
2827
  const { msgs, systemParts, preamble, customToolCallIds } = responsesToCore(parsed);
2799
2828
  if (process.env.ACP_DEBUG) {
2800
- log("info", `[${sessionId}] input items: ${Array.isArray(parsed.input) ? parsed.input.map((i) => i.type).join(",") : "(string)"}`);
2829
+ log2("info", `[${sessionId}] input items: ${Array.isArray(parsed.input) ? parsed.input.map((i) => i.type).join(",") : "(string)"}`);
2801
2830
  }
2802
2831
  const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
2803
2832
  const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: process.env.ACP_RENDER_NONE ? "none" : "text-only" });
2804
2833
  session.state = turn.state;
2805
- log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2806
- log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2807
- processedMessages = applyCondense(turn.messages, opts, session);
2834
+ log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2835
+ log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2836
+ processedMessages = turn.messages;
2808
2837
  reapOrphanBlocks(session, msgs, deactivateBlock4);
2809
2838
  const conversationItems = coreToResponses(processedMessages, customToolCallIds);
2810
2839
  if (preamble.length > 0) {
2811
- log("info", `[${sessionId}] preserved ${preamble.length} opaque preamble item(s): ${preamble.map((p) => p.type).join(",")}`);
2840
+ log2("info", `[${sessionId}] preserved ${preamble.length} opaque preamble item(s): ${preamble.map((p) => p.type).join(",")}`);
2812
2841
  }
2813
2842
  const inputItems = [...preamble];
2814
2843
  if (shouldInject && !process.env.ACP_NO_COMPRESS_PROMPT) {
@@ -2824,7 +2853,7 @@ function prepareResponses(parsed, req, opts, core, config, log, session) {
2824
2853
  if (process.env.ACP_DEBUG) {
2825
2854
  const ctcs = conversationItems.filter((i) => i.type === "custom_tool_call").length;
2826
2855
  const ctcos = conversationItems.filter((i) => i.type === "custom_tool_call_output").length;
2827
- log("info", `[${sessionId}] rebuilt: msgs=${msgs.length} -> conv=${conversationItems.length} (custom_tool_call=${ctcs} custom_tool_call_output=${ctcos})`);
2856
+ log2("info", `[${sessionId}] rebuilt: msgs=${msgs.length} -> conv=${conversationItems.length} (custom_tool_call=${ctcs} custom_tool_call_output=${ctcos})`);
2828
2857
  }
2829
2858
  if (turn.nudge?.shouldInject && shouldInject) {
2830
2859
  try {
@@ -2837,7 +2866,7 @@ function prepareResponses(parsed, req, opts, core, config, log, session) {
2837
2866
  }
2838
2867
  rebuiltInput = inputItems;
2839
2868
  } catch (err) {
2840
- log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
2869
+ log2("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
2841
2870
  processedMessages = [];
2842
2871
  }
2843
2872
  const rebuilt = { ...parsed, input: rebuiltInput, tools: toolsOut };
@@ -2847,10 +2876,10 @@ function prepareResponses(parsed, req, opts, core, config, log, session) {
2847
2876
  const sub = Array.isArray(r.tools) ? `(${r.tools.length} sub)` : "";
2848
2877
  return `${r.type}:${r.name ?? "?"}${sub}`;
2849
2878
  });
2850
- log("info", `[${sessionId}] responses forward tools=[${fwdTools.join(",")}] injectTool=${shouldInject} NO_INJECT_TOOL=${!!process.env.ACP_NO_INJECT_TOOL} NO_COMPRESS_PROMPT=${!!process.env.ACP_NO_COMPRESS_PROMPT}`);
2879
+ log2("info", `[${sessionId}] responses forward tools=[${fwdTools.join(",")}] injectTool=${shouldInject} NO_INJECT_TOOL=${!!process.env.ACP_NO_INJECT_TOOL} NO_COMPRESS_PROMPT=${!!process.env.ACP_NO_COMPRESS_PROMPT}`);
2851
2880
  }
2852
2881
  markDirty(session);
2853
- return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "responses", stream, compressInjected: shouldInject };
2882
+ return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "responses", stream: stream2, compressInjected: shouldInject };
2854
2883
  }
2855
2884
  function injectSystem(parsed, opts) {
2856
2885
  const baseText = extractSystem(parsed.system);
@@ -2886,19 +2915,19 @@ function injectResponsesTool(tools) {
2886
2915
  const additions = ACP_TOOLS_RESPONSES.filter((t) => !present.has(t.name));
2887
2916
  return [...tools, ...additions];
2888
2917
  }
2889
- async function forward(req, res, opts, body, prepared, core, config, log, route, affinity) {
2918
+ async function forward(req, res, opts, body, prepared, core, config, log2, route, affinity) {
2890
2919
  const upstreamUrl = route ? route.rewrittenUrl : opts.upstream + (req.url ?? "");
2891
- log("info", `forward ${req.method} ${req.url ?? ""} \u2192 ${upstreamUrl}${route ? ` (${route.provider})` : ""}`);
2920
+ log2("info", `forward ${req.method} ${req.url ?? ""} \u2192 ${upstreamUrl}${route ? ` (${route.provider})` : ""}`);
2892
2921
  if (process.env.ACP_DEBUG && prepared) {
2893
2922
  const sid = prepared.session.id;
2894
2923
  const hdrKeys = Object.keys(req.headers);
2895
- log("info", `[${sid}] client headers: ${hdrKeys.join(",")}`);
2924
+ log2("info", `[${sid}] client headers: ${hdrKeys.join(",")}`);
2896
2925
  for (const k of ["authorization", "x-api-key", "x-session-id", "x-session-affinity", "x-acp-session", "x-opencode-session", "prompt-cache-key", "anthropic-beta"]) {
2897
2926
  const v = req.headers[k] ?? req.headers[k.toLowerCase()];
2898
2927
  if (v) {
2899
2928
  const s = Array.isArray(v) ? v.join(",") : String(v);
2900
2929
  const masked = /key|auth|token/i.test(k) ? s.slice(0, 8) + "..." + s.slice(-4) + ` (${s.length} chars)` : s.slice(0, 60);
2901
- log("info", `[${sid}] client hdr ${k}=${masked}`);
2930
+ log2("info", `[${sid}] client hdr ${k}=${masked}`);
2902
2931
  }
2903
2932
  }
2904
2933
  }
@@ -2909,11 +2938,11 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
2909
2938
  const fn = t.function;
2910
2939
  return fn?.name ?? t.name ?? "?";
2911
2940
  });
2912
- log("info", `[debug] tools=[${toolNames.join(",")}] msgs=${parsed.messages?.length ?? 0} stream=${parsed.stream ?? false} system_len=${JSON.stringify(parsed.messages?.find((m) => m.role === "system")?.content ?? "").length}`);
2941
+ log2("info", `[debug] tools=[${toolNames.join(",")}] msgs=${parsed.messages?.length ?? 0} stream=${parsed.stream ?? false} system_len=${JSON.stringify(parsed.messages?.find((m) => m.role === "system")?.content ?? "").length}`);
2913
2942
  if (process.env.ACP_DUMP_REQ === "1") {
2914
2943
  const out = `/tmp/acp-proxy-debug-req-${Date.now()}.json`;
2915
2944
  fs2.writeFileSync(out, body.slice(0, 5e4));
2916
- log("info", `[debug] forwarded body written to ${out}`);
2945
+ log2("info", `[debug] forwarded body written to ${out}`);
2917
2946
  }
2918
2947
  } catch {
2919
2948
  }
@@ -2961,7 +2990,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
2961
2990
  config,
2962
2991
  messages: prepared.processedMessages,
2963
2992
  session: prepared.session,
2964
- log: (msg2) => log("info", `[${prepared.session.id}] ${msg2}`),
2993
+ log: (msg2) => log2("info", `[${prepared.session.id}] ${msg2}`),
2965
2994
  debug: opts.debug
2966
2995
  };
2967
2996
  if (prepared.stream) {
@@ -2991,7 +3020,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
2991
3020
  {
2992
3021
  const s = chunk.toString("utf8");
2993
3022
  if (s.includes("<acp ") || s.includes("</acp")) {
2994
- log("warn", `[${prepared.session.id}] tag echo: openai response stream contains <acp tag`);
3023
+ log2("warn", `[${prepared.session.id}] tag echo: openai response stream contains <acp tag`);
2995
3024
  }
2996
3025
  }
2997
3026
  if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
@@ -3014,7 +3043,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
3014
3043
  {
3015
3044
  const s = chunk.toString("utf8");
3016
3045
  if (s.includes("<acp ") || s.includes("</acp")) {
3017
- log("warn", `[${prepared.session.id}] tag echo: responses response stream contains <acp tag`);
3046
+ log2("warn", `[${prepared.session.id}] tag echo: responses response stream contains <acp tag`);
3018
3047
  }
3019
3048
  }
3020
3049
  if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
@@ -3025,7 +3054,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
3025
3054
  {
3026
3055
  const s = chunk.toString("utf8");
3027
3056
  if (s.includes("<acp ") || s.includes("</acp")) {
3028
- log("warn", `[${prepared.session.id}] tag echo: anthropic response stream contains <acp tag`);
3057
+ log2("warn", `[${prepared.session.id}] tag echo: anthropic response stream contains <acp tag`);
3029
3058
  }
3030
3059
  }
3031
3060
  if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
@@ -3033,7 +3062,7 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
3033
3062
  }
3034
3063
  res.end();
3035
3064
  } catch (e) {
3036
- emitStreamError(res, prepared.protocol, e?.message ?? String(e), (m) => log("error", `[${prepared.session.id}] ${m}`));
3065
+ emitStreamError(res, prepared.protocol, e?.message ?? String(e), (m) => log2("error", `[${prepared.session.id}] ${m}`));
3037
3066
  } finally {
3038
3067
  clearUpstreamTimer();
3039
3068
  if (dumpRaw) await dumpRaw;
@@ -3058,8 +3087,8 @@ async function forward(req, res, opts, body, prepared, core, config, log, route,
3058
3087
  }
3059
3088
  markDirty(prepared.session);
3060
3089
  }
3061
- async function pipeThrough(stream, res) {
3062
- const reader = stream.getReader();
3090
+ async function pipeThrough(stream2, res) {
3091
+ const reader = stream2.getReader();
3063
3092
  try {
3064
3093
  for (; ; ) {
3065
3094
  const { done, value } = await reader.read();
@@ -3073,13 +3102,13 @@ async function pipeThrough(stream, res) {
3073
3102
  res.end();
3074
3103
  }
3075
3104
  }
3076
- async function dumpStreamToFile(stream, dir, name) {
3077
- const { mkdirSync: mkdirSync3, createWriteStream } = await import("fs");
3105
+ async function dumpStreamToFile(stream2, dir, name) {
3106
+ const { mkdirSync: mkdirSync4, createWriteStream: createWriteStream2 } = await import("fs");
3078
3107
  const { join: join3 } = await import("path");
3079
3108
  try {
3080
- mkdirSync3(dir, { recursive: true });
3081
- const ws = createWriteStream(join3(dir, name));
3082
- const reader = stream.getReader();
3109
+ mkdirSync4(dir, { recursive: true });
3110
+ const ws = createWriteStream2(join3(dir, name));
3111
+ const reader = stream2.getReader();
3083
3112
  try {
3084
3113
  for (; ; ) {
3085
3114
  const { done, value } = await reader.read();
@@ -3097,7 +3126,6 @@ function sendStats(res) {
3097
3126
  const sessions2 = listSessions().map((s) => ({
3098
3127
  id: s.id,
3099
3128
  requests: s.requests,
3100
- condensedToolResults: s.condensedToolResults,
3101
3129
  tokensSaved: s.tokensSaved,
3102
3130
  lastSeen: new Date(s.lastSeen).toISOString()
3103
3131
  }));
@@ -3144,27 +3172,133 @@ function readBody(req) {
3144
3172
  }
3145
3173
  function logMsg(opts, level, msg2) {
3146
3174
  if (!opts.log) return;
3147
- const ts = (/* @__PURE__ */ new Date()).toISOString();
3148
- console.error(`${ts} [${level}] ${msg2}`);
3175
+ log(level, msg2);
3176
+ }
3177
+
3178
+ // src/update.ts
3179
+ import { readFile, writeFile, mkdir } from "fs/promises";
3180
+ import { execFile } from "child_process";
3181
+ import path4 from "path";
3182
+ var REGISTRY_BASE = "https://registry.npmjs.org";
3183
+ var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
3184
+ var THROTTLE_FILE = path4.join(cacheDir(), ".update-check");
3185
+ var SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/;
3186
+ var timer;
3187
+ var inFlight = false;
3188
+ function parseVersion(v) {
3189
+ return v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
3190
+ }
3191
+ function isNewer(latest, current) {
3192
+ const l = parseVersion(latest);
3193
+ const c = parseVersion(current);
3194
+ for (let i = 0; i < 3; i++) {
3195
+ const lv = l[i] ?? 0;
3196
+ const cv = c[i] ?? 0;
3197
+ if (lv > cv) return true;
3198
+ if (lv < cv) return false;
3199
+ }
3200
+ return false;
3201
+ }
3202
+ async function readLastCheck() {
3203
+ try {
3204
+ const data = await readFile(THROTTLE_FILE, "utf-8");
3205
+ return parseInt(data.trim(), 10) || 0;
3206
+ } catch {
3207
+ return 0;
3208
+ }
3209
+ }
3210
+ async function writeLastCheck(ts) {
3211
+ try {
3212
+ await mkdir(path4.dirname(THROTTLE_FILE), { recursive: true });
3213
+ await writeFile(THROTTLE_FILE, String(ts), "utf-8");
3214
+ } catch {
3215
+ }
3216
+ }
3217
+ async function checkForUpdate(opts, force = false) {
3218
+ if (!opts.autoUpdate && !force) return;
3219
+ if (inFlight) return;
3220
+ inFlight = true;
3221
+ try {
3222
+ const now = Date.now();
3223
+ if (!force && now - await readLastCheck() < CHECK_INTERVAL_MS) return;
3224
+ await writeLastCheck(now);
3225
+ const url = `${REGISTRY_BASE}/${opts.packageName}/latest`;
3226
+ const res = await fetch(url, {
3227
+ signal: AbortSignal.timeout(5e3),
3228
+ headers: { Accept: "application/json" }
3229
+ });
3230
+ if (!res.ok) return;
3231
+ const data = await res.json();
3232
+ const latest = data.version;
3233
+ if (!latest || !isNewer(latest, opts.currentVersion)) return;
3234
+ const installed = await installLatest(opts.packageName, latest);
3235
+ if (installed) {
3236
+ console.error(
3237
+ `\x1B[32m\u2714 ${opts.packageName} auto-updated ${opts.currentVersion} \u2192 ${latest}. Restart bili to finish.\x1B[0m`
3238
+ );
3239
+ } else {
3240
+ console.error(
3241
+ `\x1B[33m${opts.packageName} ${latest} is available (you have ${opts.currentVersion}). Update with: npm install -g ${opts.packageName}@latest\x1B[0m`
3242
+ );
3243
+ }
3244
+ } catch {
3245
+ } finally {
3246
+ inFlight = false;
3247
+ }
3248
+ }
3249
+ async function installLatest(packageName, latest) {
3250
+ if (!SEMVER_RE.test(latest)) return false;
3251
+ try {
3252
+ const code = await new Promise((resolve) => {
3253
+ execFile(
3254
+ "npm",
3255
+ ["install", "-g", `${packageName}@${latest}`, "--silent", "--no-audit", "--no-fund"],
3256
+ { timeout: 12e4, shell: process.platform === "win32" },
3257
+ (err) => resolve(err ? 1 : 0)
3258
+ );
3259
+ });
3260
+ return code === 0;
3261
+ } catch {
3262
+ return false;
3263
+ }
3264
+ }
3265
+ function startAutoUpdate(opts) {
3266
+ setTimeout(() => {
3267
+ void checkForUpdate(opts);
3268
+ }, 1e4);
3269
+ timer = setInterval(() => {
3270
+ void checkForUpdate(opts);
3271
+ }, CHECK_INTERVAL_MS);
3272
+ timer.unref?.();
3149
3273
  }
3150
3274
 
3151
3275
  // src/cli.ts
3152
3276
  import { readFileSync as readFileSync3 } from "fs";
3153
3277
  import { fileURLToPath } from "url";
3154
- import path3 from "path";
3278
+ import path5 from "path";
3155
3279
  var VERSION = (() => {
3156
3280
  try {
3157
3281
  const here = fileURLToPath(import.meta.url);
3158
- const pkg = path3.join(path3.dirname(here), "..", "package.json");
3282
+ const pkg = path5.join(path5.dirname(here), "..", "package.json");
3159
3283
  return JSON.parse(readFileSync3(pkg, "utf8")).version ?? "dev";
3160
3284
  } catch {
3161
3285
  return "dev";
3162
3286
  }
3163
3287
  })();
3288
+ var PACKAGE_NAME = (() => {
3289
+ try {
3290
+ const here = fileURLToPath(import.meta.url);
3291
+ const pkg = path5.join(path5.dirname(here), "..", "package.json");
3292
+ return JSON.parse(readFileSync3(pkg, "utf8")).name ?? "billion-context";
3293
+ } catch {
3294
+ return "billion-context";
3295
+ }
3296
+ })();
3164
3297
  var HELP = `bili ${VERSION} \u2014 billion-context proxy
3165
3298
 
3166
3299
  Usage:
3167
3300
  bili [start] [options] start the proxy (default: reads ${configFile()})
3301
+ bili update check for & install a newer version now
3168
3302
  bili --version print version
3169
3303
  bili --help show this help
3170
3304
 
@@ -3175,9 +3309,10 @@ Options (override config file / env):
3175
3309
  --debug verbose logging
3176
3310
  --passthrough forward without compression
3177
3311
  --no-passthrough force compression on (overrides config)
3312
+ --no-auto-update disable background self-update this run
3178
3313
 
3179
3314
  Config: ${configFile()}
3180
- Set port/host/debug/providers/condense/compress there. See README \xA7Configuration.
3315
+ Set port/host/debug/providers/compress/autoUpdate there. See README \xA7Configuration.
3181
3316
  Env vars (ACP_*, BILI_*) also work and override the file; CLI flags win.
3182
3317
 
3183
3318
  Docs: https://github.com/ranxianglei/billion-context
@@ -3200,6 +3335,9 @@ function parseArgs(argv) {
3200
3335
  case "--debug":
3201
3336
  overrides.ACP_DEBUG = "1";
3202
3337
  break;
3338
+ case "--no-auto-update":
3339
+ overrides.ACP_AUTO_UPDATE = "0";
3340
+ break;
3203
3341
  case "--passthrough":
3204
3342
  overrides.ACP_PASSTHROUGH = "1";
3205
3343
  break;
@@ -3237,6 +3375,8 @@ function parseArgs(argv) {
3237
3375
  const cmd = positional[0];
3238
3376
  if (cmd === "start") {
3239
3377
  command = command === "help" || command === "version" ? command : "start";
3378
+ } else if (cmd === "update") {
3379
+ command = "update";
3240
3380
  } else {
3241
3381
  console.error(`bili: unknown command "${cmd}" (try "bili --help")`);
3242
3382
  process.exit(2);
@@ -3254,17 +3394,23 @@ async function main() {
3254
3394
  process.stdout.write(VERSION + "\n");
3255
3395
  return;
3256
3396
  }
3397
+ if (command === "update") {
3398
+ await checkForUpdate({ packageName: PACKAGE_NAME, currentVersion: VERSION, autoUpdate: true }, true);
3399
+ return;
3400
+ }
3257
3401
  for (const [k, v] of Object.entries(overrides)) {
3258
3402
  if (v !== void 0) process.env[k] = v;
3259
3403
  }
3260
3404
  const opts = loadOptions();
3261
3405
  await startServer(opts);
3406
+ if (opts.autoUpdate) {
3407
+ startAutoUpdate({ packageName: PACKAGE_NAME, currentVersion: VERSION, autoUpdate: true });
3408
+ }
3262
3409
  }
3410
+
3411
+ // src/index.ts
3263
3412
  main().catch((err) => {
3264
3413
  console.error("bili: failed to start:", err);
3265
3414
  process.exit(1);
3266
3415
  });
3267
-
3268
- // src/index.ts
3269
- main();
3270
3416
  //# sourceMappingURL=index.js.map