@xiaoso/dsh-tool-plus 0.1.16 → 0.1.17-beta.0

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.
Files changed (109) hide show
  1. package/lib/{abort-DZ54CaKU.mjs → abort-BTblp0Rt.mjs} +2 -2
  2. package/lib/{alibaba-coding-plan-DODpkcqi.mjs → alibaba-coding-plan-t3R4YKCB.mjs} +3 -3
  3. package/lib/{alibaba-token-plan-C6lpKCda.mjs → alibaba-token-plan-CO4djG1y.mjs} +4 -4
  4. package/lib/{alibaba-token-plan-CHGyWOfX.mjs → alibaba-token-plan-CUjYJX0-.mjs} +1 -1
  5. package/lib/{anthropic-Dv_GGX6g.mjs → anthropic-nf7w6so1.mjs} +48 -21
  6. package/lib/{api-key-validation-MnMf_ihX.mjs → api-key-validation-C_tZ7Y9a.mjs} +2 -2
  7. package/lib/behavior-BwvTUtag.mjs +111 -0
  8. package/lib/{browser-render-BZXt548F.mjs → browser-render-BQaEVK_n.mjs} +4 -4
  9. package/lib/{reference-CtvM4zLU.mjs → bundled-references-CrQ4dQcT.mjs} +57 -4
  10. package/lib/cascade-BWjRTD1q.mjs +30265 -0
  11. package/lib/{cline-pass-BT9CBEOj.mjs → cline-pass-Bjm-O8GG.mjs} +2 -2
  12. package/lib/cloudflare-CKXRJ0l9.mjs +37 -0
  13. package/lib/{cloudflare-ai-gateway-DReN6ohJ.mjs → cloudflare-ai-gateway-BpnBSjwV.mjs} +8 -4
  14. package/lib/cloudflare-workers-ai-odj9XswL.mjs +32 -0
  15. package/lib/collapse-_nkb2dn7.mjs +1945 -0
  16. package/lib/{coreweave-C82HrLpb.mjs → coreweave-CGDZOD8-.mjs} +1 -1
  17. package/lib/{coreweave-C68UgRQO.mjs → coreweave-DwffcgW5.mjs} +4 -4
  18. package/lib/cursor-BgOuV7Qf.mjs +794 -0
  19. package/lib/{cursor-_gHHq-Dq.mjs → cursor-CF1iHHbv.mjs} +68 -19
  20. package/lib/{cursor-proto-Cr_WEMuv.mjs → cursor-proto-BuNTjX3L.mjs} +823 -23
  21. package/lib/{devin-QpfTSl9H.mjs → devin-7OEeEb80.mjs} +112 -31
  22. package/lib/{devin-proto-DYhp4Ty6.mjs → devin-proto-C-QreeAv.mjs} +71 -5
  23. package/lib/{dom-DG3sPESc.mjs → dom-BmbZv4v9.mjs} +598 -182
  24. package/lib/{error-O63pC8zg.mjs → error-8LVo7tug.mjs} +725 -104
  25. package/lib/factory-droid-ChhmsGWb.mjs +71 -0
  26. package/lib/{fs-promises-shim-B2YFWkcd.mjs → fs-promises-shim-CANAxFWh.mjs} +10 -1
  27. package/lib/{github-copilot-BMEcw8N1.mjs → github-copilot-QwevdT30.mjs} +4 -4
  28. package/lib/{github-copilot-headers-DnjGEbko.mjs → github-copilot-headers-CAUZ0rUD.mjs} +138 -26
  29. package/lib/{gitlab-duo-DnHqJjFY.mjs → gitlab-duo-DigV7k_o.mjs} +2 -2
  30. package/lib/{google-antigravity-B1mSckrK.mjs → google-antigravity-D-oe0r8A.mjs} +6 -7
  31. package/lib/{google-gemini-cli-DJwThCCn.mjs → google-gemini-cli-CQBHgGJT.mjs} +5 -6
  32. package/lib/{google-oauth-shared-BctLpVUN.mjs → google-oauth-shared-CvgVIUwJ.mjs} +2 -2
  33. package/lib/google-validation-CdvRj4ec.mjs +126 -0
  34. package/lib/id-EJwVLDHE.mjs +2548 -0
  35. package/lib/index.mjs +248 -258
  36. package/lib/{kilo-BEgEwMJQ.mjs → kilo-5r6OffCa.mjs} +2 -2
  37. package/lib/{kimi-T9NldEIJ.mjs → kimi-DOOtbXmm.mjs} +9 -10
  38. package/lib/{launch-BIWNyrBB.mjs → launch-DkAjib7h.mjs} +1 -1
  39. package/lib/{markit-Dvp2JPdT.mjs → markit-CCcyeAds.mjs} +359 -140
  40. package/lib/{models-DnXMYGcJ.mjs → models-DCpDT_im.mjs} +199203 -112275
  41. package/lib/{muse-code-CNvqqAp6.mjs → muse-code-CYf8zj_B.mjs} +5 -5
  42. package/lib/{openai-codex-D5-CyTxa.mjs → openai-codex-Cj-TvSFh.mjs} +13 -10
  43. package/lib/{perplexity-A2PEoGaC.mjs → perplexity-Dh_Qgsmx.mjs} +68 -25
  44. package/lib/{pkce-BYjmugmw.mjs → pkce-DO-jBJLV.mjs} +1 -1
  45. package/lib/pricing-DusBEhwm.mjs +593 -0
  46. package/lib/{readability-uGhqb4GC.mjs → readability-XHK0o3bv.mjs} +287 -27
  47. package/lib/{scrapers-C1ujjV8k.mjs → scrapers-B6PADOVm.mjs} +13 -13
  48. package/lib/slugify-ynYWBIA5.mjs +11 -0
  49. package/lib/snowflake-B_jap6lz.mjs +666 -0
  50. package/lib/snowflake-Cu1ZyOp6.mjs +151 -0
  51. package/lib/{src-AELUUb5U.mjs → src-BhinCSpi.mjs} +1172 -458
  52. package/lib/{stream-AVOoe5Hj.mjs → stream-D2ivg3GH.mjs} +3740 -5311
  53. package/lib/{turndown-D6fof1OX.mjs → turndown-Vya8SavL.mjs} +82 -30
  54. package/lib/types/tools/hashline/native/index.d.ts +2 -2
  55. package/lib/types/tools/hashline/native/policy.d.ts +19 -21
  56. package/lib/types/tools/omp/session/agent-storage.d.ts +3 -3
  57. package/lib/types/tools/shared/bun-named-shim.d.ts +5 -0
  58. package/lib/{types-BLrLohwK.mjs → types-tH1v9uRm.mjs} +2 -2
  59. package/lib/{utils-cZX3f_Ls.mjs → utils-C_Ncya1r.mjs} +1737 -509
  60. package/lib/utils-D2tK38xq.mjs +34006 -0
  61. package/lib/{utils-I4I8azHN.mjs → utils-DrDwmNtu.mjs} +8 -3
  62. package/lib/{xai-oauth-CbeldUSV.mjs → xai-oauth-DLLzX6my.mjs} +4 -4
  63. package/lib/{xiaomi-Dl3VSs04.mjs → xiaomi-CqK-KivK.mjs} +2 -2
  64. package/lib/{zai-FyjGSypr.mjs → zai-DoJ2xPIH.mjs} +2 -2
  65. package/lib/{zip-DRrycScC.mjs → zip-DG1mUV6X.mjs} +1 -1
  66. package/package.json +8 -8
  67. package/lib/amazon-bedrock-Dx0rr9dY.mjs +0 -996
  68. package/lib/anthropic-ClCw0PV_.mjs +0 -3624
  69. package/lib/anthropic-wire-lbGAFzYb.mjs +0 -927
  70. package/lib/azure-openai-responses-WE_bgFWp.mjs +0 -285
  71. package/lib/bundled-references-DY3EJxZV.mjs +0 -56
  72. package/lib/cloudflare-ai-gateway-CrMa97Ap.mjs +0 -44
  73. package/lib/collapse-CkMNRNr7.mjs +0 -2304
  74. package/lib/cursor-C6jaIJDg.mjs +0 -283
  75. package/lib/cursor-D-XZYDf_.mjs +0 -4544
  76. package/lib/deterministic-id-CUEIMSqR.mjs +0 -17
  77. package/lib/devin-ExGIghmx.mjs +0 -69
  78. package/lib/devin-u0ljw5P6.mjs +0 -628
  79. package/lib/effort-BdtQMN-_.mjs +0 -23
  80. package/lib/event-stream-BKEtb45Y.mjs +0 -167
  81. package/lib/gemini-headers-Co9cVvex.mjs +0 -54
  82. package/lib/google-auth-qAN-6DMW.mjs +0 -232
  83. package/lib/google-gemini-cli-B2Aw1S5w.mjs +0 -844
  84. package/lib/google-lTxuq28n.mjs +0 -38
  85. package/lib/google-shared-Y01j9xOr.mjs +0 -856
  86. package/lib/google-validation-BEkNs-Bx.mjs +0 -18
  87. package/lib/google-vertex-DcM-FRC5.mjs +0 -99
  88. package/lib/hosts-00rjRzUD.mjs +0 -203
  89. package/lib/id-AL5obQzR.mjs +0 -71
  90. package/lib/idle-iterator-D0ZXEcUq.mjs +0 -366
  91. package/lib/ollama-6R0oXZks.mjs +0 -574
  92. package/lib/openai-codex-responses-C8bnBWOe.mjs +0 -3232
  93. package/lib/openai-completions-DNr3mUsn.mjs +0 -1430
  94. package/lib/openai-configuration-update-xMYUjaK_.mjs +0 -150
  95. package/lib/openai-reasoning-fallback-Ju-5pSR8.mjs +0 -380
  96. package/lib/openai-responses-CjpafAEr.mjs +0 -861
  97. package/lib/protobuf-BMgDYu_U.mjs +0 -717
  98. package/lib/provider-response-SqKSkkUc.mjs +0 -22
  99. package/lib/proxy-DxvCQOCA.mjs +0 -230
  100. package/lib/request-debug-BwPOqN_j.mjs +0 -183
  101. package/lib/sanitize-text-BCf5KmCf.mjs +0 -33
  102. package/lib/sse-debug-B-tkLCPW.mjs +0 -11
  103. package/lib/stream-markup-healing-CN3RsqG9.mjs +0 -267
  104. package/lib/strict-tool-validation-CBTz1dAA.mjs +0 -105
  105. package/lib/taxonomy-DSIM99rS.mjs +0 -15392
  106. package/lib/thinking-ngMwf_1Y.mjs +0 -336
  107. package/lib/type-guards-CttAANEx.mjs +0 -11
  108. package/lib/utils-CE3DSebh.mjs +0 -11763
  109. package/lib/vision-guard-BCpXWUj3.mjs +0 -77
@@ -1,21 +1,21 @@
1
1
  import { installBunShim as __installBunShim } from "./bun-shim.mjs";
2
2
  __installBunShim();
3
- import { n as fs_promises_shim_exports } from "./fs-promises-shim-B2YFWkcd.mjs";
3
+ import { n as fs_promises_shim_exports } from "./fs-promises-shim-CANAxFWh.mjs";
4
4
  import * as fs from "node:fs";
5
5
  import { Process } from "@oh-my-pi/pi-natives";
6
6
  import * as path$1 from "node:path";
7
7
  import * as os from "node:os";
8
8
  import assert from "node:assert/strict";
9
+ import "node:timers/promises";
10
+ import { expandWindowsLongPath } from "@oh-my-pi/pi-natives/path";
11
+ import { parseEnv } from "node:util";
12
+ import JSON5 from "json5";
9
13
  import { parse, stringify } from "yaml";
10
14
  import * as fsp from "fs/promises";
11
- import * as crypto$1 from "node:crypto";
12
15
  import { createHash, randomUUID } from "node:crypto";
13
16
  import { gzipSync } from "node:zlib";
14
- import JSON5 from "json5";
15
17
  import { AsyncLocalStorage } from "node:async_hooks";
16
18
  import { isPromise } from "node:util/types";
17
- import inspector from "node:inspector";
18
- import { isMainThread } from "node:worker_threads";
19
19
  import * as tls from "node:tls";
20
20
  //#region src/tools/bash/which.ts
21
21
  /**
@@ -61,7 +61,7 @@ function findOnPath(command, env = process.env) {
61
61
  }
62
62
  }
63
63
  //#endregion
64
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/abortable.ts
64
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/abortable.ts
65
65
  var AbortError$1 = class extends Error {
66
66
  constructor(signal) {
67
67
  assert(signal.aborted, "Abort signal must be aborted");
@@ -154,54 +154,19 @@ function once(fn) {
154
154
  };
155
155
  }
156
156
  //#endregion
157
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/peek-file.ts
157
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/peek-file.ts
158
158
  /**
159
159
  * Read the first `maxBytes` of a file (offset 0) and pass that slice to `op`.
160
160
  *
161
- * Buffers are reused to avoid allocating on every peek: sync uses one growable
162
- * `Uint8Array`; async uses a small fixed pool of `Buffer`s with a bounded wait
163
- * queue, falling back to a fresh allocation when the pool and queue are saturated
164
- * or when `maxBytes` exceeds the pool slot size.
161
+ * Synchronous peeks reuse one growable `Uint8Array`; asynchronous peeks allocate
162
+ * their result window so callback-retained slices cannot be changed by a later read.
165
163
  */
166
- /** Async pool slot size; larger peeks allocate ad hoc. */
167
- const POOLED_BUFFER_SIZE = 512;
168
- const ASYNC_POOL_SIZE = 10;
169
- /** Cap waiter queue so heavy concurrency does not queue unbounded; overflow uses alloc. */
170
- const MAX_ASYNC_WAITERS = 4;
171
164
  const INITIAL_SYNC_BUFFER_SIZE = 1024;
172
- const EMPTY_BUFFER = Buffer.alloc(0);
173
- const asyncPool = Array.from({ length: ASYNC_POOL_SIZE }, () => Buffer.allocUnsafe(POOLED_BUFFER_SIZE));
174
- const availableAsyncPoolIndexes = Array.from({ length: ASYNC_POOL_SIZE }, (_, index) => index);
175
- const asyncPoolWaiters = [];
165
+ const EMPTY_BUFFER = /* @__PURE__ */ new Uint8Array(0);
176
166
  new Uint8Array(INITIAL_SYNC_BUFFER_SIZE);
177
- /** Returns a pool slot index, or `-1` when the caller should use a standalone buffer. */
178
- function acquireAsyncPoolIndex() {
179
- const index = availableAsyncPoolIndexes.pop();
180
- if (index !== void 0) return index;
181
- if (asyncPoolWaiters.length >= MAX_ASYNC_WAITERS) return -1;
182
- const { promise, resolve } = Promise.withResolvers();
183
- asyncPoolWaiters.push(resolve);
184
- return promise;
185
- }
186
- function releaseAsyncPoolIndex(index) {
187
- if (index < 0) return;
188
- const waiter = asyncPoolWaiters.shift();
189
- if (waiter) {
190
- waiter(index);
191
- return;
192
- }
193
- availableAsyncPoolIndexes.push(index);
194
- }
195
- async function withAsyncPoolBuffer(maxBytes, op) {
196
- if (maxBytes <= 0) return op(EMPTY_BUFFER);
197
- if (maxBytes > POOLED_BUFFER_SIZE) return op(Buffer.allocUnsafe(maxBytes));
198
- const poolIndex = await acquireAsyncPoolIndex();
199
- const buffer = poolIndex >= 0 ? asyncPool[poolIndex] : Buffer.allocUnsafe(maxBytes);
200
- try {
201
- return await op(buffer.subarray(0, maxBytes));
202
- } finally {
203
- releaseAsyncPoolIndex(poolIndex);
204
- }
167
+ function allocateWindow(length) {
168
+ const buffer = Buffer.allocUnsafe(length);
169
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
205
170
  }
206
171
  /**
207
172
  * Like {@link peekFileSync} but uses async I/O.
@@ -210,16 +175,15 @@ async function peekFile(filePath, maxBytes, op) {
210
175
  if (maxBytes <= 0) return op(EMPTY_BUFFER);
211
176
  const fileHandle = await fs.promises.open(filePath, "r");
212
177
  try {
213
- return await withAsyncPoolBuffer(maxBytes, async (buffer) => {
214
- const { bytesRead } = await fileHandle.read(buffer, 0, buffer.byteLength, 0);
215
- return op(buffer.subarray(0, bytesRead));
216
- });
178
+ const buffer = allocateWindow(maxBytes);
179
+ const { bytesRead } = await fileHandle.read(buffer, 0, buffer.byteLength, 0);
180
+ return op(buffer.subarray(0, bytesRead));
217
181
  } finally {
218
182
  await fileHandle.close();
219
183
  }
220
184
  }
221
185
  //#endregion
222
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/binary.ts
186
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/binary.ts
223
187
  /**
224
188
  * Content-based binary/text classification for files that are about to be
225
189
  * decoded as UTF-8 text and shown to a model or user.
@@ -266,11 +230,11 @@ function isProbablyBinary(filePath, maxBytes = BINARY_SNIFF_BYTES) {
266
230
  return peekFile(filePath, maxBytes, isProbablyBinaryHeader);
267
231
  }
268
232
  //#endregion
269
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/package.json
270
- var version = "18.1.17";
233
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/package.json
234
+ var version = "18.12.6";
271
235
  var engines = { "bun": ">=1.3.14" };
272
236
  //#endregion
273
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/fs-error.ts
237
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/fs-error.ts
274
238
  function isFsError(err) {
275
239
  return err instanceof Error && "code" in err && typeof err.code === "string";
276
240
  }
@@ -283,6 +247,16 @@ function isEnotdir(err) {
283
247
  function hasFsCode(err, code) {
284
248
  return isFsError(err) && err.code === code;
285
249
  }
250
+ //#endregion
251
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/test-runtime.ts
252
+ const BUN_TEST_ENTRY_PATTERN = /[._](?:test|spec)\.[cm]?[jt]sx?$/;
253
+ /** True when the process is an explicitly marked test child or Bun is running a test entrypoint. */
254
+ function isBunTestRuntime() {
255
+ if (Bun.env.PI_TEST_RUNTIME === "1") return true;
256
+ return (Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test") && BUN_TEST_ENTRY_PATTERN.test(Bun.main);
257
+ }
258
+ /** Public homepage that inference gateways (OpenRouter, Vercel AI Gateway) credit omp traffic to. */
259
+ const APP_URL = "https://omp.sh/";
286
260
  /** Version (e.g. "1.0.0") */
287
261
  const VERSION = version;
288
262
  /** Default User-Agent header string (e.g. "omp/17.2.12") */
@@ -341,6 +315,7 @@ function readProfileFromEnvSafe() {
341
315
  return;
342
316
  }
343
317
  }
318
+ /** Profile-independent config root (~/.omp), shared by every omp profile. */
344
319
  function getBaseConfigRoot() {
345
320
  return path$1.join(os.homedir(), getConfigDirName());
346
321
  }
@@ -373,26 +348,34 @@ function standardizeMacOSPath(p) {
373
348
  } catch {}
374
349
  return p;
375
350
  }
351
+ /** Keep the current directory's spelling while expanding Windows 8.3 aliases. */
352
+ function standardizeProjectPath(p) {
353
+ return process.platform === "win32" ? expandWindowsLongPath(p) : standardizeMacOSPath(p);
354
+ }
376
355
  let projectDir;
377
356
  /** Get the project directory. */
378
357
  function getProjectDir() {
379
- if (projectDir === void 0) try {
380
- projectDir = standardizeMacOSPath(process.cwd());
381
- } catch {
382
- const candidates = [
383
- process.env.PWD,
384
- os.homedir(),
385
- os.tmpdir()
386
- ];
387
- for (const candidate of candidates) {
388
- if (!candidate || !path$1.isAbsolute(candidate)) continue;
389
- try {
390
- process.chdir(candidate);
391
- projectDir = standardizeMacOSPath(candidate);
392
- break;
393
- } catch {}
358
+ if (projectDir === void 0) {
359
+ let cwd;
360
+ try {
361
+ cwd = process.cwd();
362
+ } catch {
363
+ const candidates = [
364
+ process.env.PWD,
365
+ os.homedir(),
366
+ os.tmpdir()
367
+ ];
368
+ for (const candidate of candidates) {
369
+ if (!candidate || !path$1.isAbsolute(candidate)) continue;
370
+ try {
371
+ process.chdir(candidate);
372
+ cwd = candidate;
373
+ break;
374
+ } catch {}
375
+ }
376
+ if (cwd === void 0) throw new Error("Unable to determine an accessible working directory");
394
377
  }
395
- if (projectDir === void 0) throw new Error("Unable to determine an accessible working directory");
378
+ projectDir = standardizeProjectPath(cwd);
396
379
  }
397
380
  return projectDir;
398
381
  }
@@ -411,6 +394,7 @@ var DirResolver = class {
411
394
  agentDir;
412
395
  #rootDirs;
413
396
  #agentDirs;
397
+ #baseRootDirs;
414
398
  #rootCache = /* @__PURE__ */ new Map();
415
399
  #agentCache = /* @__PURE__ */ new Map();
416
400
  constructor(options = {}) {
@@ -423,7 +407,8 @@ var DirResolver = class {
423
407
  let xdgData;
424
408
  let xdgState;
425
409
  let xdgCache;
426
- if ((process.platform === "linux" || process.platform === "darwin") && isDefault) {
410
+ const xdgPlatform = process.platform === "linux" || process.platform === "darwin";
411
+ if (xdgPlatform && isDefault) {
427
412
  const resolveIf = (envVar) => {
428
413
  const value = process.env[envVar];
429
414
  if (!value) return void 0;
@@ -434,13 +419,22 @@ var DirResolver = class {
434
419
  if (fs.existsSync(profilePath)) return profilePath;
435
420
  return;
436
421
  }
437
- if (fs.existsSync(appRoot)) return appRoot;
422
+ return fs.existsSync(appRoot) ? appRoot : void 0;
438
423
  } catch {}
439
424
  };
440
425
  xdgData = resolveIf("XDG_DATA_HOME");
441
426
  xdgState = resolveIf("XDG_STATE_HOME");
442
427
  xdgCache = resolveIf("XDG_CACHE_HOME");
443
428
  }
429
+ const resolveBase = (envVar) => {
430
+ if (!xdgPlatform) return void 0;
431
+ const value = process.env[envVar];
432
+ if (!value) return void 0;
433
+ try {
434
+ const appRoot = path$1.join(value, "omp");
435
+ return fs.existsSync(appRoot) ? appRoot : void 0;
436
+ } catch {}
437
+ };
444
438
  this.#rootDirs = {
445
439
  data: xdgData ?? this.configRoot,
446
440
  state: xdgState ?? this.configRoot,
@@ -451,6 +445,16 @@ var DirResolver = class {
451
445
  state: xdgState ?? this.agentDir,
452
446
  cache: xdgCache ?? this.agentDir
453
447
  };
448
+ const baseRoot = getBaseConfigRoot();
449
+ this.#baseRootDirs = {
450
+ data: resolveBase("XDG_DATA_HOME") ?? baseRoot,
451
+ state: resolveBase("XDG_STATE_HOME") ?? baseRoot,
452
+ cache: resolveBase("XDG_CACHE_HOME") ?? baseRoot
453
+ };
454
+ }
455
+ /** Profile-independent config-root subdirectory, with optional XDG override. Shared across profiles. */
456
+ baseRootSubdir(subdir, xdg) {
457
+ return path$1.join(xdg ? this.#baseRootDirs[xdg] : getBaseConfigRoot(), subdir);
454
458
  }
455
459
  /** Config-root subdirectory, with optional XDG override. */
456
460
  rootSubdir(subdir, xdg) {
@@ -528,10 +532,30 @@ function getConfigRootDir() {
528
532
  function getAgentDir() {
529
533
  return dirs.agentDir;
530
534
  }
531
- /** Get the logs directory (~/.omp/logs). */
535
+ /**
536
+ * Get the logs directory (~/.omp/logs). `bun test` processes get
537
+ * `<tmpdir>/omp-test-logs-<uid>` instead, so suites never write into the
538
+ * developer's real logs; the uid suffix (as for the test session-owners dir)
539
+ * keeps users of a shared `/tmp` from owning each other's directory.
540
+ */
532
541
  function getLogsDir() {
542
+ if (isBunTestRuntime()) {
543
+ const uid = typeof process.getuid === "function" ? process.getuid() : os.userInfo().username;
544
+ return path$1.join(os.tmpdir(), `omp-test-logs-${uid}`);
545
+ }
533
546
  return dirs.rootSubdir("logs", "state");
534
547
  }
548
+ /**
549
+ * Local-timezone `YYYY-MM-DD` day key (zero-padded), formatted exactly like
550
+ * the rotating log sink's file naming: log files are named `omp.<day>.<pid>.log`
551
+ * with the LOCAL day, not the UTC day `toISOString()` yields. Anything that
552
+ * computes "today's" log path or matches same-day log files by name must use
553
+ * this key, or between local midnight and UTC midnight it points at files that
554
+ * do not exist (e.g. 00:00–08:00 in UTC+8).
555
+ */
556
+ function localDay(date) {
557
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
558
+ }
535
559
  /** Get the remote mount directory (~/.omp/remote). */
536
560
  function getRemoteDir() {
537
561
  return dirs.rootSubdir("remote", "data");
@@ -552,6 +576,10 @@ function getAutoQaDbPath() {
552
576
  function getDocumentConversionCacheDir(agentDir) {
553
577
  return dirs.agentSubdir(agentDir, path$1.join("cache", "document-conversions"), "cache");
554
578
  }
579
+ /** Get the custom themes directory (~/.omp/agent/themes). */
580
+ function getCustomThemesDir(agentDir) {
581
+ return dirs.agentSubdir(agentDir, "themes");
582
+ }
555
583
  /** Get the tools directory (~/.omp/agent/tools). */
556
584
  function getToolsDir(agentDir) {
557
585
  return dirs.agentSubdir(agentDir, "tools");
@@ -614,7 +642,38 @@ function getInstallId() {
614
642
  return next;
615
643
  }
616
644
  //#endregion
617
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/env.ts
645
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/path.ts
646
+ const WINDOWS_DRIVE_EXTENDED_PREFIX = /^\\\\[?]\\([A-Za-z]:[\\/].*)$/;
647
+ const WINDOWS_UNC_EXTENDED_PREFIX = /^\\\\[?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
648
+ const WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/([A-Za-z]:\/.*)$/;
649
+ const WINDOWS_UNC_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/UNC\/([^/]+)\/(.+)$/i;
650
+ const WINDOWS_DRIVE_NT_PREFIX = /^\\\\[?][?]\\([A-Za-z]:[\\/].*)$/;
651
+ const WINDOWS_UNC_NT_PREFIX = /^\\\\[?][?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
652
+ /** Removes Win32 extended-length prefixes before passing paths to Bun APIs. */
653
+ function stripWindowsExtendedLengthPathPrefix(filePath, platform = process.platform) {
654
+ if (platform !== "win32") return filePath;
655
+ const uncMatch = WINDOWS_UNC_EXTENDED_PREFIX.exec(filePath) ?? WINDOWS_UNC_NT_PREFIX.exec(filePath);
656
+ if (uncMatch) return `\\\\${uncMatch[1]}\\${uncMatch[2]}`;
657
+ const driveMatch = WINDOWS_DRIVE_EXTENDED_PREFIX.exec(filePath) ?? WINDOWS_DRIVE_NT_PREFIX.exec(filePath);
658
+ if (driveMatch) return driveMatch[1];
659
+ const forwardUncMatch = WINDOWS_UNC_EXTENDED_FORWARD_PREFIX.exec(filePath);
660
+ if (forwardUncMatch) return `//${forwardUncMatch[1]}/${forwardUncMatch[2]}`;
661
+ const forwardDriveMatch = WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX.exec(filePath);
662
+ if (forwardDriveMatch) return forwardDriveMatch[1];
663
+ return filePath;
664
+ }
665
+ /**
666
+ * Test whether a path is fully qualified and drive-independent.
667
+ * On Windows, requires a drive letter with separator (e.g. `C:\`) or UNC (`\\server\share` or `//server/share`).
668
+ * On POSIX, requires an absolute path.
669
+ */
670
+ function isFullyQualifiedPath(filePath, platform = process.platform) {
671
+ if (!(platform === "win32" ? path$1.win32 : path$1.posix).isAbsolute(filePath)) return false;
672
+ if (platform === "win32") return /^[a-zA-Z]:[/\\]/.test(filePath) || /^[\\/]{2}[^\\/]/.test(filePath);
673
+ return true;
674
+ }
675
+ //#endregion
676
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/env.ts
618
677
  const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
619
678
  /**
620
679
  * Strict shell-identifier shape. Used for dotenv keys we accept into
@@ -672,49 +731,17 @@ function readLaunchEnv() {
672
731
  readLaunchEnv();
673
732
  const projectEnvNamesLoadedByOmp = /* @__PURE__ */ new Set();
674
733
  /**
675
- * Parse one dotenv line with Bun-compatible semantics: an optional `export`
676
- * prefix, full-line `#` comments, inline `#` comments after whitespace on
677
- * unquoted values, and single/double/backtick quoting (a `#` inside quotes
678
- * stays literal). Returns undefined for blank lines, comments, and malformed
679
- * names.
680
- */
681
- function parseEnvLine(line) {
682
- const trimmed = line.trim();
683
- if (!trimmed || trimmed.startsWith("#")) return void 0;
684
- const eqIndex = trimmed.indexOf("=");
685
- if (eqIndex === -1) return void 0;
686
- let key = trimmed.slice(0, eqIndex).trim();
687
- const exported = key.match(/^export[ \t]+(.*)$/);
688
- if (exported) key = exported[1].trim();
689
- if (!isValidEnvName(key)) return void 0;
690
- const raw = trimmed.slice(eqIndex + 1).replace(/^[ \t]+/, "");
691
- const quote = raw[0];
692
- if (quote === "\"" || quote === "'" || quote === "`") {
693
- let close = raw.indexOf(quote, 1);
694
- while (close !== -1 && raw[close - 1] === "\\") close = raw.indexOf(quote, close + 1);
695
- return {
696
- key,
697
- value: close === -1 ? raw.slice(1) : raw.slice(1, close)
698
- };
699
- }
700
- const commentIndex = raw.search(/[ \t]#/);
701
- return {
702
- key,
703
- value: (commentIndex === -1 ? raw : raw.slice(0, commentIndex)).trimEnd()
704
- };
705
- }
706
- /**
707
- * Parses a .env file synchronously into key-value string pairs using
708
- * {@link parseEnvLine} for Bun-compatible line semantics, then mirrors valid
734
+ * Parses a complete .env file with the runtime's dotenv grammar, then retains
735
+ * only shell-identifier names and spawn-safe values before mirroring valid
709
736
  * `OMP_` variables to their `PI_` aliases.
710
737
  */
711
738
  function parseEnvFile(filePath) {
712
739
  const result = {};
713
740
  try {
714
- const content = fs.readFileSync(filePath, "utf-8");
715
- for (const line of content.split("\n")) {
716
- const parsed = parseEnvLine(line);
717
- if (parsed && isSafeEnvValue(parsed.value)) result[parsed.key] = parsed.value;
741
+ const parsed = parseEnv(fs.readFileSync(filePath, "utf-8"));
742
+ for (const key in parsed) {
743
+ const value = parsed[key];
744
+ if (value !== void 0 && isValidEnvName(key) && isSafeEnvValue(value)) result[key] = value;
718
745
  }
719
746
  } catch {}
720
747
  for (const k in result) if (k.startsWith("OMP_")) result[`PI_${k.slice(4)}`] = result[k];
@@ -768,12 +795,6 @@ function $envpos(name, defaultValue) {
768
795
  if (Number.isNaN(parsed) || parsed <= 0) return defaultValue;
769
796
  return parsed;
770
797
  }
771
- const BUN_TEST_ENTRY_PATTERN = /[._](?:test|spec)\.[cm]?[jt]sx?$/;
772
- /** True when the process is an explicitly marked test child or Bun is running a test entrypoint. */
773
- function isBunTestRuntime() {
774
- if (Bun.env.PI_TEST_RUNTIME === "1") return true;
775
- return (Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test") && BUN_TEST_ENTRY_PATTERN.test(Bun.main);
776
- }
777
798
  let terminalHeadless = isBunTestRuntime();
778
799
  /**
779
800
  * True when real-terminal side effects must be suppressed: stdout escape/frame
@@ -1550,11 +1571,6 @@ function installBunShim() {
1550
1571
  }
1551
1572
  //#endregion
1552
1573
  //#region src/tools/shared/bun-named-shim.ts
1553
- /**
1554
- * DSH named-import shim for `import { ... } from "bun"` (pi-utils and other
1555
- * always-bundled OMP packages). tsdown aliases "bun" to this module.
1556
- * Runtime Bun global is installed separately by src/tools/shared/bun-shim.ts.
1557
- */
1558
1574
  /** Bun.YAML (pi-utils frontmatter) — yaml package under the hood. */
1559
1575
  const YAML = {
1560
1576
  parse: (s) => parse(s),
@@ -1638,64 +1654,163 @@ var CookieMap = class {
1638
1654
  }
1639
1655
  };
1640
1656
  //#endregion
1641
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/logger/rotating-file.ts
1642
- /** Behavior-compatible reimplementation of winston-daily-rotate-file's used surface. */
1643
- function isAuditEntry(value) {
1644
- if (value === null || typeof value !== "object") return false;
1645
- const entry = value;
1646
- return typeof entry.date === "number" && typeof entry.name === "string" && typeof entry.hash === "string";
1647
- }
1648
- /** Synchronous append sink with local-day and size rotation plus bounded retention. */
1657
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/fs-open.ts
1658
+ /** Close-on-exec file opens for descriptors held past a child spawn. */
1659
+ /**
1660
+ * `O_CLOEXEC`, which neither Node nor Bun exposes in `fs.constants`.
1661
+ *
1662
+ * Kernel ABI values: `0o2000000` on Linux, `0x0100_0000` on Darwin. Windows has
1663
+ * no `exec` and its CRT handles are inherited only when a spawn explicitly asks
1664
+ * for it, so the bit is 0 there — as it is on any other platform, where adding
1665
+ * an unknown flag bit would be worse than leaving the descriptor inheritable.
1666
+ */
1667
+ const O_CLOEXEC = process.platform === "linux" ? 524288 : process.platform === "darwin" ? 16777216 : 0;
1668
+ /**
1669
+ * `fs.openSync` with close-on-exec, matching Node's default.
1670
+ *
1671
+ * libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
1672
+ * dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
1673
+ * `bun@>=1.4` pin), and descriptors it returns survive
1674
+ * into any child spawned through a path that does not close strays itself — the
1675
+ * bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
1676
+ * this for every descriptor kept open past such a spawn (session transcripts,
1677
+ * logs, spools); a child must never hold a writable handle to them (#13224).
1678
+ *
1679
+ * Flags are numeric only: the bit cannot be OR-ed into `"a"`/`"w+"` strings.
1680
+ *
1681
+ * @example
1682
+ * const fd = openCloexecSync(logPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
1683
+ */
1684
+ function openCloexecSync(filePath, flags, mode) {
1685
+ return fs.openSync(filePath, flags | O_CLOEXEC, mode);
1686
+ }
1687
+ //#endregion
1688
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/logger/rotating-file.ts
1689
+ /** Process-local rotating file sink: local-day and size rotation with bounded retention. */
1690
+ /**
1691
+ * Synchronous append sink with local-day and size rotation plus bounded retention.
1692
+ *
1693
+ * Construction performs no filesystem I/O: the first file is created when the
1694
+ * first record is written out, so a process that never logs leaves nothing on
1695
+ * disk. Every file name embeds the owning process's suffix (its PID), so the
1696
+ * set of files this sink created is tracked in memory; files left by earlier
1697
+ * processes are pruned by name pattern in the logger, not by this sink.
1698
+ *
1699
+ * With {@link RotatingFileOptions.batch}, records are buffered and written in
1700
+ * one `write` per batch. Buffered bytes already count toward the active file's
1701
+ * size, and the buffer is written to its file before any rotation, so file
1702
+ * selection matches an unbatched sink record for record.
1703
+ */
1649
1704
  var RotatingFileSink = class {
1650
1705
  #directory;
1651
1706
  #filenamePrefix;
1652
1707
  #filenameSuffix;
1653
- #auditFile;
1654
1708
  #maxBytes;
1655
1709
  #maxFiles;
1656
- #files;
1710
+ #batch;
1711
+ #onRotate;
1712
+ /** Files this sink created, oldest first. */
1713
+ #files = [];
1657
1714
  #activeDay;
1658
1715
  #activeIndex = 0;
1659
1716
  #activePath;
1717
+ /** Size of the active file including buffered records not yet written. */
1660
1718
  #activeBytes = 0;
1661
1719
  #closed = false;
1720
+ #fd;
1721
+ /** Records for the active file not yet written, each ending in EOL. */
1722
+ #pending = "";
1723
+ #pendingBytes = 0;
1724
+ #flushTimer;
1662
1725
  constructor(options) {
1663
1726
  this.#directory = options.directory;
1664
1727
  this.#filenamePrefix = options.filenamePrefix;
1665
1728
  this.#filenameSuffix = options.filenameSuffix;
1666
- this.#auditFile = options.auditFile;
1667
1729
  this.#maxBytes = options.maxBytes;
1668
1730
  this.#maxFiles = options.maxFiles;
1669
- this.#files = this.#readAudit();
1670
- const now = /* @__PURE__ */ new Date();
1671
- this.#selectFile(this.#localDay(now));
1672
- const activePath = this.#activePath;
1673
- if (activePath) {
1674
- this.#registerFile(activePath, now.getTime());
1675
- fs.closeSync(fs.openSync(activePath, "a"));
1731
+ this.#batch = options.batch;
1732
+ this.#onRotate = options.onRotate;
1733
+ }
1734
+ #openFd(filePath) {
1735
+ this.#closeFd();
1736
+ this.#fd = openCloexecSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
1737
+ try {
1738
+ this.#onRotate?.(filePath);
1739
+ } catch {}
1740
+ }
1741
+ #closeFd() {
1742
+ if (this.#fd !== void 0) {
1743
+ try {
1744
+ fs.closeSync(this.#fd);
1745
+ } catch {}
1746
+ this.#fd = void 0;
1676
1747
  }
1677
1748
  }
1678
- /** Append one already-formatted log record. */
1679
- write(line) {
1749
+ /**
1750
+ * Append one already-formatted log record. A batching sink buffers it unless
1751
+ * `flushNow` is set or the buffer reached its size threshold; either way the
1752
+ * whole buffer, this record included, is then written in order. `day` is the
1753
+ * record's `localDay` key, so callers that already took the record's `Date`
1754
+ * reuse it.
1755
+ */
1756
+ write(line, flushNow = false, day = localDay(/* @__PURE__ */ new Date())) {
1680
1757
  if (this.#closed) return;
1681
- const now = /* @__PURE__ */ new Date();
1682
- this.#selectFile(this.#localDay(now));
1683
- const activePath = this.#activePath;
1684
- if (!activePath) return;
1685
- this.#registerFile(activePath, now.getTime());
1758
+ this.#selectFile(day);
1686
1759
  const record = `${line}${os.EOL}`;
1687
- fs.appendFileSync(activePath, record, "utf8");
1688
- this.#activeBytes += Buffer.byteLength(record);
1760
+ const bytes = Buffer.byteLength(record, "utf8");
1761
+ this.#pending += record;
1762
+ this.#pendingBytes += bytes;
1763
+ this.#activeBytes += bytes;
1764
+ const batch = this.#batch;
1765
+ if (flushNow || !batch || this.#pendingBytes >= batch.maxBytes) this.flush();
1766
+ else if (this.#flushTimer === void 0) {
1767
+ this.#flushTimer = setTimeout(this.#flushFromTimer, batch.intervalMs);
1768
+ this.#flushTimer.unref();
1769
+ }
1770
+ }
1771
+ /** Write every buffered record to the active file now. */
1772
+ flush() {
1773
+ if (this.#flushTimer !== void 0) {
1774
+ clearTimeout(this.#flushTimer);
1775
+ this.#flushTimer = void 0;
1776
+ }
1777
+ const activePath = this.#activePath;
1778
+ if (this.#pendingBytes === 0 || !activePath) return;
1779
+ const buf = Buffer.from(this.#pending, "utf8");
1780
+ this.#pending = "";
1781
+ this.#pendingBytes = 0;
1782
+ if (this.#fd === void 0) {
1783
+ this.#registerFile(activePath);
1784
+ this.#openFd(activePath);
1785
+ }
1786
+ let off = 0;
1787
+ while (off < buf.length) {
1788
+ const written = fs.writeSync(this.#fd, buf, off);
1789
+ if (written <= 0) break;
1790
+ off += written;
1791
+ }
1689
1792
  }
1690
- /** Stop accepting records. Synchronous writes require no drain phase. */
1793
+ #flushFromTimer = () => {
1794
+ this.#flushTimer = void 0;
1795
+ try {
1796
+ this.flush();
1797
+ } catch {}
1798
+ };
1799
+ /** Write buffered records, then stop accepting records. */
1691
1800
  close() {
1801
+ if (this.#closed) return;
1802
+ try {
1803
+ this.flush();
1804
+ } catch {}
1692
1805
  this.#closed = true;
1693
- }
1694
- #localDay(date) {
1695
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
1806
+ this.#closeFd();
1696
1807
  }
1697
1808
  #selectFile(day) {
1698
- if (day !== this.#activeDay) {
1809
+ const dayChanged = day !== this.#activeDay;
1810
+ if (!dayChanged && this.#activeBytes <= this.#maxBytes) return;
1811
+ this.flush();
1812
+ this.#closeFd();
1813
+ if (dayChanged) {
1699
1814
  this.#activeDay = day;
1700
1815
  this.#activeIndex = 0;
1701
1816
  this.#setActivePath(day, 0);
@@ -1714,46 +1829,120 @@ var RotatingFileSink = class {
1714
1829
  this.#activeBytes = 0;
1715
1830
  }
1716
1831
  }
1717
- #registerFile(filePath, date) {
1718
- if (this.#files.some((file) => file.name === filePath)) return;
1719
- const hash = crypto$1.createHash("sha256").update(`${filePath}LOG_FILE${date}`).digest("hex");
1720
- this.#files.push({
1721
- date,
1722
- name: filePath,
1723
- hash
1724
- });
1832
+ #registerFile(filePath) {
1833
+ if (this.#files.includes(filePath)) return;
1834
+ this.#files.push(filePath);
1725
1835
  while (this.#files.length > this.#maxFiles) {
1726
1836
  const removed = this.#files.shift();
1727
1837
  if (!removed) break;
1728
1838
  try {
1729
- fs.rmSync(removed.name, { force: true });
1839
+ fs.rmSync(removed, { force: true });
1730
1840
  } catch {}
1731
1841
  }
1732
- this.#writeAudit();
1733
- }
1734
- #readAudit() {
1735
- try {
1736
- const parsed = JSON.parse(fs.readFileSync(this.#auditFile, "utf8"));
1737
- return Array.isArray(parsed.files) ? parsed.files.filter(isAuditEntry) : [];
1738
- } catch {
1739
- return [];
1740
- }
1741
1842
  }
1742
- #writeAudit() {
1743
- const state = {
1744
- keep: {
1745
- days: false,
1746
- amount: this.#maxFiles
1843
+ };
1844
+ //#endregion
1845
+ //#region src/tools/shared/bun-ffi-shim.ts
1846
+ /**
1847
+ * DSH named-import shim for `bun:ffi` (pi-utils stderr-guard / process-name).
1848
+ * Both consumers degrade silently when dlopen is unavailable ("Never throws:
1849
+ * bun:ffi unavailability ... degrades silently"), so this shim returns null.
1850
+ */
1851
+ const FFIType = {};
1852
+ function dlopen() {
1853
+ return null;
1854
+ }
1855
+ const ptr = () => 0;
1856
+ //#endregion
1857
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/stderr-guard.ts
1858
+ /**
1859
+ * Terminal stderr guard: keeps unmanaged fd-2 writes off the terminal while a
1860
+ * TUI owns the viewport.
1861
+ *
1862
+ * Native runtimes and libraries can write directly to file descriptor 2 at
1863
+ * arbitrary times. Examples include macOS libmalloc diagnostics and Bun HTTP
1864
+ * decompression diagnostics on Linux. Those bytes bypass the renderer and
1865
+ * paint straight into the viewport.
1866
+ *
1867
+ * While the TUI owns the terminal, dup fd 2 aside and dup2 a redirect target
1868
+ * over it; restore the saved fd whenever terminal ownership is released
1869
+ * (external editor, Ctrl+Z suspend, shutdown, crash restore). This mirrors
1870
+ * openai/codex#24459, but redirects to the omp log file instead of /dev/null
1871
+ * so diagnostics stay greppable and Bun native-crash reports (which abort
1872
+ * before any JS cleanup can restore fd 2) are preserved.
1873
+ *
1874
+ * Only dup/dup2 go through bun:ffi. fcntl is deliberately avoided: it is
1875
+ * variadic, and the arm64-darwin ABI passes variadic arguments on the stack,
1876
+ * so a fixed-arity FFI signature would read garbage for the third argument.
1877
+ */
1878
+ const STDERR_FILENO = 2;
1879
+ let libcFdOpsCache;
1880
+ function libcFdOps() {
1881
+ if (libcFdOpsCache !== void 0) return libcFdOpsCache;
1882
+ libcFdOpsCache = null;
1883
+ if (process.platform === "win32") return null;
1884
+ const candidates = process.platform === "darwin" ? ["libSystem.B.dylib", "/usr/lib/libSystem.B.dylib"] : ["libc.so.6", "libc.so"];
1885
+ for (const candidate of candidates) try {
1886
+ libcFdOpsCache = dlopen(candidate, {
1887
+ dup: {
1888
+ args: [FFIType.i32],
1889
+ returns: FFIType.i32
1747
1890
  },
1748
- auditLog: this.#auditFile,
1749
- files: this.#files,
1750
- hashType: "sha256"
1751
- };
1752
- fs.writeFileSync(this.#auditFile, JSON.stringify(state, void 0, 4), "utf8");
1891
+ dup2: {
1892
+ args: [FFIType.i32, FFIType.i32],
1893
+ returns: FFIType.i32
1894
+ }
1895
+ }).symbols;
1896
+ return libcFdOpsCache;
1897
+ } catch {}
1898
+ return libcFdOpsCache;
1899
+ }
1900
+ /** Saved dup of the real stderr while suppression is active, else null. */
1901
+ let savedStderrFd = null;
1902
+ /**
1903
+ * Point fd 2 at `logPath`, the log file the rotating sink is currently
1904
+ * writing.
1905
+ *
1906
+ * The logger calls this on every sink rotation. Without it fd 2 keeps the
1907
+ * descriptor opened at TUI start, so after local midnight raw stderr — this
1908
+ * process's and every child that inherited fd 2 — keeps landing in the
1909
+ * previous day's file, which the sink eventually prunes out from under it.
1910
+ *
1911
+ * The path is recorded even while suppression is inactive, so a later
1912
+ * {@link suppressTerminalStderr} redirects to the live file instead of
1913
+ * re-deriving a dated name. Best-effort: if the new file cannot be opened,
1914
+ * fd 2 keeps its current target.
1915
+ */
1916
+ function setStderrRedirectTarget(logPath) {
1917
+ if (savedStderrFd === null) return;
1918
+ const libc = libcFdOps();
1919
+ if (!libc) return;
1920
+ let redirectFd;
1921
+ try {
1922
+ redirectFd = fs.openSync(logPath, "a");
1923
+ } catch {
1924
+ return;
1753
1925
  }
1754
- };
1926
+ libc.dup2(redirectFd, STDERR_FILENO);
1927
+ fs.closeSync(redirectFd);
1928
+ }
1929
+ /**
1930
+ * Re-point fd 2 at the saved terminal stderr. Safe to call unconditionally:
1931
+ * no-op when suppression is not active. Called at every terminal-ownership
1932
+ * release and by the postmortem fatal handlers before they print, so crash
1933
+ * reports reach the real terminal.
1934
+ */
1935
+ function restoreTerminalStderr() {
1936
+ if (savedStderrFd === null) return;
1937
+ const saved = savedStderrFd;
1938
+ savedStderrFd = null;
1939
+ libcFdOps()?.dup2(saved, STDERR_FILENO);
1940
+ try {
1941
+ fs.closeSync(saved);
1942
+ } catch {}
1943
+ }
1755
1944
  //#endregion
1756
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/logger.ts
1945
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/logger.ts
1757
1946
  /**
1758
1947
  * Centralized logger for omp.
1759
1948
  *
@@ -1763,7 +1952,11 @@ var RotatingFileSink = class {
1763
1952
  * transport so a process supervisor (pm2, journald, k8s) captures the logs.
1764
1953
  *
1765
1954
  * Each entry includes `process.pid` so concurrent omp instances stay
1766
- * traceable.
1955
+ * traceable. The file is created on the first record written. Records are
1956
+ * batched — one write per second or per 64 KiB — while `warn`/`error` records
1957
+ * are written at once together with everything buffered before them; exit,
1958
+ * fatal, and signal paths flush the rest. `OMP_LOG_LEVEL` optionally limits
1959
+ * the levels written to the file (default: all).
1767
1960
  */
1768
1961
  const logSinks = /* @__PURE__ */ new Set();
1769
1962
  function emitToSinks(level, message, context) {
@@ -1779,7 +1972,12 @@ function emitToSinks(level, message, context) {
1779
1972
  } catch {}
1780
1973
  }
1781
1974
  const PROCESS_LOG_PATTERN = /^omp\.(\d{4}-\d{2}-\d{2})\.(\d+)\.log(?:\.(\d+))?$/;
1975
+ /** Per-process audit files written by earlier releases; current sinks track rotations in memory. */
1782
1976
  const PROCESS_AUDIT_PATTERN = /^\.omp\.(\d+)-audit\.json$/;
1977
+ /** Shared daily logs (plain, size-rolled, or gzipped) written by the winston-era logger. */
1978
+ const LEGACY_LOG_PATTERN = /^omp\.(\d{4}-\d{2}-\d{2})\.log(?:\.\d+)?(?:\.gz)?$/;
1979
+ /** Hash-named audit files written by winston-daily-rotate-file. */
1980
+ const LEGACY_AUDIT_PATTERN = /^\.[0-9a-f]{8,}-audit\.json$/;
1783
1981
  const RETAINED_STALE_LOGS_PER_PROCESS_DAY = 1;
1784
1982
  function processIsRunning(pid) {
1785
1983
  try {
@@ -1791,9 +1989,11 @@ function processIsRunning(pid) {
1791
1989
  }
1792
1990
  /**
1793
1991
  * Retain one newest completed-process log per process/day within the current
1794
- * and previous four local calendar days, and remove one-use audit files. Live
1795
- * PID namespaces are never touched. The calendar-day boundary preserves daily
1796
- * diagnostic coverage while bounding completed-process storage and scans.
1992
+ * and previous four local calendar days, remove completed-process audit files
1993
+ * left by earlier releases, and age out legacy shared daily logs and their
1994
+ * hash-named audits past the same window. Live PID namespaces are never
1995
+ * touched. The calendar-day boundary preserves daily diagnostic coverage while
1996
+ * bounding completed-process storage and scans.
1797
1997
  */
1798
1998
  function pruneStaleProcessLogs(dir) {
1799
1999
  let entries;
@@ -1803,29 +2003,38 @@ function pruneStaleProcessLogs(dir) {
1803
2003
  return;
1804
2004
  }
1805
2005
  const current = /* @__PURE__ */ new Date();
1806
- const currentDate = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-` + String(current.getDate()).padStart(2, "0");
2006
+ const currentDate = localDay(current);
1807
2007
  const cutoff = new Date(current);
1808
2008
  cutoff.setDate(cutoff.getDate() - 4);
1809
- const cutoffDate = `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-` + String(cutoff.getDate()).padStart(2, "0");
2009
+ const cutoffDate = localDay(cutoff);
2010
+ cutoff.setHours(0, 0, 0, 0);
2011
+ const cutoffMs = cutoff.getTime();
1810
2012
  const staleLogsByProcessDay = /* @__PURE__ */ new Map();
1811
2013
  for (const entry of entries) {
1812
2014
  if (!entry.isFile()) continue;
2015
+ const entryPath = path$1.join(dir, entry.name);
2016
+ const legacyMatch = LEGACY_LOG_PATTERN.exec(entry.name);
2017
+ if (legacyMatch?.[1]) {
2018
+ if (legacyMatch[1] < cutoffDate) removeBestEffort(entryPath);
2019
+ continue;
2020
+ }
2021
+ if (LEGACY_AUDIT_PATTERN.test(entry.name)) {
2022
+ try {
2023
+ if (fs.statSync(entryPath).mtimeMs < cutoffMs) removeBestEffort(entryPath);
2024
+ } catch {}
2025
+ continue;
2026
+ }
1813
2027
  const logMatch = PROCESS_LOG_PATTERN.exec(entry.name);
1814
2028
  const auditMatch = PROCESS_AUDIT_PATTERN.exec(entry.name);
1815
2029
  const pidText = logMatch?.[2] ?? auditMatch?.[1];
1816
2030
  if (!pidText || processIsRunning(Number(pidText))) continue;
1817
- const entryPath = path$1.join(dir, entry.name);
1818
2031
  if (auditMatch) {
1819
- try {
1820
- fs.rmSync(entryPath, { force: true });
1821
- } catch {}
2032
+ removeBestEffort(entryPath);
1822
2033
  continue;
1823
2034
  }
1824
2035
  if (!logMatch?.[1]) continue;
1825
2036
  if (logMatch[1] < cutoffDate || logMatch[1] > currentDate) {
1826
- try {
1827
- fs.rmSync(entryPath, { force: true });
1828
- } catch {}
2037
+ removeBestEffort(entryPath);
1829
2038
  continue;
1830
2039
  }
1831
2040
  const key = `${pidText}:${logMatch[1]}`;
@@ -1846,11 +2055,24 @@ function pruneStaleProcessLogs(dir) {
1846
2055
  });
1847
2056
  } catch {}
1848
2057
  ranked.sort((a, b) => b.mtimeMs - a.mtimeMs || b.rollover - a.rollover || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
1849
- for (const stale of ranked.slice(RETAINED_STALE_LOGS_PER_PROCESS_DAY)) try {
1850
- fs.rmSync(stale.path, { force: true });
1851
- } catch {}
2058
+ for (const stale of ranked.slice(RETAINED_STALE_LOGS_PER_PROCESS_DAY)) removeBestEffort(stale.path);
1852
2059
  }
1853
2060
  }
2061
+ function removeBestEffort(filePath) {
2062
+ try {
2063
+ fs.rmSync(filePath, { force: true });
2064
+ } catch {}
2065
+ }
2066
+ const scheduledPruneDirs = /* @__PURE__ */ new Set();
2067
+ /** Run shared retention only after logger construction has returned to the event loop. */
2068
+ function schedulePruneStaleProcessLogs(dir) {
2069
+ if (scheduledPruneDirs.has(dir)) return;
2070
+ scheduledPruneDirs.add(dir);
2071
+ setImmediate(() => {
2072
+ scheduledPruneDirs.delete(dir);
2073
+ pruneStaleProcessLogs(dir);
2074
+ }).unref();
2075
+ }
1854
2076
  /** Ensure a logs directory exists; return the resolved path. */
1855
2077
  function ensureDir(dir) {
1856
2078
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -1876,6 +2098,27 @@ function jsonReplacer(_key, value) {
1876
2098
  }
1877
2099
  return value;
1878
2100
  }
2101
+ const ERROR_SCAN_BUDGET = 128;
2102
+ /**
2103
+ * Whether `value` may contain an {@link Error} that {@link jsonReplacer} must
2104
+ * unwrap. Walks what `JSON.stringify` serializes (arrays and own enumerable
2105
+ * keys); objects with a `toJSON` other than `Date` may return an Error, and an
2106
+ * exhausted budget answers true.
2107
+ */
2108
+ function mayContainError(root) {
2109
+ const stack = [root];
2110
+ let budget = ERROR_SCAN_BUDGET;
2111
+ while (stack.length > 0) {
2112
+ const value = stack.pop();
2113
+ if (value === null || typeof value !== "object") continue;
2114
+ if (value instanceof Error) return true;
2115
+ if (--budget < 0) return true;
2116
+ if (value instanceof Date) continue;
2117
+ if ("toJSON" in value && typeof value.toJSON === "function") return true;
2118
+ for (const child of Object.values(value)) stack.push(child);
2119
+ }
2120
+ return false;
2121
+ }
1879
2122
  function padTimestampPart(value, width = 2) {
1880
2123
  return String(value).padStart(width, "0");
1881
2124
  }
@@ -1886,42 +2129,81 @@ function formatLocalTimestamp(date) {
1886
2129
  return `${padTimestampPart(date.getFullYear(), 4)}-${padTimestampPart(date.getMonth() + 1)}-${padTimestampPart(date.getDate())}T${padTimestampPart(date.getHours())}:${padTimestampPart(date.getMinutes())}:${padTimestampPart(date.getSeconds())}.${padTimestampPart(date.getMilliseconds(), 3)}${offsetSign}${padTimestampPart(Math.floor(absoluteOffset / 60))}:${padTimestampPart(absoluteOffset % 60)}`;
1887
2130
  }
1888
2131
  const FORMAT_TOKEN_PATTERN = /%[scdjifoO%]/;
1889
- function normalizeLogInfo(level, message, context) {
2132
+ /** Serialize one log record: timestamp, level, pid, message, then context fields. */
2133
+ function formatLogRecord(level, message, context, timestamp) {
1890
2134
  const metadata = !FORMAT_TOKEN_PATTERN.test(message) && context !== null && typeof context === "object" ? context : void 0;
1891
- const info = Object.assign({}, metadata, {
1892
- level,
1893
- message
1894
- });
1895
- if (metadata?.message) info.message = `${message} ${metadata.message}`;
1896
- if (metadata?.stack) info.stack = metadata.stack;
1897
- if (metadata?.cause) info.cause = metadata.cause;
1898
- return info;
1899
- }
1900
- function formatLogInfo(info) {
1901
- const timestamp = formatLocalTimestamp(/* @__PURE__ */ new Date());
1902
- info.timestamp = timestamp;
1903
2135
  const entry = {
1904
2136
  timestamp,
1905
- level: info.level,
2137
+ level,
1906
2138
  pid: process.pid,
1907
- message: info.message
2139
+ message: metadata?.message ? `${message} ${metadata.message}` : message
1908
2140
  };
1909
- for (const [key, value] of Object.entries(info)) if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = value;
1910
- return JSON.stringify(entry, jsonReplacer);
2141
+ if (metadata) {
2142
+ for (const key of Object.keys(metadata)) if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = metadata[key];
2143
+ if (metadata.stack) entry.stack = metadata.stack;
2144
+ if (metadata.cause) entry.cause = metadata.cause;
2145
+ }
2146
+ return mayContainError(entry) ? JSON.stringify(entry, jsonReplacer) : JSON.stringify(entry);
2147
+ }
2148
+ /** Longest a buffered file record waits before it is written. */
2149
+ const FILE_FLUSH_INTERVAL_MS = 1e3;
2150
+ /** Buffered bytes at which the file batch is written immediately. */
2151
+ const FILE_FLUSH_BYTES = 65536;
2152
+ let exitFlushInstalled = false;
2153
+ /** Set once the process is exiting: every later record is written immediately. */
2154
+ let exiting = false;
2155
+ function flushOnExit() {
2156
+ exiting = true;
2157
+ flush();
1911
2158
  }
1912
2159
  /** Build a rotating file sink with process-local rotation and shared retention. */
1913
2160
  function makeFileTransport(dir) {
1914
2161
  const logsDir = ensureDir(dir ?? getLogsDir());
1915
- pruneStaleProcessLogs(logsDir);
2162
+ schedulePruneStaleProcessLogs(logsDir);
2163
+ if (!exitFlushInstalled) {
2164
+ exitFlushInstalled = true;
2165
+ process.on("exit", flushOnExit);
2166
+ }
1916
2167
  return new RotatingFileSink({
1917
2168
  directory: logsDir,
1918
2169
  filenamePrefix: "omp",
1919
2170
  filenameSuffix: String(process.pid),
1920
2171
  maxBytes: 10485760,
1921
2172
  maxFiles: 5,
1922
- auditFile: path$1.join(logsDir, `.omp.${process.pid}-audit.json`)
2173
+ batch: {
2174
+ intervalMs: FILE_FLUSH_INTERVAL_MS,
2175
+ maxBytes: FILE_FLUSH_BYTES
2176
+ },
2177
+ onRotate: setStderrRedirectTarget
1923
2178
  });
1924
2179
  }
2180
+ const LOG_LEVEL_RANK = {
2181
+ error: 0,
2182
+ warn: 1,
2183
+ info: 2,
2184
+ debug: 3
2185
+ };
2186
+ /** `warn` and `error` records are written at once, with everything buffered before them. */
2187
+ const IMMEDIATE_FLUSH_RANK = LOG_LEVEL_RANK.warn;
2188
+ /**
2189
+ * Rank of the most verbose level the file transport persists: `OMP_LOG_LEVEL`
2190
+ * (error|warn|info|debug, case-insensitive), default `debug` (everything).
2191
+ */
2192
+ function resolveFileLevelRank() {
2193
+ const requested = process.env.OMP_LOG_LEVEL?.trim().toLowerCase();
2194
+ return requested && Object.hasOwn(LOG_LEVEL_RANK, requested) ? LOG_LEVEL_RANK[requested] : LOG_LEVEL_RANK.debug;
2195
+ }
2196
+ /**
2197
+ * Write buffered file records to disk now. The file transport batches records
2198
+ * (one write per second or per 64 KiB) and writes `warn`/`error` records
2199
+ * immediately; process exit flushes on its own. Call this before handing the
2200
+ * log file to something that reads it in-process. Never throws.
2201
+ */
2202
+ function flush() {
2203
+ try {
2204
+ activeTransports?.file?.flush();
2205
+ } catch {}
2206
+ }
1925
2207
  /**
1926
2208
  * Desired transport configuration, applied when local logging is initialized.
1927
2209
  * Default: file ON (TUI-safe), console OFF.
@@ -1932,7 +2214,8 @@ let activeTransports;
1932
2214
  function buildTransports(opts) {
1933
2215
  return {
1934
2216
  file: opts.file ? makeFileTransport(typeof opts.file === "string" ? opts.file : void 0) : void 0,
1935
- console: opts.console === true
2217
+ console: opts.console === true,
2218
+ fileLevelRank: resolveFileLevelRank()
1936
2219
  };
1937
2220
  }
1938
2221
  function getLocalTransports() {
@@ -1941,11 +2224,13 @@ function getLocalTransports() {
1941
2224
  }
1942
2225
  function emitLocally(level, message, context) {
1943
2226
  const transports = getLocalTransports();
1944
- const info = normalizeLogInfo(level, message, context);
1945
- if (!transports.file && !transports.console) return;
1946
- const line = formatLogInfo(info);
1947
- if (transports.file) transports.file.write(line);
1948
- if (transports.console) fs.writeSync(1, `${formatLogInfo(info)}${os.EOL}`);
2227
+ const rank = LOG_LEVEL_RANK[level];
2228
+ const file = rank <= transports.fileLevelRank ? transports.file : void 0;
2229
+ if (!file && !transports.console) return;
2230
+ const now = /* @__PURE__ */ new Date();
2231
+ const line = formatLogRecord(level, message, context, formatLocalTimestamp(now));
2232
+ file?.write(line, rank <= IMMEDIATE_FLUSH_RANK || exiting, localDay(now));
2233
+ if (transports.console) fs.writeSync(1, `${line}${os.EOL}`);
1949
2234
  }
1950
2235
  /**
1951
2236
  * Log an error message.
@@ -2022,9 +2307,12 @@ function time(op, fn, ...args) {
2022
2307
  }
2023
2308
  }
2024
2309
  //#endregion
2025
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/frontmatter.ts
2026
- function stripHtmlComments(content) {
2027
- return content.replace(/<!--[\s\S]*?-->/g, "");
2310
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/frontmatter.ts
2311
+ function stripLeadingHtmlComments(content) {
2312
+ const leading = /^(?:(?:\s*<!--[\s\S]*?-->)+)/.exec(content);
2313
+ if (!leading) return content;
2314
+ const rest = content.slice(leading[0].length);
2315
+ return /^\s*---/.test(rest) ? rest.replace(/^\s+/, "") : content;
2028
2316
  }
2029
2317
  /** Convert kebab-case to camelCase (e.g. "thinking-level" -> "thinkingLevel") */
2030
2318
  function kebabToCamel(key) {
@@ -2118,7 +2406,8 @@ function parseFrontmatter(content, options) {
2118
2406
  const loc = location ?? source;
2119
2407
  const frontmatter = { ...fallback };
2120
2408
  const newlineNormalized = normalize ? content.replace(/\r\n?/g, "\n") : content;
2121
- const normalized = normalize && repair ? stripHtmlComments(newlineNormalized) : newlineNormalized;
2409
+ const repairHtmlComments = normalize && repair;
2410
+ const normalized = repairHtmlComments ? stripLeadingHtmlComments(newlineNormalized) : newlineNormalized;
2122
2411
  if (!normalized.startsWith("---")) return {
2123
2412
  frontmatter,
2124
2413
  body: normalized
@@ -2128,7 +2417,8 @@ function parseFrontmatter(content, options) {
2128
2417
  frontmatter,
2129
2418
  body: normalized
2130
2419
  };
2131
- const metadata = normalized.slice(4, endIndex);
2420
+ const rawMetadata = normalized.slice(4, endIndex);
2421
+ const metadata = repairHtmlComments ? rawMetadata.replace(/<!--[\s\S]*?-->/g, "") : rawMetadata;
2132
2422
  const body = normalized.slice(endIndex + 4).trim();
2133
2423
  try {
2134
2424
  const loaded = parseYamlRecord(metadata, repair);
@@ -2173,6 +2463,15 @@ function parseFrontmatter(content, options) {
2173
2463
  }
2174
2464
  }
2175
2465
  const U$1 = 117;
2466
+ /**
2467
+ * Minimum remaining string length for bulk-skipping ordinary characters in
2468
+ * {@link JsonLexer.string} with two engine `indexOf` scans (nearest quote or
2469
+ * backslash) instead of one `charCodeAt` per character. Below this the
2470
+ * `indexOf` setup costs more than the per-char loop on quote-dense input;
2471
+ * long string runs — the multi-KB tool-argument payloads that dominate
2472
+ * streaming re-parses — scan several times faster above it.
2473
+ */
2474
+ const STRING_BULK_SCAN_MIN = 32;
2176
2475
  /** Valid chars after `\` in a strict JSON escape: `" \ / b f n r t u`. */
2177
2476
  const VALID_ESCAPE_CHAR = /* @__PURE__ */ new Uint8Array(128);
2178
2477
  for (const ch of "\"\\/bfnrtu") VALID_ESCAPE_CHAR[ch.charCodeAt(0)] = 1;
@@ -2270,6 +2569,17 @@ var JsonLexer = class {
2270
2569
  src;
2271
2570
  mode;
2272
2571
  pos;
2572
+ /**
2573
+ * Shared across {@link string} calls: `src` has no backslash in
2574
+ * `[#backslashFrom, #backslashAt)` and `#backslashAt` is a backslash or
2575
+ * `src.length`. An escape-free run can extend past its string's closing
2576
+ * quote to the end of the document, so searching per string would rescan
2577
+ * the same tail for every sibling string (quadratic on objects with many
2578
+ * short values). `src` is immutable, so the range never goes stale; a
2579
+ * cursor outside it (e.g. a rollback) re-searches.
2580
+ */
2581
+ #backslashFrom = 0;
2582
+ #backslashAt = -1;
2273
2583
  constructor(src, mode, pos = 0) {
2274
2584
  this.src = src;
2275
2585
  this.mode = mode;
@@ -2299,7 +2609,27 @@ var JsonLexer = class {
2299
2609
  let runStart = i;
2300
2610
  let unstableFrom = -1;
2301
2611
  const lenient = quote === 39 || this.mode === "streaming";
2612
+ const quoteChar = String.fromCharCode(quote);
2613
+ let nextQuote = quoteChar.charCodeAt(0) === quote ? -1 : n;
2614
+ let nextBackslash = -1;
2302
2615
  while (i < n) {
2616
+ const c0 = s.charCodeAt(i);
2617
+ if (c0 !== 92 && c0 !== quote && n - i > STRING_BULK_SCAN_MIN) {
2618
+ if (nextQuote < i) {
2619
+ const q = s.indexOf(quoteChar, i);
2620
+ nextQuote = q === -1 ? n : q;
2621
+ }
2622
+ if (nextBackslash < i) {
2623
+ if (i < this.#backslashFrom || i > this.#backslashAt) {
2624
+ const b = s.indexOf("\\", i);
2625
+ this.#backslashFrom = i;
2626
+ this.#backslashAt = b === -1 ? n : b;
2627
+ }
2628
+ nextBackslash = this.#backslashAt;
2629
+ }
2630
+ i = nextQuote < nextBackslash ? nextQuote : nextBackslash;
2631
+ if (i >= n) break;
2632
+ }
2303
2633
  const cc = s.charCodeAt(i);
2304
2634
  if (cc !== 92 && cc !== quote) {
2305
2635
  i++;
@@ -2476,7 +2806,7 @@ var JsonLexer = class {
2476
2806
  }
2477
2807
  };
2478
2808
  //#endregion
2479
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/json-parse.ts
2809
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/json-parse.ts
2480
2810
  const U = 117;
2481
2811
  (() => {
2482
2812
  const e = [];
@@ -2655,16 +2985,25 @@ function parseJsonWithRepair(json) {
2655
2985
  function parseStreamingJson(partialJson) {
2656
2986
  const trimmed = partialJson?.trimStart();
2657
2987
  if (!trimmed) return {};
2658
- try {
2988
+ const first = trimmed.charCodeAt(0);
2989
+ if (first !== 123 && first !== 91 || canCloseContainer(trimmed)) try {
2659
2990
  return JSON.parse(trimmed);
2991
+ } catch {}
2992
+ try {
2993
+ return new RelaxedJson(trimmed, true).parse() ?? {};
2660
2994
  } catch {
2661
- try {
2662
- return new RelaxedJson(trimmed, true).parse() ?? {};
2663
- } catch {
2664
- return {};
2665
- }
2995
+ return {};
2666
2996
  }
2667
2997
  }
2998
+ /** True when the last non-whitespace character is `}` or `]`. */
2999
+ function canCloseContainer(text) {
3000
+ for (let i = text.length - 1; i >= 0; i--) {
3001
+ const code = text.charCodeAt(i);
3002
+ if (code === 32 || code === 9 || code === 10 || code === 13) continue;
3003
+ return code === 125 || code === 93;
3004
+ }
3005
+ return false;
3006
+ }
2668
3007
  /**
2669
3008
  * Throttled variant of {@link parseStreamingJson} for the per-delta hot path.
2670
3009
  *
@@ -2885,10 +3224,71 @@ function classifyJsonPrefix(text) {
2885
3224
  }
2886
3225
  return expect === 7 ? "complete" : "prefix";
2887
3226
  }
2888
- new Intl.Segmenter();
2889
3227
  //#endregion
2890
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/mime.ts
2891
- const DEFAULT_IMAGE_METADATA_HEADER_BYTES = 262144;
3228
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/loop-phase.ts
3229
+ /**
3230
+ * Process-global synchronous loop phases. The watchdog arms a late window and
3231
+ * reads attribution via {@link takeLoopPhaseAttribution}; attribution totals
3232
+ * only the innermost label's time inside that window. A label is named only
3233
+ * when its total outweighs all unlabeled time, otherwise the block remains
3234
+ * "unknown". Disarmed push/pop do not read the clock.
3235
+ *
3236
+ * This is deliberately a process-global stack and not part of the logger span
3237
+ * machinery: `main.ts` ends timing spans before the interactive TUI starts, so
3238
+ * `logger.openSpanPath()` is empty in a live session.
3239
+ *
3240
+ * Correctness constraint: each `pushLoopPhase` must be balanced by a
3241
+ * `popLoopPhase` within the SAME synchronous execution (always via `try`/
3242
+ * `finally`). The stack is global and shared, so a label held across an
3243
+ * `await`/async boundary — or interleaved between concurrent tasks — would
3244
+ * misattribute or leak phases. Instrument only synchronous spans; for async
3245
+ * work, push/pop around each synchronous chunk, not across the await.
3246
+ */
3247
+ const stack = [];
3248
+ const defaultClock = () => performance.now();
3249
+ let windowStart = Number.POSITIVE_INFINITY;
3250
+ let segmentStart = windowStart;
3251
+ let clock = defaultClock;
3252
+ const totals = /* @__PURE__ */ new Map();
3253
+ let labeledMs = 0;
3254
+ function boundary(t) {
3255
+ const ms = Math.max(0, t - Math.max(segmentStart, windowStart));
3256
+ const label = stack[stack.length - 1];
3257
+ if (label !== void 0 && ms > 0) {
3258
+ totals.set(label, (totals.get(label) ?? 0) + ms);
3259
+ labeledMs += ms;
3260
+ }
3261
+ segmentStart = t;
3262
+ }
3263
+ /** Arm an attribution window starting at `start` (a deadline on `now`'s clock). Called with no arguments it disarms and clears. */
3264
+ function resetLoopPhaseWindow(start = Number.POSITIVE_INFINITY, now = defaultClock) {
3265
+ windowStart = segmentStart = start;
3266
+ clock = now;
3267
+ totals.clear();
3268
+ labeledMs = 0;
3269
+ }
3270
+ /** Attribute the window [start, end] and disarm it. Returns undefined when no window is armed or no label outweighs unlabeled time. */
3271
+ function takeLoopPhaseAttribution(end) {
3272
+ if (windowStart === Number.POSITIVE_INFINITY) return void 0;
3273
+ const t = end ?? clock();
3274
+ boundary(t);
3275
+ const unknownMs = Math.max(0, t - windowStart - labeledMs);
3276
+ let winner;
3277
+ let winnerMs = 0;
3278
+ for (const [label, ms] of totals) if (ms >= winnerMs) {
3279
+ winner = label;
3280
+ winnerMs = ms;
3281
+ }
3282
+ const result = winner !== void 0 && winnerMs > unknownMs ? {
3283
+ label: winner,
3284
+ ms: winnerMs
3285
+ } : void 0;
3286
+ resetLoopPhaseWindow();
3287
+ return result;
3288
+ }
3289
+ //#endregion
3290
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/mime.ts
3291
+ const IMAGE_METADATA_HEADER_BYTES = 262144;
2892
3292
  const PNG_MAGIC = Buffer.from([
2893
3293
  137,
2894
3294
  80,
@@ -3065,83 +3465,24 @@ function parseWebpMetadata(header) {
3065
3465
  function parseImageMetadata(header) {
3066
3466
  return parsePngMetadata(header) ?? parseJpegMetadata(header) ?? parseGifMetadata(header) ?? parseWebpMetadata(header);
3067
3467
  }
3068
- function readImageMetadata(filePath, maxBytes = DEFAULT_IMAGE_METADATA_HEADER_BYTES) {
3069
- return peekFile(filePath, maxBytes, parseImageMetadata);
3070
- }
3071
- //#endregion
3072
- //#region src/tools/shared/bun-ffi-shim.ts
3073
- /**
3074
- * DSH named-import shim for `bun:ffi` (pi-utils stderr-guard / process-name).
3075
- * Both consumers degrade silently when dlopen is unavailable ("Never throws:
3076
- * bun:ffi unavailability ... degrades silently"), so this shim returns null.
3077
- */
3078
- const FFIType = {};
3079
- function dlopen() {
3080
- return null;
3081
- }
3082
- //#endregion
3083
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/stderr-guard.ts
3468
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3084
3469
  /**
3085
- * Terminal stderr guard: keeps unmanaged fd-2 writes off the terminal while a
3086
- * TUI owns the viewport.
3087
- *
3088
- * On macOS, runtime diagnostics are written by the platform directly to file
3089
- * descriptor 2 at arbitrary times — e.g. libmalloc's "MallocStackLogging:
3090
- * can't turn off malloc stack logging because it was not enabled" when the OS
3091
- * broadcasts a memory-diagnostic event to long-lived processes. Those bytes
3092
- * bypass the renderer and paint straight into the viewport. Stripping the
3093
- * MallocStackLogging* env vars (cli.ts) only protects child processes; it
3094
- * cannot stop libmalloc inside THIS process from logging.
3095
- *
3096
- * Fix (mirrors openai/codex#24459): while the TUI owns the terminal, dup fd 2
3097
- * aside and dup2 a redirect target over it; restore the saved fd whenever
3098
- * terminal ownership is released (external editor, Ctrl+Z suspend, shutdown,
3099
- * crash restore). Unlike codex we redirect to the omp log file — not
3100
- * /dev/null — so the diagnostics stay greppable and Bun native-crash reports
3101
- * (which abort before any JS cleanup can restore fd 2) are preserved.
3102
- *
3103
- * Only dup/dup2 go through bun:ffi. fcntl is deliberately avoided: it is
3104
- * variadic, and the arm64-darwin ABI passes variadic arguments on the stack,
3105
- * so a fixed-arity FFI signature would read garbage for the third argument.
3470
+ * Validate standard base64 image data without decoding it. Padding may be omitted, but
3471
+ * present padding must be canonical, the trailing bits must be zero, and the data non-empty.
3106
3472
  */
3107
- const STDERR_FILENO = 2;
3108
- let libcFdOpsCache;
3109
- function libcFdOps() {
3110
- if (libcFdOpsCache !== void 0) return libcFdOpsCache;
3111
- libcFdOpsCache = null;
3112
- if (process.platform === "win32") return null;
3113
- const candidates = process.platform === "darwin" ? ["libSystem.B.dylib", "/usr/lib/libSystem.B.dylib"] : ["libc.so.6", "libc.so"];
3114
- for (const candidate of candidates) try {
3115
- libcFdOpsCache = dlopen(candidate, {
3116
- dup: {
3117
- args: [FFIType.i32],
3118
- returns: FFIType.i32
3119
- },
3120
- dup2: {
3121
- args: [FFIType.i32, FFIType.i32],
3122
- returns: FFIType.i32
3123
- }
3124
- }).symbols;
3125
- return libcFdOpsCache;
3126
- } catch {}
3127
- return libcFdOpsCache;
3128
- }
3129
- /** Saved dup of the real stderr while suppression is active, else null. */
3130
- let savedStderrFd = null;
3131
- /**
3132
- * Re-point fd 2 at the saved terminal stderr. Safe to call unconditionally:
3133
- * no-op when suppression is not active. Called at every terminal-ownership
3134
- * release and by the postmortem fatal handlers before they print, so crash
3135
- * reports reach the real terminal.
3136
- */
3137
- function restoreTerminalStderr() {
3138
- if (savedStderrFd === null) return;
3139
- const saved = savedStderrFd;
3140
- savedStderrFd = null;
3141
- libcFdOps()?.dup2(saved, STDERR_FILENO);
3142
- try {
3143
- fs.closeSync(saved);
3144
- } catch {}
3473
+ function base64ImageDataFailureReason(data) {
3474
+ if (data.length === 0) return "empty image data";
3475
+ const match = /^([A-Za-z0-9+/]+)(={0,2})$/.exec(data);
3476
+ if (!match) return "invalid base64 image data";
3477
+ const encoded = match[1];
3478
+ const paddingLength = match[2].length;
3479
+ const remainder = encoded.length % 4;
3480
+ if (remainder === 1 || paddingLength === 1 && remainder !== 3 || paddingLength === 2 && remainder !== 2) return "invalid base64 image data";
3481
+ const lastSextet = BASE64_ALPHABET.indexOf(encoded[encoded.length - 1]);
3482
+ if (remainder === 2 && (lastSextet & 15) !== 0 || remainder === 3 && (lastSextet & 3) !== 0) return "invalid base64 image data";
3483
+ }
3484
+ function readImageMetadata(filePath, maxBytes = IMAGE_METADATA_HEADER_BYTES) {
3485
+ return peekFile(filePath, maxBytes, parseImageMetadata);
3145
3486
  }
3146
3487
  const callbackList = [];
3147
3488
  let cleanupStage = "idle";
@@ -3163,6 +3504,25 @@ const CLEANUP_DEADLINE_MS = 1e4;
3163
3504
  */
3164
3505
  const NATIVE_PROCESS_EXIT = Symbol.for("omp.postmortem.nativeProcessExit");
3165
3506
  /**
3507
+ * Walk a guarded exit primitive down to the native it shadows.
3508
+ *
3509
+ * `withHostGuard` stamps each throwing replacement with the primitive it
3510
+ * shadows under {@link NATIVE_PROCESS_EXIT}; nested guard windows stack, so a
3511
+ * single unwrap can still land on another throwing stub. Follow the chain
3512
+ * (cycle-guarded) until a link carries no stamp — that link is native.
3513
+ */
3514
+ function nativeHardExit(fn) {
3515
+ let current = fn;
3516
+ const seen = /* @__PURE__ */ new Set();
3517
+ while (typeof current === "function" && !seen.has(current)) {
3518
+ seen.add(current);
3519
+ const behind = Reflect.get(current, NATIVE_PROCESS_EXIT);
3520
+ if (typeof behind !== "function") return current;
3521
+ current = behind;
3522
+ }
3523
+ return typeof current === "function" ? current : void 0;
3524
+ }
3525
+ /**
3166
3526
  * Hard-exit the process through the native primitive, resolved on every call.
3167
3527
  *
3168
3528
  * The native exit is deliberately re-resolved here rather than bound at module
@@ -3173,13 +3533,35 @@ const NATIVE_PROCESS_EXIT = Symbol.for("omp.postmortem.nativeProcessExit");
3173
3533
  * init could freeze the throwing stub forever and turn every later shutdown
3174
3534
  * (SIGHUP/SIGINT/fatal) into an unhandled-rejection loop (#7393). When the
3175
3535
  * guard is active the stub carries the native exit under
3176
- * {@link NATIVE_PROCESS_EXIT}; unwrapping it lets a mid-guard signal still exit
3177
- * (#6488). Otherwise the current `process.reallyExit`/`process.exit` is native.
3536
+ * {@link NATIVE_PROCESS_EXIT} (#6488).
3537
+ *
3538
+ * Both globals are reinstalled to their natives before exiting: Bun's
3539
+ * `process.exit` re-reads `process.reallyExit` at call time, so exiting through
3540
+ * one primitive while its sibling still holds the throwing stub re-enters the
3541
+ * guard and loops the rejection storm (#11789). After restoring, `reallyExit`
3542
+ * (the low-level primitive) is preferred; `process.exit` and finally `SIGKILL`
3543
+ * are fallbacks so a poisoned or absent chain can never leave the process alive.
3544
+ *
3545
+ * Buffered log records are written first: `reallyExit` skips the `exit` event
3546
+ * the logger otherwise flushes on, which would drop the last batch on every
3547
+ * signal and fatal exit.
3178
3548
  */
3179
3549
  function exitProcess(code) {
3180
- const current = typeof process.reallyExit === "function" ? process.reallyExit : process.exit;
3181
- const behind = Reflect.get(current, NATIVE_PROCESS_EXIT);
3182
- return (typeof behind === "function" ? behind : current).call(process, code);
3550
+ flush();
3551
+ const reallyExit = nativeHardExit(typeof process.reallyExit === "function" ? process.reallyExit : void 0);
3552
+ const exit = nativeHardExit(process.exit);
3553
+ if (reallyExit) process.reallyExit = reallyExit;
3554
+ if (exit) process.exit = exit;
3555
+ try {
3556
+ reallyExit?.call(process, code);
3557
+ } catch {}
3558
+ try {
3559
+ exit?.call(process, code);
3560
+ } catch {}
3561
+ try {
3562
+ process.kill(process.pid, "SIGKILL");
3563
+ } catch {}
3564
+ throw new Error(`exitProcess(${code}) failed to terminate the process`);
3183
3565
  }
3184
3566
  let cleanupPromise;
3185
3567
  const fatalRecoveryHintProviders = /* @__PURE__ */ new Set();
@@ -3382,12 +3764,19 @@ async function exitAfterFatal(output, logMessage, err, reason) {
3382
3764
  exitProcess(1);
3383
3765
  }
3384
3766
  }
3385
- if (isMainThread) process.on("SIGINT", async () => {
3767
+ /** Contain an EPIPE from an optional worker IPC `send()` (#2997, #9158). */
3768
+ function handleWorkerSendEpipe(err) {
3769
+ if (!isIpcSendEpipe(err)) return false;
3770
+ warn("Ignoring EPIPE from worker IPC send; optional subsystem will self-recover", { err });
3771
+ return true;
3772
+ }
3773
+ if (Bun.isMainThread) process.on("SIGINT", async () => {
3386
3774
  await runCleanup("sigint");
3387
3775
  exitProcess(130);
3388
- }).on("SIGUSR1", () => {
3776
+ }).on("SIGUSR1", async () => {
3389
3777
  if (inspectorOpened) return;
3390
3778
  inspectorOpened = true;
3779
+ const { default: inspector } = await import("node:inspector");
3391
3780
  inspector.open(void 0, void 0, false);
3392
3781
  const url = inspector.url();
3393
3782
  process.stderr.write(`Inspector opened: ${url}\n`);
@@ -3397,10 +3786,7 @@ if (isMainThread) process.on("SIGINT", async () => {
3397
3786
  return;
3398
3787
  }
3399
3788
  const err = thrown instanceof Error ? thrown : new Error(String(thrown));
3400
- if (isIpcSendEpipe(err)) {
3401
- warn("Ignoring EPIPE from worker IPC send; optional subsystem will self-recover", { err });
3402
- return;
3403
- }
3789
+ if (handleWorkerSendEpipe(err)) return;
3404
3790
  if (isWorkerIpcDeserializeError(err)) {
3405
3791
  warn("Malformed worker IPC frame; faulting active worker subsystems", { err });
3406
3792
  faultWorkerIpcChannels(err);
@@ -3413,10 +3799,7 @@ if (isMainThread) process.on("SIGINT", async () => {
3413
3799
  await exitAfterFatal(`${formatFatalError("Uncaught Exception", err)}${formatFatalRecoveryHints()}`, "Uncaught exception", err, "uncaught_exception");
3414
3800
  }).on("unhandledRejection", async (reason) => {
3415
3801
  const err = reason instanceof Error ? reason : new Error(String(reason));
3416
- if (classifyBrokenPipe(err) === "ipc-send") {
3417
- warn("Ignoring EPIPE from worker IPC send; optional subsystem will self-recover", { err });
3418
- return;
3419
- }
3802
+ if (handleWorkerSendEpipe(err)) return;
3420
3803
  if (isExpectedCleanupError(reason)) {
3421
3804
  warn("Ignoring expected cleanup rejection", { err });
3422
3805
  return;
@@ -3471,8 +3854,8 @@ function register(id, callback, options = {}) {
3471
3854
  try {
3472
3855
  const pending = invokeCleanup(registration, reason, keepAlive, cleanupPass);
3473
3856
  if (!pending) return;
3474
- const tracked = pending.catch((error$2) => {
3475
- const err = error$2 instanceof Error ? error$2 : new Error(String(error$2));
3857
+ const tracked = pending.catch((error$3) => {
3858
+ const err = error$3 instanceof Error ? error$3 : new Error(String(error$3));
3476
3859
  error("Cleanup callback failed", {
3477
3860
  err,
3478
3861
  id,
@@ -3480,8 +3863,8 @@ function register(id, callback, options = {}) {
3480
3863
  });
3481
3864
  });
3482
3865
  activeLatePromises?.push(tracked);
3483
- } catch (error$1) {
3484
- const err = error$1 instanceof Error ? error$1 : new Error(String(error$1));
3866
+ } catch (error$2) {
3867
+ const err = error$2 instanceof Error ? error$2 : new Error(String(error$2));
3485
3868
  error("Cleanup callback failed", {
3486
3869
  err,
3487
3870
  id,
@@ -3504,7 +3887,7 @@ function register(id, callback, options = {}) {
3504
3887
  return cancel;
3505
3888
  }
3506
3889
  //#endregion
3507
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/which.ts
3890
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/which.ts
3508
3891
  const XCODE_BINS = /* @__PURE__ */ new Set([
3509
3892
  "clang",
3510
3893
  "clang++",
@@ -3599,14 +3982,16 @@ function darwinWhich(command, options) {
3599
3982
  if (isXcodeBin(command)) return getMacosToolPaths().get(command) ?? null;
3600
3983
  return null;
3601
3984
  }
3985
+ function filterAbsoluteSearchPath(rawPath) {
3986
+ if (!rawPath) return null;
3987
+ return rawPath.split(path$1.delimiter).filter((dir) => dir.length > 0 && isFullyQualifiedPath(dir)).join(path$1.delimiter) || null;
3988
+ }
3602
3989
  const whichFresh = os.platform() === "darwin" ? darwinWhich : (command, options) => Bun.which(command, options);
3603
3990
  function cacheKey$1(command, options) {
3604
3991
  if (!options) return command;
3605
- if (!options.cwd && !options.PATH) return command;
3606
- let h = Bun.hash(command);
3607
- if (options.cwd) h = Bun.hash(options.cwd, h);
3608
- if (options.PATH) h = Bun.hash(options.PATH, h);
3609
- return h;
3992
+ const cwd = options?.cwd ?? "";
3993
+ const binPath = options?.PATH ?? "";
3994
+ return `${command.length}:${command}${cwd.length}:${cwd}${binPath.length}:${binPath}`;
3610
3995
  }
3611
3996
  /**
3612
3997
  * Locate binary on PATH (with flexible caching).
@@ -3617,10 +4002,18 @@ function cacheKey$1(command, options) {
3617
4002
  */
3618
4003
  function $which(command, options) {
3619
4004
  const cachePolicy = options?.cache ?? 0;
3620
- const lookupOptions = options?.PATH !== void 0 || process.env.PATH === void 0 ? options : {
4005
+ let lookupOptions = options?.PATH !== void 0 || process.env.PATH === void 0 ? options : {
3621
4006
  ...options,
3622
4007
  PATH: process.env.PATH
3623
4008
  };
4009
+ if (options?.requireAbsolutePaths) {
4010
+ const safePath = filterAbsoluteSearchPath(lookupOptions?.PATH);
4011
+ if (!safePath) return null;
4012
+ lookupOptions = {
4013
+ ...lookupOptions,
4014
+ PATH: safePath
4015
+ };
4016
+ }
3624
4017
  let key;
3625
4018
  if (cachePolicy !== 1) {
3626
4019
  key = cacheKey$1(command, lookupOptions);
@@ -3630,11 +4023,148 @@ function $which(command, options) {
3630
4023
  }
3631
4024
  }
3632
4025
  const result = whichFresh(command, lookupOptions);
4026
+ if (result && options?.requireAbsolutePaths && !isFullyQualifiedPath(result)) return null;
3633
4027
  if (key != null && cachePolicy !== 3) toolCache.set(key, result);
3634
4028
  return result;
3635
4029
  }
3636
4030
  //#endregion
3637
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/template.ts
4031
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/lru.ts
4032
+ function positiveInteger(value, name) {
4033
+ if (value === void 0) return 0;
4034
+ if (!Number.isInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer`);
4035
+ return value;
4036
+ }
4037
+ /** A bounded least-recently-used cache with optional size and lifetime limits. */
4038
+ var LRUCache = class {
4039
+ #entries = /* @__PURE__ */ new Map();
4040
+ #max;
4041
+ #maxSize;
4042
+ #maxEntrySize;
4043
+ #sizeCalculation;
4044
+ #ttl;
4045
+ #updateAgeOnGet;
4046
+ #dispose;
4047
+ #calculatedSize = 0;
4048
+ /** Creates an empty cache. */
4049
+ constructor(options) {
4050
+ this.#max = positiveInteger(options.max, "max");
4051
+ this.#maxSize = positiveInteger(options.maxSize, "maxSize");
4052
+ const explicitMaxEntrySize = positiveInteger(options.maxEntrySize, "maxEntrySize");
4053
+ this.#maxEntrySize = explicitMaxEntrySize || this.#maxSize;
4054
+ this.#ttl = positiveInteger(options.ttl, "ttl");
4055
+ if (this.#max === 0 && this.#maxSize === 0 && this.#ttl === 0) throw new TypeError("At least one of max, maxSize, or ttl is required");
4056
+ if ((this.#maxSize !== 0 || this.#maxEntrySize !== 0) && options.sizeCalculation === void 0) throw new TypeError("sizeCalculation is required when a size limit is set");
4057
+ this.#sizeCalculation = options.sizeCalculation;
4058
+ this.#updateAgeOnGet = options.updateAgeOnGet === true;
4059
+ this.#dispose = options.dispose;
4060
+ }
4061
+ /** Number of entries, including stale entries not yet removed by `get`. */
4062
+ get size() {
4063
+ return this.#entries.size;
4064
+ }
4065
+ /** Aggregate calculated size of retained entries. */
4066
+ get calculatedSize() {
4067
+ return this.#calculatedSize;
4068
+ }
4069
+ /** Stores a value and makes it most recently used. */
4070
+ set(key, value) {
4071
+ if (value === void 0) {
4072
+ this.delete(key);
4073
+ return this;
4074
+ }
4075
+ const size = this.#entrySize(value, key);
4076
+ const previous = this.#entries.get(key);
4077
+ if (this.#maxEntrySize !== 0 && size > this.#maxEntrySize) {
4078
+ if (previous !== void 0) this.#remove(key, previous, "set");
4079
+ return this;
4080
+ }
4081
+ if (previous !== void 0) {
4082
+ if (previous.value !== value) this.#dispose?.(previous.value, key, "set");
4083
+ this.#calculatedSize -= previous.size;
4084
+ this.#entries.delete(key);
4085
+ }
4086
+ while (this.#max !== 0 && this.#entries.size >= this.#max || this.#maxSize !== 0 && this.#calculatedSize + size > this.#maxSize) {
4087
+ const oldest = this.#entries.entries().next().value;
4088
+ if (oldest === void 0) break;
4089
+ this.#remove(oldest[0], oldest[1], "evict");
4090
+ }
4091
+ this.#entries.set(key, {
4092
+ value,
4093
+ size,
4094
+ start: this.#ttl === 0 ? 0 : performance.now()
4095
+ });
4096
+ this.#calculatedSize += size;
4097
+ return this;
4098
+ }
4099
+ /** Returns a value and makes a fresh entry most recently used. */
4100
+ get(key) {
4101
+ const entry = this.#entries.get(key);
4102
+ if (entry === void 0) return void 0;
4103
+ if (this.#isStale(entry)) {
4104
+ this.#remove(key, entry, "expire");
4105
+ return;
4106
+ }
4107
+ if (this.#updateAgeOnGet && this.#ttl !== 0) entry.start = performance.now();
4108
+ this.#entries.delete(key);
4109
+ this.#entries.set(key, entry);
4110
+ return entry.value;
4111
+ }
4112
+ /** Reports whether a fresh value is present without changing recency. */
4113
+ has(key) {
4114
+ const entry = this.#entries.get(key);
4115
+ return entry !== void 0 && !this.#isStale(entry);
4116
+ }
4117
+ /** Returns a fresh value without changing recency or removing stale data. */
4118
+ peek(key) {
4119
+ const entry = this.#entries.get(key);
4120
+ return entry === void 0 || this.#isStale(entry) ? void 0 : entry.value;
4121
+ }
4122
+ /** Removes a value, returning whether one was present. */
4123
+ delete(key) {
4124
+ const entry = this.#entries.get(key);
4125
+ if (entry === void 0) return false;
4126
+ this.#remove(key, entry, "delete");
4127
+ return true;
4128
+ }
4129
+ /** Removes every value from least to most recently used. */
4130
+ clear() {
4131
+ for (const [key, entry] of this.#entries) this.#dispose?.(entry.value, key, "delete");
4132
+ this.#entries.clear();
4133
+ this.#calculatedSize = 0;
4134
+ }
4135
+ /** Iterates fresh keys from most to least recently used. */
4136
+ *keys() {
4137
+ const entries = [...this.#entries.entries()];
4138
+ for (let index = entries.length - 1; index >= 0; index--) {
4139
+ const [key, entry] = entries[index];
4140
+ if (!this.#isStale(entry)) yield key;
4141
+ }
4142
+ }
4143
+ /** Iterates fresh values from most to least recently used. */
4144
+ *values() {
4145
+ const entries = [...this.#entries.values()];
4146
+ for (let index = entries.length - 1; index >= 0; index--) {
4147
+ const entry = entries[index];
4148
+ if (!this.#isStale(entry)) yield entry.value;
4149
+ }
4150
+ }
4151
+ #entrySize(value, key) {
4152
+ if (this.#sizeCalculation === void 0) return 0;
4153
+ const size = this.#sizeCalculation(value, key);
4154
+ if (!Number.isInteger(size) || size <= 0) throw new TypeError("sizeCalculation return invalid (expect positive integer)");
4155
+ return size;
4156
+ }
4157
+ #isStale(entry) {
4158
+ return this.#ttl !== 0 && performance.now() - entry.start > this.#ttl;
4159
+ }
4160
+ #remove(key, entry, reason) {
4161
+ this.#dispose?.(entry.value, key, reason);
4162
+ this.#entries.delete(key);
4163
+ this.#calculatedSize -= entry.size;
4164
+ }
4165
+ };
4166
+ //#endregion
4167
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/template.ts
3638
4168
  /** A string that bypasses HTML escaping when interpolated. */
3639
4169
  var SafeString = class {
3640
4170
  #value;
@@ -3856,7 +4386,8 @@ function parseTemplate(source) {
3856
4386
  stack.push({
3857
4387
  node: nested,
3858
4388
  target: parentTarget,
3859
- inverted: false
4389
+ inverted: false,
4390
+ chained: true
3860
4391
  });
3861
4392
  }
3862
4393
  continue;
@@ -3874,13 +4405,15 @@ function parseTemplate(source) {
3874
4405
  stack.push({
3875
4406
  node,
3876
4407
  target,
3877
- inverted
4408
+ inverted,
4409
+ chained: false
3878
4410
  });
3879
4411
  target = node.body;
3880
4412
  continue;
3881
4413
  }
3882
4414
  if (raw.startsWith("/")) {
3883
- const current = stack.pop();
4415
+ let current = stack.pop();
4416
+ while (current?.chained) current = stack.pop();
3884
4417
  if (!current || current.node.expression.name !== raw.slice(1).trim()) throw new Error(`Parse error: mismatched ${raw}`);
3885
4418
  if (current.inverted) [current.node.body, current.node.inverse] = [current.node.inverse, current.node.body];
3886
4419
  target = current.target;
@@ -4136,7 +4669,7 @@ var TemplateEngine = class {
4136
4669
  }
4137
4670
  };
4138
4671
  //#endregion
4139
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/prompt.ts
4672
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/prompt.ts
4140
4673
  const OPENING_XML = /^<([a-z_-]+)(?:\s+[^>]*)?>$/;
4141
4674
  /**
4142
4675
  * Closing XML tag matcher, manual equivalent of `/^<\/([a-z_-]+)>$/` — avoids a
@@ -4508,7 +5041,7 @@ handlebars.registerHelper("includes", (collection, item) => {
4508
5041
  */
4509
5042
  handlebars.registerHelper("not", (value) => !value);
4510
5043
  handlebars.registerHelper("jsonStringify", (value) => JSON.stringify(value));
4511
- const compiledTemplateCache = /* @__PURE__ */ new Map();
5044
+ const compiledTemplateCache = new LRUCache({ max: 256 });
4512
5045
  function compile(template) {
4513
5046
  const cached = compiledTemplateCache.get(template);
4514
5047
  if (cached) return cached;
@@ -4579,7 +5112,7 @@ function _usingCtx() {
4579
5112
  };
4580
5113
  }
4581
5114
  //#endregion
4582
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/ptree.ts
5115
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/ptree.ts
4583
5116
  /**
4584
5117
  * Process tree management utilities for Bun subprocesses.
4585
5118
  *
@@ -4590,6 +5123,8 @@ function _usingCtx() {
4590
5123
  */
4591
5124
  const LINUX_SUBREAPER_COMMAND_ENV = "OMP_PTREE_SUBREAPER_COMMAND";
4592
5125
  const LINUX_SUBREAPER_BUN_BE_BUN_ENV = "OMP_PTREE_SUBREAPER_BUN_BE_BUN";
5126
+ const SUBREAPER_KILL_WINDOW_MS = 100;
5127
+ const SUBREAPER_KILL_POLL_MS = 5;
4593
5128
  /**
4594
5129
  * Build the Linux child-subreaper entrypoint.
4595
5130
  *
@@ -4723,11 +5258,13 @@ var ChildProcess = class {
4723
5258
  #stderrDone;
4724
5259
  #exited;
4725
5260
  #openPipeReaders = 1;
4726
- #drainCutoff;
4727
- #resolveDrainCutoff;
5261
+ #cutoff = false;
5262
+ /** Readers of piped stdio; the timeout cancels them so pending reads settle. */
5263
+ #pipeReaders = /* @__PURE__ */ new Set();
4728
5264
  #timeoutTimer;
4729
5265
  #stderrStream;
4730
5266
  #terminating;
5267
+ #hardKillSweep;
4731
5268
  #terminateGroup;
4732
5269
  #hardKillTree;
4733
5270
  #windowsRootProcess;
@@ -4750,28 +5287,23 @@ var ChildProcess = class {
4750
5287
  }
4751
5288
  const { promise, resolve, reject } = Promise.withResolvers();
4752
5289
  this.#exited = promise;
4753
- const drainCutoff = Promise.withResolvers();
4754
- this.#drainCutoff = drainCutoff.promise;
4755
- this.#resolveDrainCutoff = drainCutoff.resolve;
4756
- const pipeCutoff = this.#drainCutoff;
4757
5290
  this.#stderrDone = (async () => {
4758
5291
  const reader = stderrStream.getReader();
5292
+ this.#pipeReaders.add(reader);
4759
5293
  try {
4760
5294
  for (;;) {
4761
- const chunk = await Promise.race([reader.read().then((r) => ({
4762
- cutoff: false,
4763
- r
4764
- })), pipeCutoff.then(() => ({ cutoff: true }))]);
4765
- if (chunk.cutoff) {
5295
+ if (this.#cutoff) {
4766
5296
  await reader.cancel().catch(() => {});
4767
5297
  break;
4768
5298
  }
4769
- if (chunk.r.done) break;
4770
- this.#stderrChunks?.push(chunk.r.value);
4771
- this.#stderrTail += dec.decode(chunk.r.value, { stream: true });
5299
+ const r = await reader.read();
5300
+ if (r.done) break;
5301
+ this.#stderrChunks?.push(r.value);
5302
+ this.#stderrTail += dec.decode(r.value, { stream: true });
4772
5303
  trim();
4773
5304
  }
4774
5305
  } catch {}
5306
+ this.#pipeReaders.delete(reader);
4775
5307
  this.#openPipeReaders--;
4776
5308
  this.#stderrTail += dec.decode();
4777
5309
  trim();
@@ -4848,11 +5380,16 @@ var ChildProcess = class {
4848
5380
  this.#exitReasonPending = reason;
4849
5381
  if (this.proc.exitCode !== null) this.#exitReason = reason;
4850
5382
  }
5383
+ if (this.#hardKillSweep) return;
4851
5384
  if (gracefulMs !== void 0 && gracefulMs < 0 && this.#hardKillTree && this.proc.exitCode === null) {
4852
5385
  const root = Process.fromPid(this.proc.pid);
4853
5386
  if (root) {
4854
- root.killTree(9);
4855
- this.#terminating = Promise.resolve();
5387
+ const sweep = this.#hardKillSubreaperTree(root).catch((e) => void 0);
5388
+ this.#hardKillSweep = sweep;
5389
+ this.#terminating = sweep;
5390
+ sweep.finally(() => {
5391
+ if (this.#hardKillSweep === sweep) this.#hardKillSweep = void 0;
5392
+ });
4856
5393
  return;
4857
5394
  }
4858
5395
  }
@@ -4873,11 +5410,56 @@ var ChildProcess = class {
4873
5410
  gracefulMs,
4874
5411
  group: this.#terminateGroup
4875
5412
  };
4876
- this.#terminating = (this.#windowsRootProcess ?? Process.fromPid(this.proc.pid))?.terminate(options)?.catch((e) => void 0);
5413
+ this.#terminating = this.#terminateTree(options);
5414
+ }
5415
+ }
5416
+ /**
5417
+ * Native tree termination. A native handle that cannot be resolved or used
5418
+ * — the PID left the process table, or a `bun test --isolate` realm whose
5419
+ * napi classes another realm superseded ("Value is not an instance of class
5420
+ * `Process`") — must not leave the child running, so signal it directly.
5421
+ */
5422
+ #terminateTree(options) {
5423
+ const gracefulMs = options?.gracefulMs ?? 1e3;
5424
+ try {
5425
+ const root = this.#windowsRootProcess ?? Process.fromPid(this.proc.pid);
5426
+ if (root) return root.terminate(options).catch(() => this.#signalChild(gracefulMs));
5427
+ } catch {}
5428
+ return this.#signalChild(gracefulMs);
5429
+ }
5430
+ /** SIGTERM the child (its group when it leads one), then SIGKILL if it outlives `gracefulMs`. */
5431
+ async #signalChild(gracefulMs) {
5432
+ const signal = (name) => {
5433
+ try {
5434
+ if (this.#terminateGroup && process.platform !== "win32") process.kill(-this.proc.pid, name);
5435
+ else this.proc.kill(name);
5436
+ } catch {}
5437
+ };
5438
+ if (gracefulMs >= 0) {
5439
+ signal("SIGTERM");
5440
+ await Promise.race([this.proc.exited, Bun.sleep(gracefulMs)]);
5441
+ }
5442
+ if (this.proc.exitCode === null && this.proc.signalCode === null) signal("SIGKILL");
5443
+ }
5444
+ async #hardKillSubreaperTree(root) {
5445
+ try {
5446
+ const deadline = Date.now() + SUBREAPER_KILL_WINDOW_MS;
5447
+ let emptySweeps = 0;
5448
+ while (emptySweeps < 2 && Date.now() < deadline) {
5449
+ const children = root.children();
5450
+ if (children.length === 0) emptySweeps++;
5451
+ else {
5452
+ emptySweeps = 0;
5453
+ for (const child of children) child.killTree(9);
5454
+ }
5455
+ if (emptySweeps < 2) await Bun.sleep(SUBREAPER_KILL_POLL_MS);
5456
+ }
5457
+ } finally {
5458
+ root.killTree(9);
4877
5459
  }
4878
5460
  }
4879
5461
  async #throwIfAborted() {
4880
- const exitReason = this.exitReason;
5462
+ const exitReason = this.#exitReason ?? this.#exitReasonPending;
4881
5463
  if (!exitReason?.aborted) return;
4882
5464
  if (this.#terminating) await this.#terminating;
4883
5465
  throw exitReason;
@@ -4895,45 +5477,43 @@ var ChildProcess = class {
4895
5477
  async #readStream(stream) {
4896
5478
  this.#openPipeReaders++;
4897
5479
  const reader = stream.getReader();
5480
+ this.#pipeReaders.add(reader);
4898
5481
  const dec = new TextDecoder();
4899
5482
  let out = "";
4900
5483
  try {
4901
5484
  for (;;) {
4902
- const chunk = await Promise.race([reader.read().then((r) => ({
4903
- cutoff: false,
4904
- r
4905
- })), this.#drainCutoff.then(() => ({ cutoff: true }))]);
4906
- if (chunk.cutoff) {
5485
+ if (this.#cutoff) {
4907
5486
  await reader.cancel().catch(() => {});
4908
5487
  break;
4909
5488
  }
4910
- if (chunk.r.done) break;
4911
- out += dec.decode(chunk.r.value, { stream: true });
5489
+ const r = await reader.read();
5490
+ if (r.done) break;
5491
+ out += dec.decode(r.value, { stream: true });
4912
5492
  }
4913
5493
  } catch {}
5494
+ this.#pipeReaders.delete(reader);
4914
5495
  this.#openPipeReaders--;
4915
5496
  return out + dec.decode();
4916
5497
  }
4917
5498
  async #readBytes() {
4918
5499
  const reader = this.proc.stdout.getReader();
4919
5500
  this.#openPipeReaders++;
5501
+ this.#pipeReaders.add(reader);
4920
5502
  const chunks = [];
4921
5503
  let length = 0;
4922
5504
  try {
4923
5505
  for (;;) {
4924
- const chunk = await Promise.race([reader.read().then((r) => ({
4925
- cutoff: false,
4926
- r
4927
- })), this.#drainCutoff.then(() => ({ cutoff: true }))]);
4928
- if (chunk.cutoff) {
5506
+ if (this.#cutoff) {
4929
5507
  await reader.cancel().catch(() => {});
4930
5508
  break;
4931
5509
  }
4932
- if (chunk.r.done) break;
4933
- chunks.push(chunk.r.value);
4934
- length += chunk.r.value.byteLength;
5510
+ const r = await reader.read();
5511
+ if (r.done) break;
5512
+ chunks.push(r.value);
5513
+ length += r.value.byteLength;
4935
5514
  }
4936
5515
  } catch {} finally {
5516
+ this.#pipeReaders.delete(reader);
4937
5517
  this.#openPipeReaders--;
4938
5518
  reader.releaseLock();
4939
5519
  }
@@ -4996,10 +5576,11 @@ var ChildProcess = class {
4996
5576
  };
4997
5577
  }
4998
5578
  attachSignal(signal) {
5579
+ const cleanup = this.#exited.catch(() => {});
4999
5580
  const onAbort = () => this.kill(new AbortError(signal.reason, "<cancelled>"));
5000
5581
  if (signal.aborted) return void onAbort();
5001
5582
  signal.addEventListener("abort", onAbort, { once: true });
5002
- this.#exited.catch(() => {}).finally(() => signal.removeEventListener("abort", onAbort));
5583
+ cleanup.finally(() => signal.removeEventListener("abort", onAbort));
5003
5584
  }
5004
5585
  #clearTimeout() {
5005
5586
  if (!this.#timeoutTimer) return;
@@ -5011,7 +5592,8 @@ var ChildProcess = class {
5011
5592
  this.#exited.catch(() => {});
5012
5593
  const timer = setTimeout(() => {
5013
5594
  if (this.proc.exitCode === null || this.#openPipeReaders > 0 && (this.#terminateGroup || this.#windowsRootProcess)) this.kill(new TimeoutError(ms, this.#stderrTail), -1);
5014
- this.#resolveDrainCutoff();
5595
+ this.#cutoff = true;
5596
+ for (const reader of this.#pipeReaders) reader.cancel().catch(() => {});
5015
5597
  }, ms);
5016
5598
  timer.unref?.();
5017
5599
  this.#timeoutTimer = timer;
@@ -5050,22 +5632,22 @@ function spawnInternal(cmd, opts, retainFullStderr) {
5050
5632
  /** Spawn, wait, and return captured output. */
5051
5633
  async function exec(cmd, opts) {
5052
5634
  try {
5053
- var _usingCtx$1 = _usingCtx();
5635
+ var _usingCtx$2 = _usingCtx();
5054
5636
  const { input, stderr, allowAbort, allowNonZero, ...spawnOpts } = opts ?? {};
5055
5637
  const stdin = typeof input === "string" ? Buffer.from(input) : input;
5056
5638
  const resolved = stdin === void 0 ? spawnOpts : {
5057
5639
  ...spawnOpts,
5058
5640
  stdin
5059
5641
  };
5060
- return await _usingCtx$1.u(spawnInternal(cmd, resolved, stderr === "full")).wait({
5642
+ return await _usingCtx$2.u(spawnInternal(cmd, resolved, stderr === "full")).wait({
5061
5643
  stderr,
5062
5644
  allowAbort,
5063
5645
  allowNonZero
5064
5646
  });
5065
5647
  } catch (_) {
5066
- _usingCtx$1.e = _;
5648
+ _usingCtx$2.e = _;
5067
5649
  } finally {
5068
- _usingCtx$1.d();
5650
+ _usingCtx$2.d();
5069
5651
  }
5070
5652
  }
5071
5653
  /** Combine AbortSignals and timeout values into a single signal. */
@@ -5091,19 +5673,21 @@ function combineSignals(...signals) {
5091
5673
  }
5092
5674
  }
5093
5675
  //#endregion
5094
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/snowflake.ts
5676
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/snowflake.ts
5095
5677
  function randu32() {
5096
5678
  return crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
5097
5679
  }
5098
5680
  const EPOCH = 14200704e5;
5099
5681
  const MAX_SEQ = 4194303;
5682
+ const MAX_DT = 2 ** 42 - 1;
5100
5683
  let Snowflake;
5101
5684
  (function(_Snowflake) {
5102
5685
  const PATTERN = _Snowflake.PATTERN = /^[0-9a-f]{16}$/;
5103
5686
  _Snowflake.EPOCH_TIMESTAMP = EPOCH;
5104
5687
  _Snowflake.MAX_SEQUENCE = MAX_SEQ;
5688
+ _Snowflake.MAX_TIMESTAMP = EPOCH + MAX_DT;
5105
5689
  function formatParts(dt, seq) {
5106
- return (BigInt(dt) << 22n | BigInt(seq)).toString(16).padStart(16, "0");
5690
+ return (BigInt(Math.min(Math.max(dt, 0), MAX_DT)) << 22n | BigInt(seq)).toString(16).padStart(16, "0");
5107
5691
  }
5108
5692
  _Snowflake.formatParts = formatParts;
5109
5693
  class Source {
@@ -5170,9 +5754,11 @@ let Snowflake;
5170
5754
  _Snowflake.getDate = getDate;
5171
5755
  })(Snowflake || (Snowflake = {}));
5172
5756
  //#endregion
5173
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/stream.ts
5757
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/stream.ts
5174
5758
  const trailingEvents = /* @__PURE__ */ new WeakSet();
5175
5759
  const LF = 10;
5760
+ const CR = 13;
5761
+ const CONCAT_SINK_RETAIN_BYTES = 1048576;
5176
5762
  async function* readJsonl(stream, signal) {
5177
5763
  const buffer = new ConcatSink();
5178
5764
  const source = abortableSource(stream, signal);
@@ -5193,9 +5779,19 @@ async function* readJsonl(stream, signal) {
5193
5779
  throw err;
5194
5780
  }
5195
5781
  }
5782
+ /**
5783
+ * Amortized byte accumulator for chunked stream readers.
5784
+ *
5785
+ * Holds the unconsumed tail of a stream in a single growing `Buffer` so that
5786
+ * appending N chunks costs O(total bytes) instead of re-copying the whole
5787
+ * prefix per chunk. Backs {@link readLines}, {@link readJsonl} and
5788
+ * {@link readSseEvents}; also usable directly when a reader needs its own
5789
+ * framing loop (see `consume` and `flush`).
5790
+ */
5196
5791
  var ConcatSink = class {
5197
5792
  #space;
5198
5793
  #length = 0;
5794
+ #skipLeadingLf = false;
5199
5795
  #ensureCapacity(size) {
5200
5796
  const space = this.#space;
5201
5797
  if (space && space.length >= size) return space;
@@ -5205,6 +5801,20 @@ var ConcatSink = class {
5205
5801
  this.#space = next;
5206
5802
  return next;
5207
5803
  }
5804
+ /** Drop or right-size an oversized buffer after the buffered length shrank. */
5805
+ #releaseCapacity() {
5806
+ const space = this.#space;
5807
+ if (!space || space.length <= CONCAT_SINK_RETAIN_BYTES) return;
5808
+ const length = this.#length;
5809
+ if (length === 0) {
5810
+ this.#space = void 0;
5811
+ return;
5812
+ }
5813
+ if (length * 4 > space.length) return;
5814
+ const next = Buffer.allocUnsafe(length);
5815
+ space.copy(next, 0, 0, length);
5816
+ this.#space = next;
5817
+ }
5208
5818
  append(chunk) {
5209
5819
  const n = chunk.length;
5210
5820
  if (!n) return;
@@ -5213,24 +5823,44 @@ var ConcatSink = class {
5213
5823
  this.#length += n;
5214
5824
  }
5215
5825
  reset(chunk) {
5826
+ this.#length = 0;
5827
+ this.#releaseCapacity();
5216
5828
  const n = chunk.length;
5217
- if (!n) {
5218
- this.#length = 0;
5219
- return;
5220
- }
5829
+ if (!n) return;
5221
5830
  this.#ensureCapacity(n).set(chunk, 0);
5222
5831
  this.#length = n;
5223
5832
  }
5224
5833
  get isEmpty() {
5225
5834
  return this.#length === 0;
5226
5835
  }
5836
+ /**
5837
+ * The buffered bytes as a live view — invalidated by the next `append`,
5838
+ * `reset` or `consume`.
5839
+ */
5227
5840
  flush() {
5228
5841
  if (!this.#length) return void 0;
5229
5842
  return this.#space.subarray(0, this.#length);
5230
5843
  }
5844
+ /** Drop the first `count` buffered bytes, keeping the remainder. */
5845
+ consume(count) {
5846
+ if (count <= 0) return;
5847
+ if (count >= this.#length) this.#length = 0;
5848
+ else {
5849
+ this.#space.copyWithin(0, count, this.#length);
5850
+ this.#length -= count;
5851
+ }
5852
+ this.#releaseCapacity();
5853
+ }
5231
5854
  clear() {
5232
5855
  this.#length = 0;
5856
+ this.#releaseCapacity();
5233
5857
  }
5858
+ /**
5859
+ * Append a chunk and yield each complete LF-delimited line.
5860
+ *
5861
+ * Yielded lines are owned snapshots. Unlike {@link flush}, they remain
5862
+ * valid after this sink or the input chunk is mutated.
5863
+ */
5234
5864
  *appendAndFlushLines(chunk) {
5235
5865
  let pos = 0;
5236
5866
  while (pos < chunk.length) {
@@ -5241,30 +5871,37 @@ var ConcatSink = class {
5241
5871
  }
5242
5872
  const suffix = chunk.subarray(pos, nl);
5243
5873
  pos = nl + 1;
5244
- if (this.isEmpty) yield suffix;
5874
+ if (this.isEmpty) yield new Uint8Array(suffix);
5245
5875
  else {
5246
5876
  this.append(suffix);
5247
5877
  const payload = this.flush();
5248
5878
  if (payload) {
5249
- yield payload;
5879
+ yield new Uint8Array(payload);
5250
5880
  this.clear();
5251
5881
  }
5252
5882
  }
5253
5883
  }
5254
5884
  }
5255
5885
  appendAndFlushText(chunk, decoder) {
5256
- const lastNewline = chunk.lastIndexOf(LF);
5257
- if (lastNewline === -1) {
5258
- this.append(chunk);
5886
+ let start = 0;
5887
+ if (this.#skipLeadingLf) {
5888
+ if (chunk.length === 0) return void 0;
5889
+ this.#skipLeadingLf = false;
5890
+ if (chunk[0] === LF) start = 1;
5891
+ }
5892
+ const lastLineEnd = Math.max(chunk.lastIndexOf(LF), chunk.lastIndexOf(CR));
5893
+ if (lastLineEnd < start) {
5894
+ if (start < chunk.length) this.append(chunk.subarray(start));
5259
5895
  return;
5260
5896
  }
5261
- const completeEnd = lastNewline + 1;
5897
+ const completeEnd = lastLineEnd + 1;
5898
+ this.#skipLeadingLf = chunk[lastLineEnd] === CR && completeEnd === chunk.length;
5262
5899
  let text;
5263
5900
  if (this.isEmpty) {
5264
- const complete = completeEnd === chunk.length ? chunk : chunk.subarray(0, completeEnd);
5901
+ const complete = start === 0 && completeEnd === chunk.length ? chunk : chunk.subarray(start, completeEnd);
5265
5902
  text = decoder.decode(complete);
5266
5903
  } else {
5267
- this.append(completeEnd === chunk.length ? chunk : chunk.subarray(0, completeEnd));
5904
+ this.append(chunk.subarray(start, completeEnd));
5268
5905
  text = decoder.decode(this.flush());
5269
5906
  this.clear();
5270
5907
  }
@@ -5294,13 +5931,13 @@ var ConcatSink = class {
5294
5931
  const { values, error, read, done } = Bun.JSONL.parseChunk(space, 0, total);
5295
5932
  if (values.length > 0) yield* values;
5296
5933
  if (error) throw error;
5297
- if (done) {
5298
- this.#length = 0;
5299
- return;
5934
+ if (done) this.#length = 0;
5935
+ else {
5936
+ const rem = total - read;
5937
+ if (rem < total) space.copyWithin(0, read, total);
5938
+ this.#length = rem;
5300
5939
  }
5301
- const rem = total - read;
5302
- if (rem < total) space.copyWithin(0, read, total);
5303
- this.#length = rem;
5940
+ this.#releaseCapacity();
5304
5941
  }
5305
5942
  };
5306
5943
  function notifySseEventObserver(observer, event) {
@@ -5315,51 +5952,104 @@ function isRecoverableTrailingJson(data) {
5315
5952
  const recovered = parseStreamingJson(data);
5316
5953
  return typeof recovered === "object" && recovered !== null;
5317
5954
  }
5318
- async function* readSseJson(stream, signal, onEvent) {
5319
- for await (const sse of readSseEvents(stream, signal)) {
5955
+ /**
5956
+ * Shared `data:`-line framing for {@link readSseJson} and
5957
+ * {@link readSseJsonOrText}: skips empty events, stops at the OpenAI `[DONE]`
5958
+ * sentinel (reporting it through `onDone`), notifies the diagnostic observer,
5959
+ * and treats a container-shaped stream tail as a clean end of iteration.
5960
+ */
5961
+ async function* readSseFrames(stream, signal, onEvent, onDone) {
5962
+ for await (const sse of readSseEvents(stream, signal, onEvent ? { captureRaw: true } : void 0)) {
5320
5963
  const isTrailing = trailingEvents.has(sse);
5321
5964
  notifySseEventObserver(onEvent, sse);
5322
5965
  const data = sse.data;
5323
5966
  if (data === "" || data === "[DONE]") {
5324
- if (data === "[DONE]") return;
5967
+ if (data === "[DONE]") {
5968
+ onDone?.();
5969
+ return;
5970
+ }
5325
5971
  continue;
5326
5972
  }
5327
5973
  try {
5328
- yield JSON.parse(data);
5974
+ yield {
5975
+ ok: true,
5976
+ value: JSON.parse(data)
5977
+ };
5329
5978
  } catch (err) {
5330
5979
  if (err instanceof SyntaxError && isTrailing && isRecoverableTrailingJson(data)) return;
5980
+ if (err instanceof SyntaxError) {
5981
+ yield {
5982
+ ok: false,
5983
+ raw: data,
5984
+ error: err
5985
+ };
5986
+ continue;
5987
+ }
5331
5988
  throw err;
5332
5989
  }
5333
5990
  }
5334
5991
  }
5992
+ async function* readSseJson(stream, signal, onEvent) {
5993
+ for await (const frame of readSseFrames(stream, signal, onEvent)) {
5994
+ if (!frame.ok) throw frame.error;
5995
+ yield frame.value;
5996
+ }
5997
+ }
5998
+ /**
5999
+ * Like {@link readSseJson}, but a `data:` frame that is not valid JSON is yielded
6000
+ * as its raw text instead of raising a `SyntaxError`. Cut-off container-shaped
6001
+ * stream tails stay recoverable, exactly as they are in {@link readSseJson}.
6002
+ *
6003
+ * Consumers that only understand objects must treat a `string` yield as a
6004
+ * transport-level failure (for example a `429 Too Many Requests` or an HTML
6005
+ * throttle page from a reverse proxy that already committed to the stream). This
6006
+ * exists because `readSseJson`'s baseline consumers span unrelated transports
6007
+ * whose error handling a text yield would subtly change; new call sites opt in.
6008
+ *
6009
+ * Note that the text lane is only the frames `JSON.parse` *rejected*: a frame
6010
+ * carrying a JSON-encoded string (`data: "429 Too Many Requests"`) parses, so it
6011
+ * is yielded as that string and is indistinguishable from a rejected frame by
6012
+ * type alone. Consumers branching on `typeof === "string"` therefore see both,
6013
+ * which is the safe direction — each is classified as text rather than trusted as
6014
+ * an event object.
6015
+ */
6016
+ async function* readSseJsonOrText(stream, signal, onEvent, onDone) {
6017
+ for await (const frame of readSseFrames(stream, signal, onEvent, onDone)) if (!frame.ok) yield frame.raw;
6018
+ else yield frame.value;
6019
+ }
5335
6020
  const SSE_DECODER = new TextDecoder("utf-8");
6021
+ /**
6022
+ * `raw` of every event dispatched with capture off. Shared instead of one
6023
+ * fresh array per event; frozen so an in-place mutation throws rather than
6024
+ * leaking lines into every other event (consumers copy or reassign `raw`).
6025
+ */
6026
+ const EMPTY_RAW = Object.freeze([]);
5336
6027
  function flushSseEvent(state) {
5337
6028
  if (state.event === null && state.data === null && state.id === void 0 && state.retry === void 0) {
5338
- state.raw = [];
6029
+ if (state.raw !== null) state.raw = [];
5339
6030
  return null;
5340
6031
  }
5341
6032
  const event = {
5342
6033
  event: state.event,
5343
6034
  data: state.data ?? "",
5344
- raw: state.raw
6035
+ raw: state.raw ?? EMPTY_RAW
5345
6036
  };
5346
6037
  if (state.id !== void 0) event.id = state.id;
5347
6038
  if (state.retry !== void 0) event.retry = state.retry;
5348
6039
  state.event = null;
5349
6040
  state.data = null;
5350
- state.raw = [];
6041
+ if (state.raw !== null) state.raw = [];
5351
6042
  state.id = void 0;
5352
6043
  state.retry = void 0;
5353
6044
  return event;
5354
6045
  }
5355
6046
  function pushSseLine(line, state) {
5356
- if (line.charCodeAt(line.length - 1) === 13) line = line.slice(0, -1);
5357
6047
  if (line.length === 0) return flushSseEvent(state);
5358
6048
  if (line.charCodeAt(0) === 58) {
5359
- state.raw.push(line);
6049
+ state.raw?.push(line);
5360
6050
  return null;
5361
6051
  }
5362
- state.raw.push(line);
6052
+ state.raw?.push(line);
5363
6053
  const colon = line.indexOf(":");
5364
6054
  const fieldName = colon === -1 ? line : line.slice(0, colon);
5365
6055
  let value = colon === -1 ? "" : line.slice(colon + 1);
@@ -5389,31 +6079,12 @@ function pushSseLine(line, state) {
5389
6079
  }
5390
6080
  return null;
5391
6081
  }
5392
- /**
5393
- * Stream raw Server-Sent Events from an HTTP response body.
5394
- *
5395
- * Yields one `ServerSentEvent` per blank-line dispatch. The consumer is
5396
- * responsible for parsing `data` (e.g. JSON, plain text, error envelope).
5397
- * Use `readSseJson` instead when every event is a single `data:` JSON object
5398
- * and you don't need access to the `event:` field.
5399
- *
5400
- * Internally backed by a Buffer-based reader (`ConcatSink`) that batches all
5401
- * complete lines in each source chunk into one UTF-8 decode.
5402
- *
5403
- * @example
5404
- * ```ts
5405
- * for await (const sse of readSseEvents(response.body!)) {
5406
- * if (sse.event === "ping") continue;
5407
- * const obj = JSON.parse(sse.data);
5408
- * }
5409
- * ```
5410
- */
5411
- async function* readSseEvents(stream, signal) {
6082
+ async function* readSseEvents(stream, signal, options) {
5412
6083
  const lineBuffer = new ConcatSink();
5413
6084
  const state = {
5414
6085
  event: null,
5415
6086
  data: null,
5416
- raw: []
6087
+ raw: options?.captureRaw === true ? [] : null
5417
6088
  };
5418
6089
  const source = abortableSource(stream, signal);
5419
6090
  try {
@@ -5422,10 +6093,16 @@ async function* readSseEvents(stream, signal) {
5422
6093
  if (text === void 0) continue;
5423
6094
  let start = 0;
5424
6095
  while (start < text.length) {
5425
- const newline = text.indexOf("\n", start);
5426
- const event = pushSseLine(text.slice(start, newline), state);
6096
+ let lineEnd = start;
6097
+ while (lineEnd < text.length) {
6098
+ const code = text.charCodeAt(lineEnd);
6099
+ if (code === LF || code === CR) break;
6100
+ lineEnd++;
6101
+ }
6102
+ const event = pushSseLine(text.slice(start, lineEnd), state);
5427
6103
  if (event) yield event;
5428
- start = newline + 1;
6104
+ if (text.charCodeAt(lineEnd) === CR && text.charCodeAt(lineEnd + 1) === LF) lineEnd++;
6105
+ start = lineEnd + 1;
5429
6106
  }
5430
6107
  }
5431
6108
  if (!lineBuffer.isEmpty) {
@@ -5450,7 +6127,14 @@ async function* readSseEvents(stream, signal) {
5450
6127
  }
5451
6128
  }
5452
6129
  //#endregion
5453
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/temp.ts
6130
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/temp.ts
6131
+ /**
6132
+ * Owned temporary directory, removed via `remove()` or `using`/`await using`.
6133
+ *
6134
+ * Prefixes follow `mkdtemp`: `"@name-"` creates under the OS temp dir, any other
6135
+ * prefix is a path (a bare `"name-"` lands in the process cwd), and no prefix
6136
+ * means `"@pi-temp-"`.
6137
+ */
5454
6138
  var TempDir = class TempDir {
5455
6139
  #path;
5456
6140
  constructor(path) {
@@ -5513,7 +6197,6 @@ const kRetryableRemoveErrorCodes = /* @__PURE__ */ new Set([
5513
6197
  "EPERM",
5514
6198
  "ENOTEMPTY"
5515
6199
  ]);
5516
- const kSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
5517
6200
  /** Removes a path recursively, retrying transient Windows deletion failures. */
5518
6201
  async function removeWithRetries(target) {
5519
6202
  for (let attempt = 0;; attempt++) try {
@@ -5521,6 +6204,7 @@ async function removeWithRetries(target) {
5521
6204
  return;
5522
6205
  } catch (err) {
5523
6206
  if (!shouldRetryRemove(err, attempt)) throw err;
6207
+ if (attempt === 0) Bun.gc(true);
5524
6208
  await Bun.sleep(kRemoveRetryDelayMs);
5525
6209
  }
5526
6210
  }
@@ -5530,7 +6214,8 @@ function removeSyncWithRetries(target) {
5530
6214
  return;
5531
6215
  } catch (err) {
5532
6216
  if (!shouldRetryRemove(err, attempt)) throw err;
5533
- sleepSync(kRemoveRetryDelayMs);
6217
+ if (attempt === 0) Bun.gc(true);
6218
+ Bun.sleepSync(kRemoveRetryDelayMs);
5534
6219
  }
5535
6220
  }
5536
6221
  function shouldRetryRemove(err, attempt) {
@@ -5539,15 +6224,8 @@ function shouldRetryRemove(err, attempt) {
5539
6224
  function isRetryableRemoveError(err) {
5540
6225
  return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string" && kRetryableRemoveErrorCodes.has(err.code);
5541
6226
  }
5542
- function sleepSync(ms) {
5543
- if ("sleepSync" in Bun && typeof Bun.sleepSync === "function") {
5544
- Bun.sleepSync(ms);
5545
- return;
5546
- }
5547
- Atomics.wait(kSleepBuffer, 0, 0, ms);
5548
- }
5549
6227
  //#endregion
5550
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/tls-fetch.ts
6228
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/tls-fetch.ts
5551
6229
  /**
5552
6230
  * `NODE_EXTRA_CA_CERTS` shim for Bun's `fetch`.
5553
6231
  *
@@ -5656,7 +6334,7 @@ function wrapFetchForExtraCa(fetchImpl) {
5656
6334
  }, fetchImpl.preconnect ? { preconnect: fetchImpl.preconnect } : {}, { [EXTRA_CA_FETCH_MARKER]: true });
5657
6335
  }
5658
6336
  //#endregion
5659
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.1.17/node_modules/@oh-my-pi/pi-utils/src/index.ts
6337
+ //#region node_modules/.pnpm/@oh-my-pi+pi-utils@18.12.6/node_modules/@oh-my-pi/pi-utils/src/index.ts
5660
6338
  function isPlainObject(val) {
5661
6339
  return Object.getPrototypeOf(val) === Object.prototype || Array.isArray(val);
5662
6340
  }
@@ -5667,5 +6345,41 @@ function structuredCloneJSON(value) {
5667
6345
  } catch {}
5668
6346
  return JSON.parse(JSON.stringify(value));
5669
6347
  }
6348
+ /**
6349
+ * Deep-copies a JSON-shaped tree (arrays and plain or null-prototype objects)
6350
+ * so later mutation of the source can never reach the copy. Strings and other
6351
+ * primitives are immutable and shared, so the cost is O(containers) instead of
6352
+ * `structuredClone`'s O(bytes) — the difference matters for hot paths that
6353
+ * re-copy large string payloads (e.g. streamed tool-call arguments) per delta.
6354
+ *
6355
+ * Copies own enumerable string keys only. Any other object (class instance,
6356
+ * Date, Map, inherited-prototype object) is copied with {@link structuredCloneJSON}.
6357
+ * Not cycle-safe: callers MUST pass acyclic data, as any JSON-serializable value is.
6358
+ */
6359
+ function cloneJsonTree(value) {
6360
+ return cloneJsonNode(value);
6361
+ }
6362
+ function isJsonRecord(value) {
6363
+ const proto = Object.getPrototypeOf(value);
6364
+ return proto === Object.prototype || proto === null;
6365
+ }
6366
+ function cloneJsonNode(value) {
6367
+ if (value === null || typeof value !== "object") return value;
6368
+ if (Array.isArray(value)) return value.map(cloneJsonNode);
6369
+ if (!isJsonRecord(value)) return structuredCloneJSON(value);
6370
+ const out = {};
6371
+ for (const key in value) {
6372
+ if (!Object.hasOwn(value, key)) continue;
6373
+ const child = cloneJsonNode(value[key]);
6374
+ if (key === "__proto__") Object.defineProperty(out, key, {
6375
+ value: child,
6376
+ enumerable: true,
6377
+ writable: true,
6378
+ configurable: true
6379
+ });
6380
+ else out[key] = child;
6381
+ }
6382
+ return out;
6383
+ }
5670
6384
  //#endregion
5671
- export { isEnoent as $, formatCount as A, USER_AGENT as B, warn as C, installBunShim as D, CookieMap as E, $flag as F, getDocsRsCacheDir as G, getAgentDir as H, $pickenv as I, getLogsDir as J, getDocumentConversionCacheDir as K, isBunTestRuntime as L, truncate as M, $env as N, formatAge as O, $envpos as P, hasFsCode as Q, isTerminalHeadless as R, time as S, Cookie as T, getAutoQaDbPath as U, VERSION as V, getDebugLogPath as W, getRemoteDir as X, getPuppeteerDir as Y, getToolsDir as Z, parseStreamingJson as _, readSseEvents as a, findOnPath as at, debug as b, combineSignals as c, $which as d, isEnotdir as et, register as f, parseJsonWithRepair as g, classifyJsonPrefix as h, readJsonl as i, untilAborted as it, formatNumber as j, formatBytes as k, exec as l, readImageMetadata as m, wrapFetchForExtraCa as n, peekFile as nt, readSseJson as o, parseImageMetadata as p, getInstallId as q, TempDir as r, once as rt, Snowflake as s, structuredCloneJSON as t, isProbablyBinary as tt, render as u, parseStreamingJsonThrottled as v, $ as w, error as x, parseFrontmatter as y, isWsl as z };
6385
+ export { getAutoQaDbPath as $, FFIType as A, formatNumber as B, parseStreamingJson as C, error as D, debug as E, CookieMap as F, $pickenv as G, $env as H, installBunShim as I, stripWindowsExtendedLengthPathPrefix as J, isTerminalHeadless as K, formatAge as L, ptr as M, $ as N, time as O, Cookie as P, getAgentDir as Q, formatBytes as R, parseJsonWithRepair as S, parseFrontmatter as T, $envpos as U, truncate as V, $flag as W, USER_AGENT as X, APP_URL as Y, VERSION as Z, parseImageMetadata as _, findOnPath as _t, readJsonl as a, getLogsDir as at, takeLoopPhaseAttribution as b, readSseJsonOrText as c, getToolsDir as ct, exec as d, isEnoent as dt, getCustomThemesDir as et, render as f, isEnotdir as ft, base64ImageDataFailureReason as g, untilAborted as gt, register as h, once as ht, TempDir as i, getInstallId as it, dlopen as j, warn as k, Snowflake as l, isBunTestRuntime as lt, $which as m, peekFile as mt, structuredCloneJSON as n, getDocsRsCacheDir as nt, readSseEvents as o, getPuppeteerDir as ot, LRUCache as p, isProbablyBinary as pt, isWsl as q, wrapFetchForExtraCa as r, getDocumentConversionCacheDir as rt, readSseJson as s, getRemoteDir as st, cloneJsonTree as t, getDebugLogPath as tt, combineSignals as u, hasFsCode as ut, readImageMetadata as v, parseStreamingJsonThrottled as w, classifyJsonPrefix as x, resetLoopPhaseWindow as y, formatCount as z };