billion-context 0.1.83 → 0.1.84-pr.370.354

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/README.md CHANGED
@@ -112,6 +112,17 @@ turn is a semantic mismatch the model tolerates (it is clearly marked
112
112
  summaries (their tool call isn't in the agent's history and the agent's view
113
113
  skips `acp_summary`) — but that needs the id match above, which doesn't occur.
114
114
 
115
+ ## Which do I need?
116
+
117
+ Pick by your client:
118
+
119
+ | Client | Use |
120
+ |---|---|
121
+ | **pi** | [`billion-context-pi`](https://github.com/ranxianglei/billion-context-pi) (in-process extension) |
122
+ | **opencode** | [`opencode-acp`](https://github.com/ranxianglei/opencode-acp) (in-process extension) |
123
+ | **omp** | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili omp` (built-in plugin) |
124
+ | **everything else** (no context hook) | [`billion-context`](https://github.com/ranxianglei/billion-context) — `bili <client>` (launcher, preferred) or `/bili/` prefix |
125
+
115
126
  ## Install
116
127
 
117
128
  ```bash
@@ -382,7 +393,7 @@ pass an explicit `x-acp-session` header per conversation to avoid collisions.
382
393
 
383
394
  Early. Protocol handling and compression work against mock tests (500+ passing). Real-model integration testing is the next milestone. Expect rough edges.
384
395
 
385
- See [billion-context-pi](https://github.com/ranxianglei/billion-context-pi) for the pi-extension mode (in-process, tighter integration, the reference implementation).
396
+ Client-side plugins for pi / omp / opencode ship inside `billion-context` (`dist/agent/*.js`) for the cooperative-proxy path. See the **"Which do I need?"** section above for how `billion-context`, the standalone `billion-context-pi`, and `opencode-acp` relate.
386
397
 
387
398
  ## License
388
399
 
package/README.zh-CN.md CHANGED
@@ -50,6 +50,17 @@ AI 编程助手的<strong>通用上下文压缩代理</strong>
50
50
 
51
51
  代理向对话注入四个上下文管理工具(`compress`、`decompress`、`search_context`、`acp_status`)。模型在对话增长时调用 `compress`,代理在服务端执行 —— 压缩后的范围在下一轮之前折叠进对话历史。
52
52
 
53
+ ## 该选哪个?
54
+
55
+ 按客户端选:
56
+
57
+ | 客户端 | 用这个 |
58
+ |---|---|
59
+ | **pi** | [`billion-context-pi`](https://github.com/ranxianglei/billion-context-pi)(进程内扩展) |
60
+ | **opencode** | [`opencode-acp`](https://github.com/ranxianglei/opencode-acp)(进程内扩展) |
61
+ | **omp** | [`billion-context`](https://github.com/ranxianglei/billion-context),`bili omp`(内置插件) |
62
+ | **其余所有**(没有上下文 hook) | [`billion-context`](https://github.com/ranxianglei/billion-context) —— `bili <client>`(启动器,优先)或 `/bili/` 前缀 |
63
+
53
64
  ## 安装
54
65
 
55
66
  ```bash
@@ -258,7 +269,7 @@ Windows 下会自动发现常见 Clash/Mihomo 静态系统代理;Web UI 会显
258
269
 
259
270
  早期。协议处理和压缩已通过 mock 测试(500+ 项通过)。真实模型集成测试是下一里程碑。预期会有粗糙的地方。
260
271
 
261
- pi 扩展模式(进程内、更紧密集成、参考实现)见 [billion-context-pi](https://github.com/ranxianglei/billion-context-pi)。
272
+ 针对 pi / omp / opencode 的客户端插件随 `billion-context` 一起发布(`dist/agent/*.js`),用于协作代理路径。三者(`billion-context`、独立的 `billion-context-pi`、`opencode-acp`)如何取舍,见上文「该选哪个?」一节。
262
273
 
263
274
  ## 许可证
264
275
 
package/dist/index.js CHANGED
@@ -49370,6 +49370,7 @@ function subagentNamespace(identityValue, instructions) {
49370
49370
 
49371
49371
  // src/persist.ts
49372
49372
  import { createHash as createHash3 } from "crypto";
49373
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
49373
49374
  import { rm } from "fs/promises";
49374
49375
  import * as path5 from "path";
49375
49376
 
@@ -49806,6 +49807,7 @@ function rel(p2, base) {
49806
49807
 
49807
49808
  // src/persist.ts
49808
49809
  var PERSIST_VERSION = 3;
49810
+ var MIGRATION_MARKER = ".bili-migration-286.done";
49809
49811
  function mergeState(parsed) {
49810
49812
  const fresh = createInitialState();
49811
49813
  return {
@@ -49865,6 +49867,21 @@ var SessionStore = class {
49865
49867
  }
49866
49868
  return out;
49867
49869
  }
49870
+ /** Single-pass boot (#401): ONE loadAll walk+parse, then the #286
49871
+ * identity migration over the SAME parsed map (no extra directory
49872
+ * walk), then hydration into Sessions. initSessions calls this instead
49873
+ * of migrateLegacyIds()+loadAll(), which walked and parsed the whole
49874
+ * tree twice per start. */
49875
+ async boot() {
49876
+ if (!this.enabled) return /* @__PURE__ */ new Map();
49877
+ const loaded = await this.store.loadAll();
49878
+ await this.applyLegacyMigration(loaded);
49879
+ const out = /* @__PURE__ */ new Map();
49880
+ for (const [id, envelope] of loaded) {
49881
+ out.set(id, buildSession(envelope.payload));
49882
+ }
49883
+ return out;
49884
+ }
49868
49885
  /** One-time migration for the #286 identity change: sessions persisted
49869
49886
  * under the old derived hash id are re-keyed to the client-provided
49870
49887
  * conversation value stored in meta.label (which is now the session id
@@ -49873,10 +49890,21 @@ var SessionStore = class {
49873
49890
  * session, are deleted. Records without a label cannot be mapped and are
49874
49891
  * left in place (they load under their old id but are never requested
49875
49892
  * again — the new proxy 400s anonymous requests). Self-terminating:
49876
- * after one pass no loaded id differs from its label. */
49893
+ * after one pass no loaded id differs from its label. A completion
49894
+ * marker makes it run ONCE EVER (#401): the old code re-scanned the tree
49895
+ * on every boot because unlabeled files are intentionally kept, so the
49896
+ * "one-time" log line repeated forever. */
49877
49897
  async migrateLegacyIds() {
49878
49898
  if (!this.enabled) return;
49899
+ if (existsSync3(this.markerPath())) return;
49879
49900
  const loaded = await this.store.loadAll();
49901
+ await this.applyLegacyMigration(loaded);
49902
+ }
49903
+ markerPath() {
49904
+ return path5.join(this.dir, MIGRATION_MARKER);
49905
+ }
49906
+ async applyLegacyMigration(loaded) {
49907
+ if (existsSync3(this.markerPath())) return;
49880
49908
  const claimed = /* @__PURE__ */ new Set();
49881
49909
  const byLabel = /* @__PURE__ */ new Map();
49882
49910
  let unlabeled = 0;
@@ -49894,24 +49922,39 @@ var SessionStore = class {
49894
49922
  }
49895
49923
  const prev = byLabel.get(label);
49896
49924
  if (!prev || envelope.savedAt >= prev.savedAt) {
49897
- if (prev) await this.removeLegacyFile(prev.id, prev.session);
49925
+ if (prev) {
49926
+ await this.removeLegacyFile(prev.id, prev.session);
49927
+ loaded.delete(prev.id);
49928
+ }
49898
49929
  byLabel.set(label, { id, savedAt: envelope.savedAt, session });
49899
49930
  } else {
49900
49931
  await this.removeLegacyFile(id, session);
49932
+ loaded.delete(id);
49901
49933
  }
49902
49934
  }
49903
49935
  let rekeyed = 0;
49904
49936
  for (const [label, { id, session }] of byLabel) {
49905
49937
  if (claimed.has(label)) {
49906
49938
  await this.removeLegacyFile(id, session);
49939
+ loaded.delete(id);
49907
49940
  continue;
49908
49941
  }
49909
49942
  session.id = label;
49910
49943
  await this.store.writeNow(label, () => buildRecord(session));
49911
49944
  await this.removeLegacyFile(id, session);
49945
+ const envelope = loaded.get(id);
49946
+ if (envelope) {
49947
+ loaded.set(label, { ...envelope, id: label, payload: { ...envelope.payload, id: label } });
49948
+ }
49949
+ loaded.delete(id);
49912
49950
  claimed.add(label);
49913
49951
  rekeyed++;
49914
49952
  }
49953
+ try {
49954
+ mkdirSync3(this.dir, { recursive: true });
49955
+ writeFileSync2(this.markerPath(), String(Date.now()), "utf8");
49956
+ } catch {
49957
+ }
49915
49958
  if (rekeyed || unlabeled) {
49916
49959
  log("info", `[persist] one-time migration (#286): rekeyed ${rekeyed} legacy session(s), left ${unlabeled} unlabeled legacy file(s) in place`);
49917
49960
  }
@@ -49997,10 +50040,21 @@ function buildRecord(session) {
49997
50040
  createdAt: session.createdAt
49998
50041
  };
49999
50042
  }
50043
+ function isBlockView(v2) {
50044
+ return !!v2 && typeof v2 === "object" && typeof v2.text === "string" && typeof v2.count === "number";
50045
+ }
50000
50046
  function buildSession(parsed) {
50001
50047
  const blockContents = /* @__PURE__ */ new Map();
50002
50048
  for (const [bid, content] of Object.entries(parsed.blockContents ?? {})) {
50003
- if (content && typeof content === "object") blockContents.set(bid, content);
50049
+ if (!content || typeof content !== "object") continue;
50050
+ const full = content.full;
50051
+ if (!isBlockView(full)) continue;
50052
+ const one = content.one;
50053
+ const oneView = isBlockView(one) ? one : null;
50054
+ blockContents.set(bid, {
50055
+ one: oneView && !(oneView.text === full.text && oneView.count === full.count) ? oneView : null,
50056
+ full
50057
+ });
50004
50058
  }
50005
50059
  const meta = parsed.meta ?? {};
50006
50060
  const stats = parsed.stats ?? {};
@@ -50085,8 +50139,7 @@ async function initSessions() {
50085
50139
  initialized = true;
50086
50140
  const store = getStore();
50087
50141
  if (!store.enabled) return;
50088
- await store.migrateLegacyIds();
50089
- const loaded = await store.loadAll();
50142
+ const loaded = await store.boot();
50090
50143
  if (loaded.size > MAX_SESSIONS) {
50091
50144
  const freshness = (s3) => Math.max(s3.createdAt ?? 0, s3.lastSeen ?? 0);
50092
50145
  const entries = [...loaded.entries()].sort((a, b2) => freshness(b2[1]) - freshness(a[1]));
@@ -50292,7 +50345,7 @@ function withStagedCompressGuidance(text) {
50292
50345
  }
50293
50346
 
50294
50347
  // src/decompress-shared.ts
50295
- import { mkdirSync as mkdirSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
50348
+ import { mkdirSync as mkdirSync4, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
50296
50349
  import { dirname as dirname2, join as join2 } from "path";
50297
50350
  import { tmpdir } from "os";
50298
50351
  var trackedTempFiles = [];
@@ -50339,7 +50392,7 @@ function resolveDecompress(args, ctx) {
50339
50392
  let body;
50340
50393
  let count;
50341
50394
  if (cached) {
50342
- const view = full ? cached.full : cached.one;
50395
+ const view = full ? cached.full : cached.one ?? cached.full;
50343
50396
  body = view.text;
50344
50397
  count = view.count;
50345
50398
  } else {
@@ -50352,8 +50405,8 @@ function resolveDecompress(args, ctx) {
50352
50405
  const outPath = body.length > 1e4 ? join2(tmpdir(), `acp-decompress-${safeBlockId}-${Date.now()}.txt`) : null;
50353
50406
  if (outPath) {
50354
50407
  try {
50355
- mkdirSync3(dirname2(outPath), { recursive: true });
50356
- writeFileSync2(outPath, body, "utf8");
50408
+ mkdirSync4(dirname2(outPath), { recursive: true });
50409
+ writeFileSync3(outPath, body, "utf8");
50357
50410
  trackedTempFiles.push({ path: outPath, mtimeMs: Date.now() });
50358
50411
  reapTempFiles();
50359
50412
  return `${header}
@@ -50649,8 +50702,9 @@ ${reasons.map((r) => `- ${r}`).join("\n")}` : "";
50649
50702
  const full = collectBlockContent(res.state, b2, ctx.messages, { full: true });
50650
50703
  const one = collectBlockContent(res.state, b2, ctx.messages, { full: false });
50651
50704
  if (full.count > 0 || one.count > 0) {
50705
+ const sameView = one.text === full.text && one.count === full.count;
50652
50706
  cacheBlockContent(ctx.session, b2.blockId, {
50653
- one: { text: one.text, count: one.count },
50707
+ one: sameView ? null : { text: one.text, count: one.count },
50654
50708
  full: { text: full.text, count: full.count }
50655
50709
  });
50656
50710
  }
@@ -51032,7 +51086,7 @@ import { dirname as dirname4, join as join3 } from "path";
51032
51086
  import { fileURLToPath } from "url";
51033
51087
 
51034
51088
  // src/web/page.ts
51035
- import { existsSync as existsSync3 } from "fs";
51089
+ import { existsSync as existsSync4 } from "fs";
51036
51090
 
51037
51091
  // src/ca.ts
51038
51092
  var import_node_forge = __toESM(require_lib(), 1);
@@ -51216,7 +51270,7 @@ function escapeHtml(value) {
51216
51270
  function renderPage(origin, version2) {
51217
51271
  const caPath = rootCaPath();
51218
51272
  const caPathEsc = escapeHtml(caPath);
51219
- const caReady = existsSync3(caPath);
51273
+ const caReady = existsSync4(caPath);
51220
51274
  return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>billion-context</title><style>${WEB_STYLES}</style></head><body>
51221
51275
  <div class="app"><aside class="sidebar"><div class="brand"><div class="brand-title">billion<span>-context</span></div><div class="version">v${version2}</div></div><a class="fork-ribbon" href="https://github.com/ranxianglei/billion-context" target="_blank" rel="noopener">\u2605 Fork me on GitHub</a><nav class="nav"><button class="active" data-page="overview">\u6982\u89C8</button><button data-page="routing">\u63A5\u5165</button><button data-page="upstream">\u4E0A\u6E38</button><button data-page="sessions">\u4F1A\u8BDD</button><button data-page="settings">\u8BBE\u7F6E</button></nav></aside>
51222
51276
  <main class="main"><section id="page-overview" class="page active"><h1>\u6982\u89C8</h1><p class="lead">\u672C\u5730\u8DEF\u7531\u4E0E\u4E0A\u6E38\u7F51\u7EDC\u76F8\u4E92\u72EC\u7ACB\uFF0CACP \u538B\u7F29\u8FD0\u884C\u5728\u4E24\u8005\u4E4B\u95F4\u3002</p><div class="grid"><div class="card"><h2>\u670D\u52A1\u5730\u5740</h2><p class="mono">${origin}</p></div><div class="card"><h2>\u5BA2\u6237\u7AEF\u63A5\u5165</h2><p class="status">\u5728"\u63A5\u5165"\u9875\u5C06\u5404\u5BA2\u6237\u7AEF\uFF08Codex / Claude Code / OpenCode / Pi \u7B49\uFF09\u7684\u6D41\u91CF\u8DEF\u7531\u5230 bili\u3002</p></div></div></section>
@@ -51238,7 +51292,7 @@ function renderPage(origin, version2) {
51238
51292
  }
51239
51293
 
51240
51294
  // src/web/api.ts
51241
- import { closeSync, existsSync as existsSync4, fsyncSync, mkdirSync as mkdirSync4, openSync, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
51295
+ import { closeSync, existsSync as existsSync5, fsyncSync, mkdirSync as mkdirSync5, openSync, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
51242
51296
  import { dirname as dirname3 } from "path";
51243
51297
  import { randomUUID as randomUUID2 } from "crypto";
51244
51298
  function readConfig() {
@@ -51246,7 +51300,7 @@ function readConfig() {
51246
51300
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
51247
51301
  }
51248
51302
  function configParseError() {
51249
- if (!existsSync4(configFile())) return null;
51303
+ if (!existsSync5(configFile())) return null;
51250
51304
  const parsed = safeReadJson(configFile());
51251
51305
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return null;
51252
51306
  return `config file is not valid JSON: ${configFile()}`;
@@ -51264,12 +51318,12 @@ function readUpstreamSettings() {
51264
51318
  }
51265
51319
  function atomicWriteConfig(config) {
51266
51320
  const filePath = configFile();
51267
- mkdirSync4(dirname3(filePath), { recursive: true });
51321
+ mkdirSync5(dirname3(filePath), { recursive: true });
51268
51322
  const tempPath = `${filePath}.${process.pid}.${randomUUID2()}.tmp`;
51269
51323
  let descriptor;
51270
51324
  try {
51271
51325
  descriptor = openSync(tempPath, "wx", 384);
51272
- writeFileSync3(descriptor, JSON.stringify(config, null, 2) + "\n", "utf8");
51326
+ writeFileSync4(descriptor, JSON.stringify(config, null, 2) + "\n", "utf8");
51273
51327
  fsyncSync(descriptor);
51274
51328
  closeSync(descriptor);
51275
51329
  descriptor = void 0;
@@ -51699,6 +51753,23 @@ function isLoopThinking(m2) {
51699
51753
  function stripLoopThinking(messages) {
51700
51754
  return messages.filter((m2) => !isLoopThinking(m2));
51701
51755
  }
51756
+ function canonicalArgs(args) {
51757
+ try {
51758
+ return JSON.stringify(sortKeys(JSON.parse(args)));
51759
+ } catch {
51760
+ return args;
51761
+ }
51762
+ }
51763
+ function sortKeys(v2) {
51764
+ if (Array.isArray(v2)) return v2.map(sortKeys);
51765
+ if (v2 !== null && typeof v2 === "object") {
51766
+ const src = v2;
51767
+ const out = {};
51768
+ for (const k2 of Object.keys(src).sort()) out[k2] = sortKeys(src[k2]);
51769
+ return out;
51770
+ }
51771
+ return v2;
51772
+ }
51702
51773
  function executeProxyTool(toolName, args, ctx, callId) {
51703
51774
  if (toolName === "compress") {
51704
51775
  return applyRanges(parseCompressInput(args, callId), ctx);
@@ -51801,6 +51872,7 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
51801
51872
  log("warn", `[acp-loop] upstream rejected replay (HTTP ${info.status}); retrying in ${info.delayMs}ms (attempt ${info.attempt}/${info.maxAttempts})${lc}`);
51802
51873
  }
51803
51874
  );
51875
+ const failedSignatures = /* @__PURE__ */ new Set();
51804
51876
  try {
51805
51877
  for (let round = 1; round <= MAX_LOOP_ROUNDS; round++) {
51806
51878
  if (signal?.aborted) break;
@@ -52055,6 +52127,19 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
52055
52127
  yield adapter.emitCompletion({ finishReason: "length", usage });
52056
52128
  return;
52057
52129
  }
52130
+ const failedMutating = proxyResults.filter(
52131
+ (pr2) => (pr2.name === "compress" || pr2.name === "decompress") && pr2.result.includes("FAILED")
52132
+ );
52133
+ if (reRequest && failedMutating.length > 0 && failedMutating.length === proxyResults.length) {
52134
+ const repeated = failedMutating.some((pr2) => failedSignatures.has(`${pr2.name}\0${canonicalArgs(pr2.arguments)}`));
52135
+ if (repeated) {
52136
+ ctx.log(`[acp-loop] round ${round}: model re-submitted an already-failed compress with identical arguments; stopping after ${round} round(s) instead of ${MAX_LOOP_ROUNDS}`);
52137
+ log("warn", `[acp-loop] repeated identical compress failure at round ${round}; breaking early (#156)`);
52138
+ yield adapter.emitCompletion({ finishReason: "length", usage });
52139
+ return;
52140
+ }
52141
+ }
52142
+ for (const pr2 of failedMutating) failedSignatures.add(`${pr2.name}\0${canonicalArgs(pr2.arguments)}`);
52058
52143
  ctx.log(`[acp-loop] round ${round}: proxy tool executed; re-requesting so the model sees the result`);
52059
52144
  if (signal?.aborted) break;
52060
52145
  let newBody = adapter.buildRequest(coreMessages, systemPrompt, requestBody);
@@ -54087,14 +54172,14 @@ var MIN_TAIL_MATCH = 3;
54087
54172
  var MAX_STORED_ITEMS = 128;
54088
54173
  var MIN_FORK_PREFIX = 3;
54089
54174
  function stableStringify(value) {
54090
- return JSON.stringify(sortKeys(value));
54175
+ return JSON.stringify(sortKeys2(value));
54091
54176
  }
54092
- function sortKeys(value) {
54093
- if (Array.isArray(value)) return value.map(sortKeys);
54177
+ function sortKeys2(value) {
54178
+ if (Array.isArray(value)) return value.map(sortKeys2);
54094
54179
  if (value && typeof value === "object") {
54095
54180
  const record = value;
54096
54181
  const out = {};
54097
- for (const key of Object.keys(record).sort()) out[key] = sortKeys(record[key]);
54182
+ for (const key of Object.keys(record).sort()) out[key] = sortKeys2(record[key]);
54098
54183
  return out;
54099
54184
  }
54100
54185
  return value;
@@ -56964,7 +57049,7 @@ Content-Length: ${Buffer.byteLength(body)}\r
56964
57049
  );
56965
57050
  }
56966
57051
  if (opts.debug) {
56967
- log2("info", `[debug] build features: raw-HTTP-capture(on) | remote_compaction_v2-strip(on) | cert-MITM-launcher(on) | strip-acp-summary(on) \u2014 seeing this line confirms the launcher build (not registry 0.1.34)`);
57052
+ log2("info", `[debug] build features: raw-HTTP-capture(${bodyDumpEnabled() ? "on" : "off"}) | remote_compaction_v2-strip(on) | cert-MITM-launcher(on) | strip-acp-summary(on) \u2014 seeing this line confirms the launcher build (not registry 0.1.34)`);
56968
57053
  }
56969
57054
  };
56970
57055
  const attemptListen = (port) => {
@@ -58640,10 +58725,10 @@ async function pipeThrough(stream2, res) {
58640
58725
  }
58641
58726
  }
58642
58727
  async function dumpStreamToFile(stream2, dir, name) {
58643
- const { mkdirSync: mkdirSync6, createWriteStream: createWriteStream2 } = await import("fs");
58728
+ const { mkdirSync: mkdirSync7, createWriteStream: createWriteStream2 } = await import("fs");
58644
58729
  const { join: join4 } = await import("path");
58645
58730
  try {
58646
- mkdirSync6(dir, { recursive: true });
58731
+ mkdirSync7(dir, { recursive: true });
58647
58732
  const ws2 = createWriteStream2(join4(dir, name));
58648
58733
  ws2.on("error", (e) => {
58649
58734
  logDumpFailure("SSE stream dump", e);
@@ -62704,7 +62789,6 @@ function selfDistFile(name) {
62704
62789
  return path15.join(selfPackageRoot(), "dist", name);
62705
62790
  }
62706
62791
  var LAUNCHER_DEFAULT_HOST = "127.0.0.1";
62707
- var LAUNCHER_DEFAULT_PORT = 8787;
62708
62792
  var LAUNCH_CLIENTS = ["pi", "codex", "claude", "omp", "opencode", "hermes", "dsh", "pi-test"];
62709
62793
  var HEALTH_PATH = "/__bili/health";
62710
62794
  var HEALTH_POLL_INTERVAL_MS = 200;
@@ -63878,6 +63962,19 @@ async function probeExistingInstance(readInstance, fetchHealthInfo) {
63878
63962
  if (health.instanceId !== void 0 && health.instanceId !== inst.instanceId) return void 0;
63879
63963
  return inst;
63880
63964
  }
63965
+ function pickEphemeralPort(host = LAUNCHER_DEFAULT_HOST) {
63966
+ return new Promise((resolve, reject) => {
63967
+ const srv = net2.createServer();
63968
+ srv.once("error", reject);
63969
+ srv.listen(0, host, () => {
63970
+ const addr = srv.address();
63971
+ srv.close(() => {
63972
+ if (addr && typeof addr === "object") resolve(addr.port);
63973
+ else reject(new Error("could not allocate a free port"));
63974
+ });
63975
+ });
63976
+ });
63977
+ }
63881
63978
  var INHERITED_PROXY_VARS = ["http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"];
63882
63979
  function stripInheritedProxy(env) {
63883
63980
  const cleaned = { ...env };
@@ -63903,7 +64000,7 @@ async function ensureProxyRunning(opts, deps = {}) {
63903
64000
  return { origin: existing.origin, port: existing.port, attached: true };
63904
64001
  }
63905
64002
  const launchToken = randomUUID5();
63906
- const port = opts.port;
64003
+ const port = opts.port > 0 ? opts.port : await pickEphemeralPort(opts.host);
63907
64004
  const script = process.argv[1];
63908
64005
  if (!script) throw new Error("bili: cannot resolve launcher script path");
63909
64006
  const logPath2 = path15.join(os5.tmpdir(), `bili-proxy-${port}.log`);
@@ -63912,7 +64009,7 @@ async function ensureProxyRunning(opts, deps = {}) {
63912
64009
  try {
63913
64010
  child = spawnImpl(
63914
64011
  process.execPath,
63915
- [script, ...proxyStartArgs(opts)],
64012
+ [script, ...proxyStartArgs({ ...opts, port })],
63916
64013
  {
63917
64014
  detached: true,
63918
64015
  stdio: ["ignore", logFd, logFd],
@@ -64019,8 +64116,9 @@ function resolveClientCommand(client, env) {
64019
64116
  return { command: resolved ?? client, prefixArgs: [] };
64020
64117
  }
64021
64118
  function parsePort(raw) {
64022
- const port = raw && raw.trim() ? parseInt(raw, 10) : LAUNCHER_DEFAULT_PORT;
64023
- if (!Number.isFinite(port) || port <= 0 || port > 65535) {
64119
+ if (!raw || !raw.trim()) return 0;
64120
+ const port = parseInt(raw, 10);
64121
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
64024
64122
  console.error(`bili: invalid --port "${raw}"`);
64025
64123
  process.exit(2);
64026
64124
  }
@@ -64248,7 +64346,7 @@ async function runTestPi(params, deps = {}) {
64248
64346
  }
64249
64347
 
64250
64348
  // src/export.ts
64251
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
64349
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
64252
64350
  import path16 from "path";
64253
64351
  function fmtDate(ms2) {
64254
64352
  return ms2 ? new Date(ms2).toISOString().replace("T", " ").slice(0, 19) + " UTC" : "\u2014";
@@ -64381,8 +64479,8 @@ async function exportSession(selector, opts = {}) {
64381
64479
  }
64382
64480
  const markdown = renderHandoff(matches[0], opts.full ?? false);
64383
64481
  if (opts.output) {
64384
- mkdirSync5(path16.dirname(path16.resolve(opts.output)), { recursive: true });
64385
- writeFileSync4(opts.output, markdown, "utf8");
64482
+ mkdirSync6(path16.dirname(path16.resolve(opts.output)), { recursive: true });
64483
+ writeFileSync5(opts.output, markdown, "utf8");
64386
64484
  return `written to ${opts.output}`;
64387
64485
  }
64388
64486
  return markdown;
@@ -64459,7 +64557,7 @@ Launcher (bili pi / bili codex / bili claude / bili omp / bili opencode / bili h
64459
64557
  Options (override config file / env):
64460
64558
  -F <url> upstream proxy to forward through (gost-style;
64461
64559
  http://host:port; must precede the client name)
64462
- --port <N> listen port (default 8787)
64560
+ --port <N> listen port (start: 8787; launcher default: random free port)
64463
64561
  --host <ADDR> listen host (default 127.0.0.1)
64464
64562
  --mitm-domain <domain> extra MITM domain (repeatable; launcher only)
64465
64563
  --config <FILE> path to config JSON (default: XDG location)