@wrongstack/core 0.265.1 → 0.267.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 (69) hide show
  1. package/dist/{agent-bridge-DrkBxszZ.d.ts → agent-bridge-STJ3JwwK.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-DM2pP-B6.d.ts → agent-subagent-runner-CzPGP3jA.d.ts} +25 -7
  3. package/dist/{brain-BXd_61kQ.d.ts → brain-Cdg77tVN.d.ts} +73 -1
  4. package/dist/{compactor-B8pOf45Y.d.ts → compactor-iMZ84CXq.d.ts} +19 -1
  5. package/dist/{config-BMCj_XDs.d.ts → config-Du3pYYln.d.ts} +54 -3
  6. package/dist/{context-MRk5PhNv.d.ts → context-dT5Ueund.d.ts} +65 -1
  7. package/dist/coordination/index.d.ts +17 -17
  8. package/dist/coordination/index.js +138 -114
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +25 -25
  11. package/dist/defaults/index.js +1729 -781
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +15 -15
  14. package/dist/execution/index.js +1119 -229
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +1 -1
  17. package/dist/extension/index.d.ts +6 -6
  18. package/dist/{goal-preamble-DvHDSKSe.d.ts → goal-preamble-SulMTowG.d.ts} +28 -11
  19. package/dist/{goal-store-DtLMySNb.d.ts → goal-store-CABDwdFE.d.ts} +1 -1
  20. package/dist/{index-CEDeNodM.d.ts → index-Bms0m4oy.d.ts} +5 -5
  21. package/dist/{index-B-ch8K9C.d.ts → index-DtCVWel4.d.ts} +8 -8
  22. package/dist/index-IEuxQd-E.d.ts +82 -0
  23. package/dist/index.d.ts +118 -45
  24. package/dist/index.js +3083 -1602
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/index.d.ts +6 -6
  27. package/dist/infrastructure/index.js +72 -1
  28. package/dist/infrastructure/index.js.map +1 -1
  29. package/dist/kernel/index.d.ts +9 -9
  30. package/dist/kernel/index.js.map +1 -1
  31. package/dist/{mcp-servers-2x4w6Jn9.d.ts → mcp-servers-C2cBTxUR.d.ts} +3 -3
  32. package/dist/models/index.d.ts +5 -5
  33. package/dist/models/index.js +30 -1
  34. package/dist/models/index.js.map +1 -1
  35. package/dist/{models-registry-DmJlKuNp.d.ts → models-registry-BqGZNJQ-.d.ts} +1 -1
  36. package/dist/{multi-agent-coordinator-DyCkCZnU.d.ts → multi-agent-coordinator-B8R43uPz.d.ts} +1 -1
  37. package/dist/{null-fleet-bus-CG9QY2aP.d.ts → null-fleet-bus-CnXa5oTH.d.ts} +14 -9
  38. package/dist/observability/index.d.ts +2 -2
  39. package/dist/{parallel-eternal-engine-Jw9uhEoT.d.ts → parallel-eternal-engine-DdNnw9BQ.d.ts} +11 -9
  40. package/dist/{path-resolver-Dy2ej-gE.d.ts → path-resolver-COIMLCQL.d.ts} +3 -3
  41. package/dist/{permission-B9SB45lp.d.ts → permission-B75JAi3-.d.ts} +1 -1
  42. package/dist/{permission-policy-CkjSXabK.d.ts → permission-policy-DlR9eJAM.d.ts} +2 -2
  43. package/dist/{pipeline-DPDxH_7m.d.ts → pipeline-BfD2k1rT.d.ts} +2 -2
  44. package/dist/{plan-templates-CzD9GnAU.d.ts → plan-templates-DSIKCXZN.d.ts} +5 -5
  45. package/dist/{llm-selector-C0tfTCUe.d.ts → provider-model-resolve-BNRsNuJx.d.ts} +40 -3
  46. package/dist/{provider-runner-DMa70ODu.d.ts → provider-runner-CX7iIvox.d.ts} +3 -3
  47. package/dist/{retry-policy-CN0khdlj.d.ts → retry-policy-BilV1ujH.d.ts} +1 -1
  48. package/dist/sdd/index.d.ts +8 -8
  49. package/dist/sdd/index.js +12 -12
  50. package/dist/sdd/index.js.map +1 -1
  51. package/dist/{secret-vault-B2yw84VT.d.ts → secret-vault-gkvEZZfE.d.ts} +2 -2
  52. package/dist/security/index.d.ts +5 -67
  53. package/dist/security/index.js +96 -76
  54. package/dist/security/index.js.map +1 -1
  55. package/dist/{selector-CzHh_igB.d.ts → selector-Bc7eWtT3.d.ts} +1 -1
  56. package/dist/{session-event-bridge-BUI6Jf-4.d.ts → session-event-bridge-D-araDEz.d.ts} +1 -1
  57. package/dist/{session-reader-CMgdMSRP.d.ts → session-reader-D7Dapswh.d.ts} +1 -1
  58. package/dist/storage/index.d.ts +11 -11
  59. package/dist/storage/index.js +81 -84
  60. package/dist/storage/index.js.map +1 -1
  61. package/dist/tools/index.d.ts +4 -2
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/types/index.d.ts +19 -19
  64. package/dist/types/index.js +1265 -400
  65. package/dist/types/index.js.map +1 -1
  66. package/dist/utils/index.d.ts +454 -406
  67. package/dist/utils/index.js +2191 -1201
  68. package/dist/utils/index.js.map +1 -1
  69. package/package.json +1 -1
@@ -1,16 +1,23 @@
1
1
  import { randomBytes, createHash } from 'crypto';
2
2
  import * as fs from 'fs/promises';
3
- import * as path2 from 'path';
3
+ import * as path3 from 'path';
4
4
  import { isAbsolute, resolve } from 'path';
5
- import * as os from 'os';
6
5
  import * as dns from 'dns/promises';
7
6
  import * as net from 'net';
7
+ import * as os from 'os';
8
8
 
9
- // src/utils/atomic-write.ts
9
+ // src/utils/assert-never.ts
10
+ function assertNever(x, message) {
11
+ const err = new Error(
12
+ message ?? `Unhandled case: ${JSON.stringify(x)}`
13
+ );
14
+ err.name = "AssertNeverError";
15
+ throw err;
16
+ }
10
17
  async function atomicWrite(targetPath, content, opts = {}) {
11
- const dir = path2.dirname(targetPath);
18
+ const dir = path3.dirname(targetPath);
12
19
  await fs.mkdir(dir, { recursive: true });
13
- const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
20
+ const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
14
21
  try {
15
22
  if (typeof content === "string") {
16
23
  await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
@@ -49,9 +56,9 @@ async function ensureDir(dir) {
49
56
  await fs.mkdir(dir, { recursive: true });
50
57
  }
51
58
  async function withFileLock(targetPath, fn, opts = {}) {
52
- const dir = path2.dirname(targetPath);
59
+ const dir = path3.dirname(targetPath);
53
60
  await fs.mkdir(dir, { recursive: true });
54
- const lockPath = path2.join(dir, `.${path2.basename(targetPath)}.lock`);
61
+ const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
55
62
  const timeoutMs = opts.timeoutMs ?? 5e3;
56
63
  const staleMs = opts.staleMs ?? 3e4;
57
64
  const started = Date.now();
@@ -80,7 +87,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
80
87
  if (Date.now() - started >= timeoutMs) {
81
88
  throw new Error(`Timed out waiting for file lock: ${targetPath}`);
82
89
  }
83
- await new Promise((resolve3) => setTimeout(resolve3, 25));
90
+ await new Promise((resolve4) => setTimeout(resolve4, 25));
84
91
  }
85
92
  }
86
93
  try {
@@ -114,150 +121,105 @@ async function renameWithRetry(from, to) {
114
121
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
115
122
  throw err;
116
123
  }
117
- await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
124
+ await new Promise((resolve4) => setTimeout(resolve4, delays[i]));
118
125
  }
119
126
  }
120
127
  throw lastErr;
121
128
  }
122
129
 
123
- // src/utils/error.ts
124
- function toErrorMessage(err) {
125
- return err instanceof Error ? err.message : String(err);
126
- }
127
-
128
- // src/utils/safe-json.ts
129
- function safeParse(input, maxBytes = 5e6) {
130
- if (input.length > maxBytes) {
131
- return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
132
- }
133
- try {
134
- return { ok: true, value: JSON.parse(input) };
135
- } catch (err) {
136
- return {
137
- ok: false,
138
- error: toErrorMessage(err)
139
- };
130
+ // src/utils/child-env.ts
131
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
132
+ "PATH",
133
+ "HOME",
134
+ "USER",
135
+ "USERNAME",
136
+ "LOGNAME",
137
+ "SHELL",
138
+ "LANG",
139
+ "LC_ALL",
140
+ "LC_CTYPE",
141
+ "TERM",
142
+ "TZ",
143
+ "TMPDIR",
144
+ "TEMP",
145
+ "TMP",
146
+ "PWD",
147
+ "OLDPWD",
148
+ "COMSPEC",
149
+ "SYSTEMROOT",
150
+ "SYSTEMDRIVE",
151
+ "WINDIR",
152
+ "PROGRAMFILES",
153
+ "PROGRAMFILES(X86)",
154
+ "PROGRAMDATA",
155
+ "APPDATA",
156
+ "LOCALAPPDATA",
157
+ "USERPROFILE",
158
+ "PUBLIC",
159
+ "PATHEXT"
160
+ ]);
161
+ var SECRET_NAME_PARTS = [
162
+ "TOKEN",
163
+ "SECRET",
164
+ "PASSWORD",
165
+ "PASSWD",
166
+ "AUTH",
167
+ "CRED",
168
+ "BEARER",
169
+ "COOKIE",
170
+ "PRIVATE"
171
+ ];
172
+ function looksSecret(name) {
173
+ const upper = name.toUpperCase();
174
+ for (const p of SECRET_NAME_PARTS) {
175
+ if (upper.includes(p)) return true;
140
176
  }
141
- }
142
- function safeStringify(value, pretty = false) {
143
- const seen = /* @__PURE__ */ new WeakSet();
144
- const replacer = (_k, v) => {
145
- if (typeof v === "bigint") return v.toString();
146
- if (v instanceof Error) {
147
- return { name: v.name, message: v.message, stack: v.stack };
148
- }
149
- if (typeof v === "object" && v !== null) {
150
- if (seen.has(v)) return "[Circular]";
151
- seen.add(v);
152
- }
153
- return v;
154
- };
155
- try {
156
- return JSON.stringify(value, replacer, pretty ? 2 : void 0) ?? "null";
157
- } catch (err) {
158
- return JSON.stringify({
159
- __serialization_error: toErrorMessage(err)
160
- });
177
+ if (/(?:^|_)KEY(?:$|_|S$)/i.test(upper)) return true;
178
+ if (/API[_-]?KEY/i.test(upper)) return true;
179
+ if (/ACCESS[_-]?KEY/i.test(upper)) return true;
180
+ if (/SESSION[_-]?ID/i.test(upper) === false && /SESSION/i.test(upper)) {
181
+ return true;
161
182
  }
183
+ return false;
162
184
  }
163
- function sanitizeJsonString(s) {
164
- let out = s.trim();
165
- out = stripSingleLineComments(out);
166
- out = out.replace(/,(\s*[}\]])/g, "$1");
167
- out = escapeControlCharsInStrings(out);
168
- try {
169
- JSON.parse(out);
170
- return out;
171
- } catch {
172
- return null;
185
+ function buildChildEnv(optsOrSessionId) {
186
+ const opts = typeof optsOrSessionId === "string" ? { sessionId: optsOrSessionId } : optsOrSessionId ?? {};
187
+ const hasOwn = Object.hasOwn(process.env, "WRONGSTACK_CHILD_ENV_PASSTHROUGH");
188
+ const legacyHasOwn = Object.hasOwn(process.env, "WRONGSTACK_BASH_ENV_PASSTHROUGH");
189
+ const passthrough = hasOwn && process.env["WRONGSTACK_CHILD_ENV_PASSTHROUGH"] === "1" || legacyHasOwn && process.env["WRONGSTACK_BASH_ENV_PASSTHROUGH"] === "1";
190
+ if (passthrough && !process.env["CI"]) {
191
+ console.warn(
192
+ "[agent] WARNING: WRONGSTACK_*_ENV_PASSTHROUGH=1 is active \u2014\n all parent env vars (including API keys) forwarded to child processes.\n Do not use on shared or multi-tenant systems."
193
+ );
173
194
  }
174
- }
175
- function escapeControlCharsInStrings(s) {
176
- let inString = false;
177
- let out = "";
178
- for (let i = 0; i < s.length; i++) {
179
- const c = s.charAt(i);
180
- if (c === '"' && (i === 0 || s[i - 1] !== "\\")) {
181
- inString = !inString;
182
- out += c;
195
+ const out = {};
196
+ const nodeEnvDefaulted = process.env["WRONGSTACK_NODE_ENV_DEFAULTED"] === "1";
197
+ for (const [k, v] of Object.entries(process.env)) {
198
+ if (v === void 0) continue;
199
+ if (nodeEnvDefaulted && (k === "NODE_ENV" || k === "WRONGSTACK_NODE_ENV_DEFAULTED")) continue;
200
+ if (passthrough) {
201
+ out[k] = v;
183
202
  continue;
184
203
  }
185
- const code = c.charCodeAt(0);
186
- if (inString && code < 32) {
187
- switch (c) {
188
- case "\n":
189
- out += "\\n";
190
- break;
191
- case "\r":
192
- out += "\\r";
193
- break;
194
- case " ":
195
- out += "\\t";
196
- break;
197
- case "\b":
198
- out += "\\b";
199
- break;
200
- case "\f":
201
- out += "\\f";
202
- break;
203
- default:
204
- out += `\\u${code.toString(16).padStart(4, "0")}`;
205
- }
204
+ const upper = k.toUpperCase();
205
+ if (ALLOWED_KEYS.has(upper)) {
206
+ out[k] = v;
206
207
  continue;
207
208
  }
208
- out += c;
209
- }
210
- return out;
211
- }
212
- function stripSingleLineComments(s) {
213
- let inString = false;
214
- const chars = [];
215
- let i = 0;
216
- while (i < s.length) {
217
- const c = s.charAt(i);
218
- if (c === '"' && (i === 0 || s.charAt(i - 1) !== "\\")) {
219
- inString = !inString;
220
- chars.push(c);
221
- } else if (c === "/" && s.charAt(i + 1) === "/" && !inString) {
222
- while (i < s.length && s.charAt(i) !== "\n") i++;
223
- } else {
224
- chars.push(c);
209
+ if (looksSecret(upper)) continue;
210
+ if (upper.startsWith("NODE_") || upper.startsWith("NPM_") || upper.startsWith("PNPM_") || upper.startsWith("YARN_") || upper.startsWith("GIT_") || upper.startsWith("CI") || upper.startsWith("XDG_") || // Our own non-secret knobs (WRONGSTACK_HOME, WRONGSTACK_SESSION_ID, …).
211
+ // Secrets never live in WRONGSTACK_* env vars (they're in the encrypted
212
+ // vault). Forwarding keeps child wstack processes — e.g. ones spawned
213
+ // by the test suite — inside the same redirected global root.
214
+ upper.startsWith("WRONGSTACK_") || upper === "EDITOR" || upper === "VISUAL" || upper === "PAGER") {
215
+ out[k] = v;
225
216
  }
226
- i++;
227
217
  }
228
- return chars.join("");
229
- }
230
-
231
- // src/utils/newline-normalize.ts
232
- function detectNewlineStyle(text) {
233
- let lf = 0;
234
- let crlf = 0;
235
- let cr = 0;
236
- for (let i = 0; i < text.length; i++) {
237
- const c = text.charCodeAt(i);
238
- if (c === 13) {
239
- if (text.charCodeAt(i + 1) === 10) {
240
- crlf++;
241
- i++;
242
- } else {
243
- cr++;
244
- }
245
- } else if (c === 10) {
246
- lf++;
247
- }
218
+ if (opts.extra) {
219
+ Object.assign(out, opts.extra);
248
220
  }
249
- if (crlf > lf && crlf > cr) return "crlf";
250
- if (cr > lf && cr > crlf) return "cr";
251
- return "lf";
252
- }
253
- function toStyle(text, style) {
254
- const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
255
- if (style === "lf") return normalized;
256
- if (style === "crlf") return normalized.replace(/\n/g, "\r\n");
257
- return normalized.replace(/\n/g, "\r");
258
- }
259
- function normalizeToLf(text) {
260
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
221
+ if (opts.sessionId) out["WRONGSTACK_SESSION_ID"] = opts.sessionId;
222
+ return out;
261
223
  }
262
224
 
263
225
  // src/utils/term.ts
@@ -360,220 +322,388 @@ function stripAnsi(s) {
360
322
  ""
361
323
  );
362
324
  }
363
-
364
- // src/utils/todos-format.ts
365
- function formatTodosList(todos) {
366
- if (todos.length === 0) return "No todos.";
367
- const lines = [];
368
- const done = todos.filter((t) => t.status === "completed").length;
369
- lines.push(color.dim(`Todos (${done}/${todos.length} done):`));
370
- todos.forEach((t, i) => {
371
- const mark = t.status === "completed" ? color.green("[x]") : t.status === "in_progress" ? color.yellow("[~]") : color.dim("[ ]");
372
- const text = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
373
- const label = t.status === "completed" ? color.dim(text) : text;
374
- lines.push(` ${color.dim(String(i + 1).padStart(2))}. ${mark} ${label}`);
325
+ var MAX_TOOL_CALLS = 80;
326
+ var MAX_FACTS = 40;
327
+ var MAX_ERRORS = 20;
328
+ var MAX_DIGEST_CHARS = 4e3;
329
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "replace", "patch"]);
330
+ var READ_TOOLS = /* @__PURE__ */ new Set(["read", "grep", "glob", "ls", "tree"]);
331
+ function createContextEvidenceState() {
332
+ return {
333
+ sessionGoals: [],
334
+ implicitFacts: [],
335
+ activeErrors: [],
336
+ toolCalls: [],
337
+ fileGraph: {},
338
+ repeatedReads: [],
339
+ updatedAt: Date.now()
340
+ };
341
+ }
342
+ function recordUserIntentEvidence(ctx, text) {
343
+ const intent = normalizeWhitespace(text).slice(0, 700);
344
+ if (!intent) return;
345
+ const state = ensureEvidence(ctx);
346
+ state.currentIntent = { text: intent, updatedAt: Date.now() };
347
+ if (state.sessionGoals.length === 0 || isGoalish(intent)) {
348
+ pushUniqueBounded(state.sessionGoals, intent, 8);
349
+ }
350
+ state.updatedAt = Date.now();
351
+ }
352
+ function recordToolOutputEvidence(ctx, input) {
353
+ const state = ensureEvidence(ctx);
354
+ const files = extractFiles(ctx, input.toolName, input.input, input.content);
355
+ const symbols = extractSymbols(input.content, input.input);
356
+ const commands = extractCommands(input.toolName, input.input);
357
+ const errors = extractErrors(input.content);
358
+ const summary = summarizeToolOutput(input.toolName, input.input, input.content, {
359
+ files,
360
+ errors,
361
+ ok: input.ok
375
362
  });
376
- return lines.join("\n");
363
+ const metadata = {
364
+ toolUseId: input.toolUseId,
365
+ toolName: input.toolName,
366
+ ok: input.ok,
367
+ inputSummary: summarizeInput(input.input),
368
+ summary,
369
+ files,
370
+ symbols,
371
+ commands,
372
+ errors,
373
+ status: "seen",
374
+ referenceCount: 0,
375
+ seenAt: Date.now(),
376
+ outputBytes: input.outputBytes,
377
+ outputTokens: input.outputTokens,
378
+ outputLines: input.outputLines
379
+ };
380
+ state.toolCalls.push(metadata);
381
+ if (state.toolCalls.length > MAX_TOOL_CALLS) {
382
+ state.toolCalls.splice(0, state.toolCalls.length - MAX_TOOL_CALLS);
383
+ }
384
+ updateFileGraph(state, metadata);
385
+ updateRepeatedReadSignals(state, metadata);
386
+ if (errors.length > 0) {
387
+ for (const err of errors) pushUniqueBounded(state.activeErrors, err, MAX_ERRORS);
388
+ }
389
+ const fact = implicitFactFor(metadata);
390
+ if (fact) pushUniqueBounded(state.implicitFacts, fact, MAX_FACTS);
391
+ state.updatedAt = Date.now();
392
+ return metadata;
393
+ }
394
+ function markAssistantReferencedEvidence(ctx, text) {
395
+ const state = ensureEvidence(ctx);
396
+ const haystack = text.toLowerCase();
397
+ if (!haystack.trim()) return;
398
+ for (const tool of state.toolCalls) {
399
+ if (!metadataReferencedByText(tool, haystack)) continue;
400
+ tool.status = "referenced";
401
+ tool.referenceCount++;
402
+ tool.referencedAt = Date.now();
403
+ for (const file of tool.files) {
404
+ const node = state.fileGraph[file];
405
+ if (node) node.referenced = true;
406
+ }
407
+ }
408
+ state.updatedAt = Date.now();
377
409
  }
378
-
379
- // src/utils/task-format.ts
380
- function computeTaskItemProgress(tasks) {
381
- let completed = 0;
382
- let pending = 0;
383
- let inProgress = 0;
384
- let blocked = 0;
385
- let failed = 0;
386
- let review = 0;
387
- let estimatedHours = 0;
388
- const actualHours = 0;
389
- for (const t of tasks) {
390
- switch (t.status) {
391
- case "completed":
392
- completed++;
393
- break;
394
- case "pending":
395
- pending++;
396
- break;
397
- case "in_progress":
398
- inProgress++;
399
- break;
400
- case "blocked":
401
- blocked++;
402
- break;
403
- case "failed":
404
- failed++;
405
- break;
406
- case "review":
407
- review++;
408
- break;
410
+ function buildContextEvidenceDigest(ctx) {
411
+ const state = ensureEvidence(ctx);
412
+ const lines = [];
413
+ if (state.currentIntent?.text) {
414
+ lines.push(`intent: ${state.currentIntent.text}`);
415
+ }
416
+ const goals = state.sessionGoals.slice(-3);
417
+ if (goals.length > 0) {
418
+ lines.push("session_goals:");
419
+ for (const goal of goals) lines.push(`- ${goal}`);
420
+ }
421
+ const activeErrors = state.activeErrors.slice(-5);
422
+ if (activeErrors.length > 0) {
423
+ lines.push("active_errors:");
424
+ for (const err of activeErrors) lines.push(`- ${err}`);
425
+ }
426
+ const files = Object.values(state.fileGraph).sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.path.localeCompare(b.path)).slice(0, 12);
427
+ if (files.length > 0) {
428
+ lines.push("dependency_graph:");
429
+ for (const file of files) {
430
+ const actions = [
431
+ file.reads > 0 ? `read ${file.reads}x` : "",
432
+ file.writes > 0 ? `write ${file.writes}x` : ""
433
+ ].filter(Boolean).join(", ");
434
+ const refs = file.referenced ? "; referenced by assistant" : "";
435
+ const via = file.lastToolUseId ? `; last via ${file.lastToolUseId}` : "";
436
+ lines.push(`- ${file.path} (${actions || "seen"}${refs}${via})`);
409
437
  }
410
- estimatedHours += t.estimateHours ?? 0;
411
438
  }
412
- return {
413
- total: tasks.length,
414
- pending,
415
- inProgress,
416
- blocked,
417
- failed,
418
- review,
419
- completed,
420
- percentComplete: tasks.length > 0 ? Math.round(completed / tasks.length * 100) : 0,
421
- estimatedHours,
422
- actualHours
439
+ const referenced = state.toolCalls.filter((tool) => tool.status === "referenced").slice(-10);
440
+ const recentSeen = state.toolCalls.filter((tool) => tool.status === "seen").slice(-5);
441
+ const trail = [...referenced, ...recentSeen];
442
+ if (trail.length > 0) {
443
+ lines.push("tool_trail:");
444
+ for (const tool of trail) {
445
+ const size = tool.outputTokens ? `; ~${tool.outputTokens} tokens` : "";
446
+ const filesText = tool.files.length > 0 ? `; files=${tool.files.slice(0, 4).join(", ")}` : "";
447
+ const symbolsText = tool.symbols.length > 0 ? `; symbols=${tool.symbols.slice(0, 4).join(", ")}` : "";
448
+ lines.push(
449
+ `- ${tool.toolUseId} ${tool.toolName} ${tool.status}: ${tool.summary}${filesText}${symbolsText}${size}`
450
+ );
451
+ }
452
+ }
453
+ const facts = state.implicitFacts.slice(-8);
454
+ if (facts.length > 0) {
455
+ lines.push("implicit_facts:");
456
+ for (const fact of facts) lines.push(`- ${fact}`);
457
+ }
458
+ const digest = lines.join("\n");
459
+ if (digest.length <= MAX_DIGEST_CHARS) return digest;
460
+ return `${digest.slice(0, MAX_DIGEST_CHARS)}... [+${digest.length - MAX_DIGEST_CHARS} chars]`;
461
+ }
462
+ function repeatedReadPressure(ctx) {
463
+ return ensureEvidence(ctx).repeatedReads.reduce((max, item) => Math.max(max, item.count), 0);
464
+ }
465
+ function ensureEvidence(ctx) {
466
+ if (!ctx.contextEvidence) {
467
+ ctx.contextEvidence = createContextEvidenceState();
468
+ }
469
+ return ctx.contextEvidence;
470
+ }
471
+ function isGoalish(text) {
472
+ return /\b(goal|objective|task|need|want|implement|fix|improve|refactor|add|remove|hedef|amac|istiyorum|gerekiyor|iyilestir|duzelt|ekle|kaldir)\b/i.test(text);
473
+ }
474
+ function normalizeWhitespace(text) {
475
+ return text.replace(/\s+/g, " ").trim();
476
+ }
477
+ function pushUniqueBounded(list, value, max) {
478
+ const normalized = normalizeWhitespace(value);
479
+ if (!normalized) return;
480
+ const existing = list.findIndex((item) => item.toLowerCase() === normalized.toLowerCase());
481
+ if (existing >= 0) list.splice(existing, 1);
482
+ list.push(normalized);
483
+ if (list.length > max) list.splice(0, list.length - max);
484
+ }
485
+ function extractFiles(ctx, toolName, input, content) {
486
+ const out = /* @__PURE__ */ new Set();
487
+ for (const value of inputPathValues(input)) addPath(ctx, out, value);
488
+ if (toolName === "grep" || toolName === "glob" || toolName === "bash") {
489
+ const re = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
490
+ for (const match of content.matchAll(re)) addPath(ctx, out, match[0]);
491
+ }
492
+ return [...out].slice(0, 30);
493
+ }
494
+ function inputPathValues(input) {
495
+ const values = [];
496
+ const visit = (value, key) => {
497
+ if (typeof value === "string") {
498
+ if (key && /^(path|file|files|fromFile|toFile|dir|cwd)$/i.test(key)) values.push(value);
499
+ return;
500
+ }
501
+ if (Array.isArray(value)) {
502
+ for (const item of value) visit(item, key);
503
+ return;
504
+ }
505
+ if (!value || typeof value !== "object") return;
506
+ for (const [k, v] of Object.entries(value)) visit(v, k);
423
507
  };
508
+ visit(input);
509
+ return values;
424
510
  }
425
- var STATUS_ICON = {
426
- pending: "\u25CB",
427
- in_progress: "\u25D0",
428
- blocked: "\u2298",
429
- failed: "\u2717",
430
- review: "\u25D1",
431
- completed: "\u25CF"
432
- };
433
- var PRIORITY_ICON = {
434
- critical: "\u{1F534}",
435
- high: "\u{1F7E0}",
436
- medium: "\u{1F7E1}",
437
- low: "\u{1F7E2}"
438
- };
439
- var TYPE_ICON = {
440
- feature: "\u26A1",
441
- bugfix: "\u{1F41B}",
442
- refactor: "\u267B\uFE0F",
443
- docs: "\u{1F4DD}",
444
- test: "\u{1F9EA}",
445
- chore: "\u{1F527}"
446
- };
447
- function formatTaskProgress(tasks) {
448
- const p = computeTaskItemProgress(tasks);
449
- if (p.total === 0) return "No tasks.";
450
- const barWidth = 24;
451
- const filled = Math.round(p.percentComplete / 100 * barWidth);
452
- const empty = barWidth - filled;
453
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
454
- return [
455
- `${color.bold("Tasks")} [${bar}] ${p.percentComplete}%`,
456
- ` ${color.green("\u25CF")} ${p.completed} done \u2502 ${color.yellow("\u25D0")} ${p.inProgress} active \u2502 ${color.dim("\u25CB")} ${p.pending} pending \u2502 \u2298 ${p.blocked} blocked \u2502 \u2717 ${p.failed} failed`,
457
- p.estimatedHours > 0 ? ` ${color.dim(`est. ${p.estimatedHours}h`)}` : ""
458
- ].filter(Boolean).join("\n");
511
+ function addPath(ctx, out, raw) {
512
+ const clean = raw.trim().replace(/^["'`]+|["'`),;:]+$/g, "");
513
+ if (!clean || clean.length > 260) return;
514
+ let normalized = clean.replace(/\\/g, "/");
515
+ try {
516
+ const abs = path3.isAbsolute(clean) ? path3.resolve(clean) : null;
517
+ if (abs) {
518
+ const rel = path3.relative(ctx.projectRoot, abs);
519
+ if (!rel.startsWith("..") && !path3.isAbsolute(rel)) {
520
+ normalized = rel.replace(/\\/g, "/");
521
+ }
522
+ }
523
+ } catch {
524
+ }
525
+ if (normalized.length > 0) out.add(normalized);
526
+ }
527
+ function extractSymbols(content, input) {
528
+ const out = /* @__PURE__ */ new Set();
529
+ const patterns = [
530
+ /\b(?:function|class|interface|type|enum|const|let|var|def|fn|struct)\s+([A-Za-z_$][\w$]*)/g,
531
+ /\b(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g
532
+ ];
533
+ for (const re of patterns) {
534
+ for (const match of content.matchAll(re)) {
535
+ if (match[1]) out.add(match[1]);
536
+ if (out.size >= 30) break;
537
+ }
538
+ }
539
+ const pattern = input && typeof input === "object" ? input["pattern"] : void 0;
540
+ if (typeof pattern === "string" && /^[A-Za-z_$][\w$]*$/.test(pattern)) {
541
+ out.add(pattern);
542
+ }
543
+ return [...out].slice(0, 30);
459
544
  }
460
- function formatTaskList(tasks) {
461
- if (tasks.length === 0) return "No tasks.";
462
- const order = ["in_progress", "blocked", "review", "pending", "failed", "completed"];
463
- const groups = /* @__PURE__ */ new Map();
464
- for (const t of tasks) {
465
- const list = groups.get(t.status) ?? [];
466
- list.push(t);
467
- groups.set(t.status, list);
545
+ function extractCommands(toolName, input) {
546
+ if (toolName !== "bash" && toolName !== "exec" && toolName !== "shell") return [];
547
+ if (!input || typeof input !== "object") return [];
548
+ const command = input["command"];
549
+ if (typeof command !== "string") return [];
550
+ return [command.slice(0, 220)];
551
+ }
552
+ function extractErrors(content) {
553
+ const lines = content.split(/\r?\n/);
554
+ const errors = [];
555
+ for (const line of lines) {
556
+ if (!/\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm|typeerror|syntaxerror)\b/i.test(line)) continue;
557
+ errors.push(normalizeWhitespace(line).slice(0, 260));
558
+ if (errors.length >= 5) break;
559
+ }
560
+ return errors;
561
+ }
562
+ function summarizeInput(input) {
563
+ if (!input || typeof input !== "object") return void 0;
564
+ const obj = input;
565
+ const parts = [];
566
+ for (const key of ["path", "file", "pattern", "glob", "command"]) {
567
+ const value = obj[key];
568
+ if (typeof value === "string") parts.push(`${key}=${value.slice(0, 160)}`);
569
+ }
570
+ return parts.length > 0 ? parts.join(", ") : void 0;
571
+ }
572
+ function summarizeToolOutput(toolName, input, content, opts) {
573
+ if (!opts.ok && opts.errors.length > 0) return opts.errors[0] ?? `${toolName} failed`;
574
+ if (toolName === "read" && opts.files[0]) return `read ${opts.files[0]}`;
575
+ if (toolName === "grep") {
576
+ const pattern = input && typeof input === "object" ? input["pattern"] : void 0;
577
+ return `searched ${typeof pattern === "string" ? pattern : "pattern"} (${opts.files.length} file hint(s))`;
578
+ }
579
+ if ((toolName === "edit" || toolName === "write") && opts.files[0]) {
580
+ return `${toolName === "write" ? "wrote" : "edited"} ${opts.files[0]}`;
581
+ }
582
+ const firstLine = normalizeWhitespace(content.split(/\r?\n/).find((line) => line.trim()) ?? "");
583
+ return firstLine ? firstLine.slice(0, 220) : `${toolName} returned no text`;
584
+ }
585
+ function updateFileGraph(state, metadata) {
586
+ const writes = WRITE_TOOLS.has(metadata.toolName) ? 1 : 0;
587
+ const reads = writes === 0 && (READ_TOOLS.has(metadata.toolName) || metadata.files.length > 0) ? 1 : 0;
588
+ for (const file of metadata.files) {
589
+ const existing = state.fileGraph[file] ?? {
590
+ path: file,
591
+ reads: 0,
592
+ writes: 0,
593
+ tools: [],
594
+ referenced: false
595
+ };
596
+ existing.reads += reads;
597
+ existing.writes += writes;
598
+ existing.lastToolUseId = metadata.toolUseId;
599
+ pushUniqueBounded(existing.tools, `${metadata.toolName}#${metadata.toolUseId}`, 8);
600
+ state.fileGraph[file] = existing;
468
601
  }
469
- const lines = [];
470
- lines.push(color.dim(`Tasks (${tasks.length} total):`));
471
- for (const status of order) {
472
- const group = groups.get(status);
473
- if (!group || group.length === 0) continue;
474
- const icon = STATUS_ICON[status];
475
- lines.push(` ${icon} ${status.toUpperCase()} (${group.length})`);
476
- for (const t of group) {
477
- const prio = PRIORITY_ICON[t.priority];
478
- const type = TYPE_ICON[t.type];
479
- const deps = t.dependsOn && t.dependsOn.length > 0 ? ` ${color.dim("\u2190")} ${color.dim(t.dependsOn.map((d) => d.slice(0, 8)).join(", "))}` : "";
480
- const who = t.assignee ? ` ${color.dim(`@${t.assignee}`)}` : "";
481
- const hrs = t.estimateHours ? ` ${color.dim(`${t.estimateHours}h`)}` : "";
482
- lines.push(` ${type} ${prio} ${t.title}${deps}${who}${hrs}`);
602
+ }
603
+ function updateRepeatedReadSignals(state, metadata) {
604
+ if (metadata.toolName !== "read" || metadata.files.length === 0) {
605
+ state.lastReadPath = void 0;
606
+ return;
607
+ }
608
+ const file = metadata.files[0];
609
+ if (state.lastReadPath === file) {
610
+ const existing = state.repeatedReads.find((item) => item.file === file);
611
+ if (existing) {
612
+ existing.count++;
613
+ existing.lastToolUseId = metadata.toolUseId;
614
+ } else {
615
+ state.repeatedReads.push({ file, count: 2, lastToolUseId: metadata.toolUseId });
483
616
  }
617
+ if (state.repeatedReads.length > 10) state.repeatedReads.shift();
484
618
  }
485
- return lines.join("\n");
619
+ state.lastReadPath = file;
486
620
  }
487
-
488
- // src/utils/string.ts
489
- function truncate(s, max) {
490
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
621
+ function implicitFactFor(metadata) {
622
+ if (metadata.errors.length > 0) return `${metadata.toolName}#${metadata.toolUseId} exposed error: ${metadata.errors[0]}`;
623
+ if (metadata.toolName === "read" && metadata.files[0]) {
624
+ const size = metadata.outputLines ? ` (${metadata.outputLines} line(s) returned)` : "";
625
+ return `read ${metadata.files[0]}${size}`;
626
+ }
627
+ if ((metadata.toolName === "edit" || metadata.toolName === "write") && metadata.files[0]) {
628
+ return `${metadata.toolName} changed ${metadata.files[0]}`;
629
+ }
630
+ if (metadata.status === "referenced") return `${metadata.toolName}#${metadata.toolUseId} was referenced`;
631
+ return void 0;
491
632
  }
492
-
493
- // src/utils/expect-defined.ts
494
- function expectDefined(value, label) {
495
- if (value === null || value === void 0) {
496
- const err = new Error(label ? `Expected ${label} to be defined` : "Expected value to be defined");
497
- err.name = "ExpectDefinedError";
498
- throw err;
633
+ function metadataReferencedByText(metadata, haystack) {
634
+ for (const file of metadata.files) {
635
+ const f = file.toLowerCase();
636
+ const base = path3.basename(file).toLowerCase();
637
+ if (f && haystack.includes(f)) return true;
638
+ if (base && haystack.includes(base)) return true;
499
639
  }
500
- return value;
640
+ for (const symbol of metadata.symbols) {
641
+ if (symbol.length >= 3 && haystack.includes(symbol.toLowerCase())) return true;
642
+ }
643
+ for (const err of metadata.errors) {
644
+ const head = err.slice(0, 80).toLowerCase();
645
+ if (head.length >= 12 && haystack.includes(head)) return true;
646
+ }
647
+ return false;
501
648
  }
502
649
 
503
- // src/utils/glob-match.ts
504
- function escapeRegex(s) {
505
- return s.replace(/[.+^${}()|\\]/g, "\\$&");
650
+ // src/utils/deep-merge.ts
651
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
652
+ "__proto__",
653
+ "constructor",
654
+ "prototype",
655
+ "__defineGetter__",
656
+ "__defineSetter__",
657
+ "__lookupGetter__",
658
+ "__lookupSetter__"
659
+ ]);
660
+ function isPrimitiveArray(a) {
661
+ return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
506
662
  }
507
- var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
508
- var CACHE_MAX_SIZE = 2e3;
509
- function getCachedGlob(pattern) {
510
- const cached = COMPILED_GLOB_CACHE.get(pattern);
511
- if (cached) return cached;
512
- if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
513
- const keys = [...COMPILED_GLOB_CACHE.keys()];
514
- for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
515
- COMPILED_GLOB_CACHE.delete(expectDefined(keys[i]));
663
+ function deepMerge(base, patch, options = {}) {
664
+ const {
665
+ conflictResolution = "prefer-patch",
666
+ arrayMode = "replace",
667
+ protectProto = true,
668
+ onNonPrimitiveArrayReplace
669
+ } = options;
670
+ if (typeof base !== "object" || base === null) {
671
+ return conflictResolution === "prefer-patch" ? patch : base;
672
+ }
673
+ if (typeof patch !== "object" || patch === null) {
674
+ return conflictResolution === "prefer-patch" ? patch : base;
675
+ }
676
+ if (Array.isArray(base) && Array.isArray(patch)) {
677
+ if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
678
+ return [.../* @__PURE__ */ new Set([...base, ...patch])];
516
679
  }
680
+ return conflictResolution === "prefer-patch" ? patch : base;
517
681
  }
518
- const re = compileGlob(pattern);
519
- COMPILED_GLOB_CACHE.set(pattern, re);
520
- return re;
521
- }
522
- var MAX_GLOB_PATTERN_LEN = 1024;
523
- function compileGlob(pattern) {
524
- if (pattern.length > MAX_GLOB_PATTERN_LEN) {
525
- throw new Error(`Glob pattern exceeds ${MAX_GLOB_PATTERN_LEN} characters`);
682
+ if (Array.isArray(base) || Array.isArray(patch)) {
683
+ return conflictResolution === "prefer-patch" ? patch : base;
526
684
  }
527
- let i = 0;
528
- let re = "^";
529
- while (i < pattern.length) {
530
- const c = pattern[i];
531
- if (c === "*") {
532
- if (pattern[i + 1] === "*") {
533
- re += ".*";
534
- i += 2;
535
- if (pattern[i] === "/") i++;
536
- } else {
537
- re += "[^/]*";
538
- i++;
539
- }
540
- } else if (c === "?") {
541
- re += "[^/]";
542
- i++;
543
- } else if (c === "[") {
544
- let cls = "[";
545
- i++;
546
- if (pattern[i] === "!" || pattern[i] === "^") {
547
- cls += "^";
548
- i++;
685
+ const baseObj = base;
686
+ const patchObj = patch;
687
+ const out = { ...baseObj };
688
+ for (const [k, v] of Object.entries(patchObj)) {
689
+ if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
690
+ const existing = out[k];
691
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
692
+ out[k] = deepMerge(existing, v, options);
693
+ } else if (Array.isArray(v) && Array.isArray(existing)) {
694
+ if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
695
+ onNonPrimitiveArrayReplace(k, existing.length, v.length);
549
696
  }
550
- while (i < pattern.length && pattern[i] !== "]") {
551
- const ch = pattern[i] ?? "";
552
- if (ch === "\\") {
553
- cls += "\\\\";
554
- } else if (ch === "]" || ch === "^") {
555
- cls += `\\${ch}`;
556
- } else {
557
- cls += ch;
558
- }
559
- i++;
697
+ out[k] = deepMerge(existing, v, options);
698
+ } else if (v !== void 0) {
699
+ if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
700
+ const existingLen = Array.isArray(existing) ? existing.length : 0;
701
+ onNonPrimitiveArrayReplace(k, existingLen, v.length);
560
702
  }
561
- cls += "]";
562
- re += cls;
563
- i++;
564
- } else {
565
- re += escapeRegex(c ?? "");
566
- i++;
703
+ out[k] = v;
567
704
  }
568
705
  }
569
- re += "$";
570
- return new RegExp(re);
571
- }
572
- function matchGlob(pattern, input) {
573
- return getCachedGlob(pattern).test(input);
574
- }
575
- function matchAny(patterns, input) {
576
- return patterns.some((p) => matchGlob(p, input));
706
+ return out;
577
707
  }
578
708
 
579
709
  // src/utils/diff.ts
@@ -727,577 +857,437 @@ function unifiedDiff(oldText, newText, opts = {}) {
727
857
  }
728
858
  return out;
729
859
  }
730
- function projectHash(absRoot) {
731
- return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
732
- }
733
- function projectSlug(absRoot) {
734
- const base = slugify(path2.basename(absRoot));
735
- const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
736
- return `${base}-${hash}`;
737
- }
738
- function slugify(name) {
739
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
740
- }
741
- function wstackGlobalRoot() {
742
- const fromEnv = process.env["WRONGSTACK_HOME"];
743
- if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
744
- return path2.join(os.homedir(), ".wrongstack");
745
- }
746
- function resolveWstackPaths(opts) {
747
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
748
- const hash = projectHash(opts.projectRoot);
749
- const slug = projectSlug(opts.projectRoot);
750
- const projectDir = path2.join(globalRoot, "projects", slug);
751
- return {
752
- globalRoot,
753
- configDir: globalRoot,
754
- globalConfig: path2.join(globalRoot, "config.json"),
755
- secretsKey: path2.join(globalRoot, ".key"),
756
- globalMemory: path2.join(globalRoot, "memory.md"),
757
- globalSkills: path2.join(globalRoot, "skills"),
758
- globalPrompts: path2.join(globalRoot, "prompts"),
759
- cacheDir: path2.join(globalRoot, "cache"),
760
- modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
761
- modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
762
- historyFile: path2.join(globalRoot, "history"),
763
- logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
764
- projectDir,
765
- projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
766
- projectMemory: path2.join(projectDir, "memory.md"),
767
- projectSessions: path2.join(projectDir, "sessions"),
768
- projectTrust: path2.join(projectDir, "trust.json"),
769
- projectMeta: path2.join(projectDir, "meta.json"),
770
- projectLocalConfig: path2.join(projectDir, "config.local.json"),
771
- inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
772
- inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
773
- inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
774
- inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
775
- projectHash: hash,
776
- projectSlug: slug,
777
- projectGoal: path2.join(projectDir, "goal.json"),
778
- projectSpecs: path2.join(projectDir, "specs"),
779
- projectTaskGraphs: path2.join(projectDir, "task-graphs"),
780
- projectSddSession: path2.join(projectDir, "sdd-session.json"),
781
- projectPlan: path2.join(projectDir, "plan.json"),
782
- projectAutophase: path2.join(projectDir, "autophase"),
783
- syncConfig: path2.join(globalRoot, "sync.json"),
784
- projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
785
- };
860
+
861
+ // src/utils/error.ts
862
+ function toErrorMessage(err) {
863
+ return err instanceof Error ? err.message : String(err);
786
864
  }
787
865
 
788
- // src/utils/child-env.ts
789
- var ALLOWED_KEYS = /* @__PURE__ */ new Set([
790
- "PATH",
791
- "HOME",
792
- "USER",
793
- "USERNAME",
794
- "LOGNAME",
795
- "SHELL",
796
- "LANG",
797
- "LC_ALL",
798
- "LC_CTYPE",
799
- "TERM",
800
- "TZ",
801
- "TMPDIR",
802
- "TEMP",
803
- "TMP",
804
- "PWD",
805
- "OLDPWD",
806
- "COMSPEC",
807
- "SYSTEMROOT",
808
- "SYSTEMDRIVE",
809
- "WINDIR",
810
- "PROGRAMFILES",
811
- "PROGRAMFILES(X86)",
812
- "PROGRAMDATA",
813
- "APPDATA",
814
- "LOCALAPPDATA",
815
- "USERPROFILE",
816
- "PUBLIC",
817
- "PATHEXT"
818
- ]);
819
- var SECRET_NAME_PARTS = [
820
- "TOKEN",
821
- "SECRET",
822
- "PASSWORD",
823
- "PASSWD",
824
- "AUTH",
825
- "CRED",
826
- "BEARER",
827
- "COOKIE",
828
- "PRIVATE"
829
- ];
830
- function looksSecret(name) {
831
- const upper = name.toUpperCase();
832
- for (const p of SECRET_NAME_PARTS) {
833
- if (upper.includes(p)) return true;
866
+ // src/utils/expect-defined.ts
867
+ function expectDefined(value, label) {
868
+ if (value === null || value === void 0) {
869
+ const err = new Error(label ? `Expected ${label} to be defined` : "Expected value to be defined");
870
+ err.name = "ExpectDefinedError";
871
+ throw err;
834
872
  }
835
- if (/(?:^|_)KEY(?:$|_|S$)/i.test(upper)) return true;
836
- if (/API[_-]?KEY/i.test(upper)) return true;
837
- if (/ACCESS[_-]?KEY/i.test(upper)) return true;
838
- if (/SESSION[_-]?ID/i.test(upper) === false && /SESSION/i.test(upper)) {
839
- return true;
873
+ return value;
874
+ }
875
+ var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
876
+ var IS_WINDOWS = process.platform === "win32";
877
+ var SEP = IS_WINDOWS ? "\\" : "/";
878
+ function isGlob(p) {
879
+ for (const c of p) {
880
+ if (GLOB_CHARS.has(c)) return true;
840
881
  }
841
882
  return false;
842
883
  }
843
- function buildChildEnv(optsOrSessionId) {
844
- const opts = typeof optsOrSessionId === "string" ? { sessionId: optsOrSessionId } : optsOrSessionId ?? {};
845
- const hasOwn = Object.hasOwn(process.env, "WRONGSTACK_CHILD_ENV_PASSTHROUGH");
846
- const legacyHasOwn = Object.hasOwn(process.env, "WRONGSTACK_BASH_ENV_PASSTHROUGH");
847
- const passthrough = hasOwn && process.env["WRONGSTACK_CHILD_ENV_PASSTHROUGH"] === "1" || legacyHasOwn && process.env["WRONGSTACK_BASH_ENV_PASSTHROUGH"] === "1";
848
- if (passthrough && !process.env["CI"]) {
849
- console.warn(
850
- "[agent] WARNING: WRONGSTACK_*_ENV_PASSTHROUGH=1 is active \u2014\n all parent env vars (including API keys) forwarded to child processes.\n Do not use on shared or multi-tenant systems."
851
- );
884
+ function globToRegex(pat) {
885
+ let i = 0;
886
+ let re = "^";
887
+ while (i < pat.length) {
888
+ const c = expectDefined(pat[i]);
889
+ if (c === "*") {
890
+ if (pat[i + 1] === "*") {
891
+ re += ".*";
892
+ i += 2;
893
+ if (pat[i] === "/") i++;
894
+ } else {
895
+ re += "[^/\\\\]*";
896
+ i++;
897
+ }
898
+ } else if (c === "?") {
899
+ re += "[^/\\\\]";
900
+ i++;
901
+ } else if (c === "[") {
902
+ let cls = "[";
903
+ i++;
904
+ if (pat[i] === "!" || pat[i] === "^") {
905
+ cls += "^";
906
+ i++;
907
+ }
908
+ while (i < pat.length && pat[i] !== "]") {
909
+ const ch = pat[i] ?? "";
910
+ if (ch === "\\") cls += "\\\\";
911
+ else if (ch === "]" || ch === "^") cls += `\\${ch}`;
912
+ else cls += ch;
913
+ i++;
914
+ }
915
+ cls += "]";
916
+ re += cls;
917
+ i++;
918
+ } else {
919
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
920
+ i++;
921
+ }
852
922
  }
853
- const out = {};
854
- const nodeEnvDefaulted = process.env["WRONGSTACK_NODE_ENV_DEFAULTED"] === "1";
855
- for (const [k, v] of Object.entries(process.env)) {
856
- if (v === void 0) continue;
857
- if (nodeEnvDefaulted && (k === "NODE_ENV" || k === "WRONGSTACK_NODE_ENV_DEFAULTED")) continue;
858
- if (passthrough) {
859
- out[k] = v;
860
- continue;
923
+ return new RegExp(re + "$");
924
+ }
925
+ function baseDir(pat) {
926
+ let i = pat.length - 1;
927
+ while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
928
+ const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
929
+ return cut < 0 ? "." : pat.slice(0, cut);
930
+ }
931
+ async function expandGlob(pattern) {
932
+ if (!isGlob(pattern)) return [pattern];
933
+ const results = /* @__PURE__ */ new Set();
934
+ const abs = isAbsolute(pattern);
935
+ const base = abs ? baseDir(pattern) : baseDir(pattern);
936
+ const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
937
+ async function walk2(dir, pat) {
938
+ let entries;
939
+ try {
940
+ entries = await fs.readdir(dir);
941
+ } catch {
942
+ return;
861
943
  }
862
- const upper = k.toUpperCase();
863
- if (ALLOWED_KEYS.has(upper)) {
864
- out[k] = v;
865
- continue;
944
+ const firstGlob = pat.search(/[*?[[]/);
945
+ if (firstGlob < 0) {
946
+ const re = globToRegex(pat);
947
+ for (const e of entries) {
948
+ if (re.test(e)) {
949
+ const full = `${dir}${SEP}${e}`;
950
+ results.add(abs ? resolve(full) : full);
951
+ }
952
+ }
953
+ return;
866
954
  }
867
- if (looksSecret(upper)) continue;
868
- if (upper.startsWith("NODE_") || upper.startsWith("NPM_") || upper.startsWith("PNPM_") || upper.startsWith("YARN_") || upper.startsWith("GIT_") || upper.startsWith("CI") || upper.startsWith("XDG_") || // Our own non-secret knobs (WRONGSTACK_HOME, WRONGSTACK_SESSION_ID, …).
869
- // Secrets never live in WRONGSTACK_* env vars (they're in the encrypted
870
- // vault). Forwarding keeps child wstack processes — e.g. ones spawned
871
- // by the test suite — inside the same redirected global root.
872
- upper.startsWith("WRONGSTACK_") || upper === "EDITOR" || upper === "VISUAL" || upper === "PAGER") {
873
- out[k] = v;
955
+ const before = pat.slice(0, firstGlob);
956
+ const rest = pat.slice(firstGlob);
957
+ if (before.endsWith("**")) {
958
+ await walk2(dir, rest);
959
+ for (const e of entries) {
960
+ const full = `${dir}${SEP}${e}`;
961
+ try {
962
+ const stat3 = await fs.stat(full);
963
+ if (stat3.isDirectory()) await walk2(full, rest);
964
+ } catch {
965
+ }
966
+ }
967
+ } else if (before === "") {
968
+ const re = globToRegex(rest);
969
+ for (const e of entries) {
970
+ if (re.test(e)) {
971
+ const full = `${dir}${SEP}${e}`;
972
+ results.add(abs ? resolve(full) : full);
973
+ }
974
+ }
975
+ } else {
976
+ const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
977
+ if (entries.includes(seg)) {
978
+ const full = `${dir}${SEP}${seg}`;
979
+ try {
980
+ const stat3 = await fs.stat(full);
981
+ if (stat3.isDirectory()) await walk2(full, rest);
982
+ } catch {
983
+ }
984
+ }
874
985
  }
875
986
  }
876
- if (opts.extra) {
877
- Object.assign(out, opts.extra);
878
- }
879
- if (opts.sessionId) out["WRONGSTACK_SESSION_ID"] = opts.sessionId;
880
- return out;
881
- }
882
-
883
- // src/utils/sleep.ts
884
- function sleep(ms) {
885
- return new Promise((resolve3) => setTimeout(resolve3, ms));
987
+ await walk2(base === "." ? "." : base, relPat);
988
+ return [...results];
886
989
  }
887
990
 
888
- // src/utils/assert-never.ts
889
- function assertNever(x, message) {
890
- const err = new Error(
891
- message ?? `Unhandled case: ${JSON.stringify(x)}`
892
- );
893
- err.name = "AssertNeverError";
894
- throw err;
991
+ // src/utils/glob-match.ts
992
+ function escapeRegex(s) {
993
+ return s.replace(/[.+^${}()|\\]/g, "\\$&");
895
994
  }
896
-
897
- // src/utils/deep-merge.ts
898
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
899
- "__proto__",
900
- "constructor",
901
- "prototype",
902
- "__defineGetter__",
903
- "__defineSetter__",
904
- "__lookupGetter__",
905
- "__lookupSetter__"
906
- ]);
907
- function isPrimitiveArray(a) {
908
- return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
995
+ var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
996
+ var CACHE_MAX_SIZE = 2e3;
997
+ function getCachedGlob(pattern) {
998
+ const cached = COMPILED_GLOB_CACHE.get(pattern);
999
+ if (cached) return cached;
1000
+ if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
1001
+ const keys = [...COMPILED_GLOB_CACHE.keys()];
1002
+ for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
1003
+ COMPILED_GLOB_CACHE.delete(expectDefined(keys[i]));
1004
+ }
1005
+ }
1006
+ const re = compileGlob(pattern);
1007
+ COMPILED_GLOB_CACHE.set(pattern, re);
1008
+ return re;
909
1009
  }
910
- function deepMerge(base, patch, options = {}) {
911
- const {
912
- conflictResolution = "prefer-patch",
913
- arrayMode = "replace",
914
- protectProto = true,
915
- onNonPrimitiveArrayReplace
916
- } = options;
917
- if (typeof base !== "object" || base === null) {
918
- return conflictResolution === "prefer-patch" ? patch : base;
919
- }
920
- if (typeof patch !== "object" || patch === null) {
921
- return conflictResolution === "prefer-patch" ? patch : base;
922
- }
923
- if (Array.isArray(base) && Array.isArray(patch)) {
924
- if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
925
- return [.../* @__PURE__ */ new Set([...base, ...patch])];
926
- }
927
- return conflictResolution === "prefer-patch" ? patch : base;
928
- }
929
- if (Array.isArray(base) || Array.isArray(patch)) {
930
- return conflictResolution === "prefer-patch" ? patch : base;
1010
+ var MAX_GLOB_PATTERN_LEN = 1024;
1011
+ function compileGlob(pattern) {
1012
+ if (pattern.length > MAX_GLOB_PATTERN_LEN) {
1013
+ throw new Error(`Glob pattern exceeds ${MAX_GLOB_PATTERN_LEN} characters`);
931
1014
  }
932
- const baseObj = base;
933
- const patchObj = patch;
934
- const out = { ...baseObj };
935
- for (const [k, v] of Object.entries(patchObj)) {
936
- if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
937
- const existing = out[k];
938
- if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
939
- out[k] = deepMerge(existing, v, options);
940
- } else if (Array.isArray(v) && Array.isArray(existing)) {
941
- if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
942
- onNonPrimitiveArrayReplace(k, existing.length, v.length);
943
- }
944
- out[k] = deepMerge(existing, v, options);
945
- } else if (v !== void 0) {
946
- if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
947
- const existingLen = Array.isArray(existing) ? existing.length : 0;
948
- onNonPrimitiveArrayReplace(k, existingLen, v.length);
1015
+ let i = 0;
1016
+ let re = "^";
1017
+ while (i < pattern.length) {
1018
+ const c = pattern[i];
1019
+ if (c === "*") {
1020
+ if (pattern[i + 1] === "*") {
1021
+ re += ".*";
1022
+ i += 2;
1023
+ if (pattern[i] === "/") i++;
1024
+ } else {
1025
+ re += "[^/]*";
1026
+ i++;
949
1027
  }
950
- out[k] = v;
951
- }
952
- }
953
- return out;
954
- }
955
-
956
- // src/utils/tool-output-serializer.ts
957
- function createToolOutputSerializer(opts = {}) {
958
- const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
959
- function serialize(value) {
960
- if (typeof value === "string") return value;
961
- if (value === null || value === void 0) return "";
962
- if (typeof value === "object") {
963
- if (Array.isArray(value)) return value.map(serialize).join("\n");
964
- if ("text" in value) {
965
- const t = value.text;
966
- return typeof t === "string" ? t : JSON.stringify(value, null, 2);
1028
+ } else if (c === "?") {
1029
+ re += "[^/]";
1030
+ i++;
1031
+ } else if (c === "[") {
1032
+ let cls = "[";
1033
+ i++;
1034
+ if (pattern[i] === "!" || pattern[i] === "^") {
1035
+ cls += "^";
1036
+ i++;
967
1037
  }
968
- try {
969
- return JSON.stringify(value, null, 2);
970
- } catch {
971
- return String(value);
1038
+ while (i < pattern.length && pattern[i] !== "]") {
1039
+ const ch = pattern[i] ?? "";
1040
+ if (ch === "\\") {
1041
+ cls += "\\\\";
1042
+ } else if (ch === "]" || ch === "^") {
1043
+ cls += `\\${ch}`;
1044
+ } else {
1045
+ cls += ch;
1046
+ }
1047
+ i++;
972
1048
  }
1049
+ cls += "]";
1050
+ re += cls;
1051
+ i++;
1052
+ } else {
1053
+ re += escapeRegex(c ?? "");
1054
+ i++;
973
1055
  }
974
- return String(value);
975
- }
976
- function enforceCap(text, remainingBudget) {
977
- if (remainingBudget <= 0) {
978
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
979
- }
980
- const textBytes = Buffer.byteLength(text, "utf8");
981
- if (textBytes <= remainingBudget) {
982
- return { text, newBudget: remainingBudget - textBytes };
983
- }
984
- const marker = `
985
- \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
986
- `;
987
- const markerBytes = Buffer.byteLength(marker, "utf8");
988
- const available = remainingBudget - markerBytes;
989
- if (available <= 0) {
990
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
991
- }
992
- const half = Math.floor(available / 2);
993
- const first = text.slice(0, half);
994
- const second = text.slice(text.length - half);
995
- return { text: `${first}${marker}${second}`, newBudget: 0 };
996
1056
  }
997
- return { serialize, enforceCap, capBytes };
1057
+ re += "$";
1058
+ return new RegExp(re);
998
1059
  }
999
-
1000
- // src/utils/token-estimate.ts
1001
- var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
1002
- var CAL_ALPHA = 0.3;
1003
- var CALIBRATION_GLOBAL_KEY = "__global__";
1004
- var _cals = /* @__PURE__ */ new Map();
1005
- function calState(key) {
1006
- let state = _cals.get(key);
1007
- if (!state) {
1008
- state = { ratio: 1, count: 0, prevEst: 0 };
1009
- _cals.set(key, state);
1010
- }
1011
- return state;
1060
+ function matchGlob(pattern, input) {
1061
+ return getCachedGlob(pattern).test(input);
1012
1062
  }
1013
- var MIN_SAMPLES_FOR_CALIBRATION = 3;
1014
- var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
1015
- var ESTIMATE_CACHE_MAX_SIZE = 1e4;
1016
- function getCachedEstimate(key, compute) {
1017
- const existing = ESTIMATE_CACHE.get(key);
1018
- if (existing !== void 0) return existing;
1019
- if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
1020
- for (const k of ESTIMATE_CACHE.keys()) {
1021
- if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
1022
- ESTIMATE_CACHE.delete(k);
1023
- }
1024
- }
1025
- const estimate = compute(key);
1026
- ESTIMATE_CACHE.set(key, estimate);
1027
- return estimate;
1063
+ function matchAny(patterns, input) {
1064
+ return patterns.some((p) => matchGlob(p, input));
1028
1065
  }
1029
- function estimateToolInputTokens(input) {
1030
- if (typeof input === "string") return RoughTokenEstimate(input);
1031
- if (input === null || typeof input !== "object") {
1032
- return RoughTokenEstimate(String(input));
1066
+ function isPrivateIPv4(addr) {
1067
+ const parts = addr.split(".").map((p) => Number.parseInt(p, 10));
1068
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {
1069
+ return true;
1033
1070
  }
1034
- return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
1035
- }
1036
- function estimateToolResultTokens(content) {
1037
- if (typeof content === "string") return RoughTokenEstimate(content);
1038
- return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
1039
- }
1040
- function estimateTextTokens(text) {
1041
- return RoughTokenEstimate(text);
1071
+ const [a, b, c] = parts;
1072
+ if (a === 0) return true;
1073
+ if (a === 10) return true;
1074
+ if (a === 127) return true;
1075
+ if (a === 169 && b === 254) return true;
1076
+ if (a === 172 && b >= 16 && b <= 31) return true;
1077
+ if (a === 192 && b === 168) return true;
1078
+ if (a === 192 && b === 0 && c === 0) return true;
1079
+ if (a === 100 && b >= 64 && b <= 127) return true;
1080
+ if (a >= 224) return true;
1081
+ return false;
1042
1082
  }
1043
- function computeMessageTokens(msg) {
1044
- if (typeof msg.content === "string") return estimateTextTokens(msg.content);
1045
- let total = 0;
1046
- for (const b of msg.content) {
1047
- if (b.type === "text") total += estimateTextTokens(b.text);
1048
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
1049
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
1050
- else total += RoughTokenEstimate(JSON.stringify(b));
1083
+ function isPrivateIPv6(raw) {
1084
+ const lower = raw.toLowerCase();
1085
+ if (lower === "::" || lower === "::1") return true;
1086
+ const groups = expandIPv6(lower);
1087
+ if (!groups) return true;
1088
+ if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) {
1089
+ const a = (groups[6] ?? 0) >> 8;
1090
+ const b = (groups[6] ?? 0) & 255;
1091
+ const c = (groups[7] ?? 0) >> 8;
1092
+ const d = (groups[7] ?? 0) & 255;
1093
+ return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
1051
1094
  }
1052
- return total;
1095
+ const high = groups[0] ?? 0;
1096
+ if ((high & 65024) === 64512) return true;
1097
+ if ((high & 65472) === 65152) return true;
1098
+ if ((high & 65280) === 65280) return true;
1099
+ return false;
1053
1100
  }
1054
- function estimateMessageTokens(messages) {
1055
- let total = 0;
1056
- for (const m of messages) {
1057
- if (typeof m._estTokens === "number" && m._estTokens > 0) {
1058
- total += m._estTokens;
1059
- continue;
1101
+ function expandIPv6(addr) {
1102
+ const parts = addr.split("::");
1103
+ if (parts.length > 2) return null;
1104
+ const parseGroups = (s) => {
1105
+ if (s === "") return [];
1106
+ const out = [];
1107
+ for (const g of s.split(":")) {
1108
+ if (g.length === 0 || g.length > 4) return null;
1109
+ const n = Number.parseInt(g, 16);
1110
+ if (Number.isNaN(n) || n < 0 || n > 65535) return null;
1111
+ out.push(n);
1060
1112
  }
1061
- total += computeMessageTokens(m);
1113
+ return out;
1114
+ };
1115
+ if (parts.length === 1) {
1116
+ const groups = parseGroups(parts[0] ?? "");
1117
+ if (!groups || groups.length !== 8) return null;
1118
+ return groups;
1062
1119
  }
1063
- return total;
1064
- }
1065
- function estimateToolDefTokens(tool) {
1066
- const cached = tool._estDefTokens;
1067
- if (typeof cached === "number" && cached > 0) return cached;
1068
- return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
1120
+ const head = parseGroups(parts[0] ?? "");
1121
+ const tail = parseGroups(parts[1] ?? "");
1122
+ if (!head || !tail) return null;
1123
+ const fill = 8 - head.length - tail.length;
1124
+ if (fill < 0) return null;
1125
+ return [...head, ...new Array(fill).fill(0), ...tail];
1069
1126
  }
1070
- function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
1071
- let messagesTokens = 0;
1072
- if (typeof messages === "string") {
1073
- messagesTokens = RoughTokenEstimate(messages);
1074
- } else if (Array.isArray(messages)) {
1075
- for (const m of messages) {
1076
- if (typeof m === "object" && m !== null && "content" in m) {
1077
- const cached = m._estTokens;
1078
- if (typeof cached === "number" && cached > 0) {
1079
- messagesTokens += cached;
1080
- continue;
1081
- }
1082
- const content = m.content;
1083
- if (typeof content === "string") {
1084
- messagesTokens += RoughTokenEstimate(content);
1085
- } else if (Array.isArray(content)) {
1086
- for (const b of content) {
1087
- if (typeof b === "object" && b !== null) {
1088
- if (b.type === "text") {
1089
- messagesTokens += RoughTokenEstimate(b.text);
1090
- } else {
1091
- messagesTokens += RoughTokenEstimate(JSON.stringify(b));
1092
- }
1093
- }
1094
- }
1127
+ async function assertNotPrivateHost(hostname) {
1128
+ const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1129
+ if (host === "localhost" || host.endsWith(".localhost")) {
1130
+ throw new Error("fetch: blocked localhost target");
1131
+ }
1132
+ const ipVersion = net.isIP(host);
1133
+ if (ipVersion === 4) {
1134
+ if (isPrivateIPv4(host)) {
1135
+ throw new Error(`fetch: blocked private/loopback address "${host}"`);
1136
+ }
1137
+ } else if (ipVersion === 6) {
1138
+ if (isPrivateIPv6(host)) {
1139
+ throw new Error(`fetch: blocked private/loopback address "${host}"`);
1140
+ }
1141
+ } else {
1142
+ try {
1143
+ const records = await dns.lookup(host, { all: true });
1144
+ for (const r of records) {
1145
+ const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
1146
+ if (bad) {
1147
+ throw new Error(`fetch: resolved to private address ${r.address}`);
1095
1148
  }
1096
1149
  }
1150
+ } catch (err) {
1151
+ if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
1097
1152
  }
1098
1153
  }
1099
- let systemTokens = 0;
1100
- if (typeof systemPrompt === "string") {
1101
- systemTokens = RoughTokenEstimate(systemPrompt);
1102
- } else if (Array.isArray(systemPrompt)) {
1103
- for (const b of systemPrompt) {
1104
- if (typeof b === "object" && b !== null && b.type === "text") {
1105
- systemTokens += RoughTokenEstimate(b.text);
1154
+ }
1155
+
1156
+ // src/utils/json-repair.ts
1157
+ function completePartialObject(s) {
1158
+ if (!s.trim().startsWith("{")) return s;
1159
+ if (tryParse(s).ok) return s;
1160
+ return repairTruncated(s);
1161
+ }
1162
+ function repairTruncated(s) {
1163
+ const stack = [];
1164
+ let inString = false;
1165
+ let escaped = false;
1166
+ let sawKey = false;
1167
+ let prevSig = "";
1168
+ let contentEnd = 0;
1169
+ let stringBraceDepth = 0;
1170
+ for (let i = 0; i < s.length; i++) {
1171
+ const ch = expectDefined(s[i]);
1172
+ if (inString) {
1173
+ contentEnd = i + 1;
1174
+ if (escaped) {
1175
+ escaped = false;
1176
+ continue;
1177
+ }
1178
+ if (ch === "\\") {
1179
+ escaped = true;
1180
+ continue;
1181
+ }
1182
+ if (ch === '"') {
1183
+ inString = false;
1184
+ prevSig = '"';
1185
+ stringBraceDepth = 0;
1186
+ continue;
1106
1187
  }
1188
+ if (ch === "{") stringBraceDepth++;
1189
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
1190
+ continue;
1191
+ }
1192
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
1193
+ contentEnd = i + 1;
1194
+ if (ch === '"') {
1195
+ inString = true;
1196
+ sawKey = true;
1197
+ stringBraceDepth = 0;
1198
+ prevSig = '"';
1199
+ } else if (ch === "{" || ch === "[") {
1200
+ stack.push(ch);
1201
+ prevSig = ch;
1202
+ } else if (ch === "}" || ch === "]") {
1203
+ stack.pop();
1204
+ prevSig = ch;
1205
+ } else {
1206
+ prevSig = ch;
1107
1207
  }
1108
1208
  }
1109
- let toolsTokens = 0;
1110
- for (const t of tools) {
1111
- toolsTokens += estimateToolDefTokens(t);
1209
+ if (!sawKey && !inString) return s;
1210
+ let result = s.slice(0, contentEnd);
1211
+ if (inString) {
1212
+ if (escaped) {
1213
+ result = result.slice(0, -1);
1214
+ } else if (endsWithInvalidEscape(result)) {
1215
+ result = result.slice(0, -2);
1216
+ }
1217
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
1218
+ result += '"';
1219
+ } else if (prevSig === ":") {
1220
+ result += "null";
1112
1221
  }
1113
- const total = messagesTokens + systemTokens + toolsTokens;
1114
- calState(calibrationKey).prevEst = total;
1115
- return {
1116
- messages: messagesTokens,
1117
- systemPrompt: systemTokens,
1118
- tools: toolsTokens,
1119
- total
1120
- };
1121
- }
1122
- function recordActualUsage(actualInputTokens, estimatedInputTokens, calibrationKey = CALIBRATION_GLOBAL_KEY) {
1123
- if (actualInputTokens <= 0) return;
1124
- const cal = calState(calibrationKey);
1125
- const est = estimatedInputTokens ?? cal.prevEst;
1126
- if (est <= 0) return;
1127
- const sampleRatio = actualInputTokens / est;
1128
- if (cal.count === 0) {
1129
- cal.ratio = sampleRatio;
1130
- } else {
1131
- cal.ratio = CAL_ALPHA * sampleRatio + (1 - CAL_ALPHA) * cal.ratio;
1222
+ for (let k = stack.length - 1; k >= 0; k--) {
1223
+ result += stack[k] === "{" ? "}" : "]";
1132
1224
  }
1133
- cal.ratio = Math.min(1.5, Math.max(0.5, cal.ratio));
1134
- cal.count++;
1135
- }
1136
- function getCalibrationState(calibrationKey = CALIBRATION_GLOBAL_KEY) {
1137
- const cal = calState(calibrationKey);
1138
- return {
1139
- ratio: cal.ratio,
1140
- count: cal.count,
1141
- calibrated: cal.count >= MIN_SAMPLES_FOR_CALIBRATION
1142
- };
1143
- }
1144
- function estimateRequestTokensCalibrated(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
1145
- const result = estimateRequestTokens(messages, systemPrompt, tools, calibrationKey);
1146
- const cal = calState(calibrationKey);
1147
- if (cal.count >= MIN_SAMPLES_FOR_CALIBRATION) {
1148
- const safeRatio = Math.min(1.5, Math.max(0.5, cal.ratio));
1149
- return {
1150
- messages: Math.round(result.messages * safeRatio),
1151
- systemPrompt: Math.round(result.systemPrompt * safeRatio),
1152
- tools: Math.round(result.tools * safeRatio),
1153
- total: Math.round(result.total * safeRatio)
1154
- };
1225
+ if (!tryParse(result).ok) {
1226
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
1227
+ if (tryParse(patched).ok) result = patched;
1155
1228
  }
1156
1229
  return result;
1157
1230
  }
1158
- function resetCalibration(calibrationKey) {
1159
- if (calibrationKey === void 0) {
1160
- _cals.clear();
1161
- return;
1231
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
1232
+ function endsWithInvalidEscape(str) {
1233
+ const last = str[str.length - 1];
1234
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
1235
+ if (VALID_ESCAPE.has(last)) return false;
1236
+ let backslashes = 0;
1237
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
1238
+ return backslashes % 2 === 1;
1239
+ }
1240
+ function tryParse(s) {
1241
+ try {
1242
+ return { ok: true, value: JSON.parse(s) };
1243
+ } catch {
1244
+ return { ok: false };
1162
1245
  }
1163
- _cals.delete(calibrationKey);
1164
1246
  }
1165
1247
 
1166
- // src/utils/message-invariants.ts
1167
- function repairToolUseAdjacency(messages) {
1168
- const removedToolUses = [];
1169
- const removedToolResults = [];
1170
- let removedMessages = 0;
1171
- let changed = false;
1172
- const out = [];
1173
- for (let i = 0; i < messages.length; i++) {
1174
- const original = expectDefined(messages[i]);
1175
- let msg = original;
1176
- if (hasToolUse(msg)) {
1177
- const nextIds = toolResultIds(messages[i + 1]);
1178
- const filtered = mapContent(msg, (blocks) => {
1179
- const next = [];
1180
- for (const block of blocks) {
1181
- if (block.type === "tool_use" && !nextIds.has(block.id)) {
1182
- removedToolUses.push(block.id);
1183
- changed = true;
1184
- continue;
1185
- }
1186
- next.push(block);
1187
- }
1188
- return next;
1248
+ // src/utils/json-schema-validate.ts
1249
+ function validateAgainstSchema(value, schema) {
1250
+ const errors = [];
1251
+ walk(value, schema, "", errors);
1252
+ return { ok: errors.length === 0, errors };
1253
+ }
1254
+ function walk(value, schema, path4, errors) {
1255
+ if (schema.enum !== void 0) {
1256
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
1257
+ errors.push({
1258
+ path: path4 || "<root>",
1259
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1189
1260
  });
1190
- msg = filtered ?? msg;
1261
+ return;
1191
1262
  }
1192
- if (hasToolResult(msg)) {
1193
- const allowed = toolUseIds(out[out.length - 1]);
1194
- const filtered = mapContent(msg, (blocks) => {
1195
- const next = [];
1196
- for (const block of blocks) {
1197
- if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
1198
- removedToolResults.push(block.tool_use_id);
1199
- changed = true;
1200
- continue;
1201
- }
1202
- next.push(block);
1203
- }
1204
- return next;
1263
+ }
1264
+ if (typeof schema.type === "string") {
1265
+ if (!checkType(value, schema.type)) {
1266
+ errors.push({
1267
+ path: path4 || "<root>",
1268
+ message: `expected ${schema.type}, got ${describeType(value)}`
1205
1269
  });
1206
- msg = filtered ?? msg;
1270
+ return;
1207
1271
  }
1208
- if (isEmptyMessage(msg)) {
1209
- removedMessages++;
1210
- changed = true;
1211
- continue;
1212
- }
1213
- out.push(msg);
1214
- }
1215
- return {
1216
- messages: changed ? out : messages,
1217
- report: { changed, removedToolUses, removedToolResults, removedMessages }
1218
- };
1219
- }
1220
- function hasToolUse(msg) {
1221
- return contentBlocks(msg).some((b) => b.type === "tool_use");
1222
- }
1223
- function hasToolResult(msg) {
1224
- return contentBlocks(msg).some((b) => b.type === "tool_result");
1225
- }
1226
- function toolUseIds(msg) {
1227
- const ids = /* @__PURE__ */ new Set();
1228
- if (!msg || msg.role !== "assistant") return ids;
1229
- for (const block of contentBlocks(msg)) {
1230
- if (block.type === "tool_use") ids.add(block.id);
1231
- }
1232
- return ids;
1233
- }
1234
- function toolResultIds(msg) {
1235
- const ids = /* @__PURE__ */ new Set();
1236
- if (!msg || msg.role !== "user") return ids;
1237
- for (const block of contentBlocks(msg)) {
1238
- if (block.type === "tool_result") ids.add(block.tool_use_id);
1239
- }
1240
- return ids;
1241
- }
1242
- function contentBlocks(msg) {
1243
- return msg && Array.isArray(msg.content) ? msg.content : [];
1244
- }
1245
- function mapContent(msg, fn) {
1246
- if (!Array.isArray(msg.content)) return msg;
1247
- const next = fn(msg.content);
1248
- if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
1249
- return msg;
1250
- }
1251
- return { ...msg, content: next };
1252
- }
1253
- function isEmptyMessage(msg) {
1254
- if (typeof msg.content === "string") return msg.content.trim().length === 0;
1255
- return msg.content.length === 0;
1256
- }
1257
-
1258
- // src/utils/json-schema-validate.ts
1259
- function validateAgainstSchema(value, schema) {
1260
- const errors = [];
1261
- walk(value, schema, "", errors);
1262
- return { ok: errors.length === 0, errors };
1263
- }
1264
- function walk(value, schema, path3, errors) {
1265
- if (schema.enum !== void 0) {
1266
- if (!schema.enum.some((e) => deepEqual(e, value))) {
1267
- errors.push({
1268
- path: path3 || "<root>",
1269
- message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1270
- });
1271
- return;
1272
- }
1273
- }
1274
- if (typeof schema.type === "string") {
1275
- if (!checkType(value, schema.type)) {
1276
- errors.push({
1277
- path: path3 || "<root>",
1278
- message: `expected ${schema.type}, got ${describeType(value)}`
1279
- });
1280
- return;
1281
- }
1282
- }
1283
- if (schema.type === "object" && isPlainObject(value)) {
1284
- const obj = value;
1285
- for (const req of schema.required ?? []) {
1286
- if (!(req in obj)) {
1287
- errors.push({ path: joinPath(path3, req), message: "required property missing" });
1288
- }
1289
- }
1290
- if (schema.properties) {
1291
- for (const [key, subSchema] of Object.entries(schema.properties)) {
1292
- if (key in obj) {
1293
- walk(obj[key], subSchema, joinPath(path3, key), errors);
1294
- }
1295
- }
1296
- }
1297
- }
1298
- if (schema.type === "array" && Array.isArray(value) && schema.items) {
1299
- for (let i = 0; i < value.length; i++) {
1300
- walk(value[i], schema.items, `${path3}[${i}]`, errors);
1272
+ }
1273
+ if (schema.type === "object" && isPlainObject(value)) {
1274
+ const obj = value;
1275
+ for (const req of schema.required ?? []) {
1276
+ if (!(req in obj)) {
1277
+ errors.push({ path: joinPath(path4, req), message: "required property missing" });
1278
+ }
1279
+ }
1280
+ if (schema.properties) {
1281
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
1282
+ if (key in obj) {
1283
+ walk(obj[key], subSchema, joinPath(path4, key), errors);
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
1289
+ for (let i = 0; i < value.length; i++) {
1290
+ walk(value[i], schema.items, `${path4}[${i}]`, errors);
1301
1291
  }
1302
1292
  }
1303
1293
  }
@@ -1351,418 +1341,1418 @@ function deepEqual(a, b) {
1351
1341
  return false;
1352
1342
  }
1353
1343
 
1354
- // src/utils/regex-guard.ts
1355
- var MAX_PATTERN_LEN = 512;
1356
- var DANGEROUS_PATTERNS = [
1357
- /(\([^)]*[+*][^)]*\))[+*]/,
1358
- // (a+)+, (.*)+, etc
1359
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
1360
- // same, with non-capturing group
1361
- ];
1362
- function compileUserRegex(pattern, flags) {
1363
- if (typeof pattern !== "string") {
1364
- return { ok: false, reason: "pattern must be a string" };
1344
+ // src/utils/merge-custom-models.ts
1345
+ function mergeCustomModelDefs(providerCustomModels, configModels) {
1346
+ const out = {};
1347
+ if (providerCustomModels) {
1348
+ for (const [id, def] of Object.entries(providerCustomModels)) {
1349
+ out[id] = { ...def };
1350
+ }
1365
1351
  }
1366
- if (pattern.length === 0) {
1367
- return { ok: false, reason: "pattern is empty" };
1352
+ if (configModels) {
1353
+ for (const [id, def] of Object.entries(configModels)) {
1354
+ out[id] = { ...def };
1355
+ }
1368
1356
  }
1369
- if (pattern.length > MAX_PATTERN_LEN) {
1370
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
1357
+ if (Object.keys(out).length === 0) return void 0;
1358
+ return out;
1359
+ }
1360
+
1361
+ // src/utils/merge-models-payload.ts
1362
+ function mergeModelsPayload(base, overlay) {
1363
+ const out = {};
1364
+ for (const [id, provider] of Object.entries(base)) {
1365
+ out[id] = cloneProvider(provider);
1371
1366
  }
1372
- for (const rx of DANGEROUS_PATTERNS) {
1373
- if (rx.test(pattern)) {
1374
- return {
1375
- ok: false,
1376
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
1377
- };
1378
- }
1367
+ for (const [id, ovProvider] of Object.entries(overlay)) {
1368
+ const existing = out[id];
1369
+ out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
1379
1370
  }
1380
- try {
1381
- return { ok: true, regex: new RegExp(pattern, flags) };
1382
- } catch (err) {
1383
- return {
1384
- ok: false,
1385
- reason: err instanceof Error ? err.message : "invalid regex"
1386
- };
1371
+ return out;
1372
+ }
1373
+ function mergeProvider(base, overlay) {
1374
+ const models = {};
1375
+ for (const [mid, m] of Object.entries(base.models ?? {})) {
1376
+ models[mid] = { ...m };
1377
+ }
1378
+ for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
1379
+ const existing = models[mid];
1380
+ models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
1387
1381
  }
1382
+ return {
1383
+ ...base,
1384
+ // Overlay scalar fields win when explicitly provided; otherwise keep base.
1385
+ ...stripUndefined({
1386
+ id: overlay.id,
1387
+ name: overlay.name,
1388
+ npm: overlay.npm,
1389
+ api: overlay.api,
1390
+ env: overlay.env,
1391
+ doc: overlay.doc
1392
+ }),
1393
+ models
1394
+ };
1388
1395
  }
1389
- var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
1390
- var IS_WINDOWS = process.platform === "win32";
1391
- var SEP = IS_WINDOWS ? "\\" : "/";
1392
- function isGlob(p) {
1393
- for (const c of p) {
1394
- if (GLOB_CHARS.has(c)) return true;
1396
+ function mergeModel(base, overlay) {
1397
+ const merged = { ...base, ...overlay };
1398
+ if (base.limit || overlay.limit) {
1399
+ merged.limit = { ...base.limit, ...overlay.limit };
1395
1400
  }
1396
- return false;
1401
+ if (base.cost || overlay.cost) {
1402
+ merged.cost = { ...base.cost, ...overlay.cost };
1403
+ }
1404
+ if (base.modalities || overlay.modalities) {
1405
+ merged.modalities = { ...base.modalities, ...overlay.modalities };
1406
+ }
1407
+ return merged;
1397
1408
  }
1398
- function globToRegex(pat) {
1399
- let i = 0;
1400
- let re = "^";
1401
- while (i < pat.length) {
1402
- const c = expectDefined(pat[i]);
1403
- if (c === "*") {
1404
- if (pat[i + 1] === "*") {
1405
- re += ".*";
1406
- i += 2;
1407
- if (pat[i] === "/") i++;
1408
- } else {
1409
- re += "[^/\\\\]*";
1410
- i++;
1411
- }
1412
- } else if (c === "?") {
1413
- re += "[^/\\\\]";
1414
- i++;
1415
- } else if (c === "[") {
1416
- let cls = "[";
1417
- i++;
1418
- if (pat[i] === "!" || pat[i] === "^") {
1419
- cls += "^";
1420
- i++;
1421
- }
1422
- while (i < pat.length && pat[i] !== "]") {
1423
- const ch = pat[i] ?? "";
1424
- if (ch === "\\") cls += "\\\\";
1425
- else if (ch === "]" || ch === "^") cls += `\\${ch}`;
1426
- else cls += ch;
1427
- i++;
1428
- }
1429
- cls += "]";
1430
- re += cls;
1431
- i++;
1432
- } else {
1433
- re += c.replace(/[.+^${}()|\\]/g, "\\$&");
1434
- i++;
1435
- }
1409
+ function cloneProvider(p) {
1410
+ const models = {};
1411
+ for (const [mid, m] of Object.entries(p.models ?? {})) {
1412
+ models[mid] = { ...m };
1436
1413
  }
1437
- return new RegExp(re + "$");
1414
+ return { ...p, models };
1438
1415
  }
1439
- function baseDir(pat) {
1440
- let i = pat.length - 1;
1441
- while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
1442
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
1443
- return cut < 0 ? "." : pat.slice(0, cut);
1416
+ function stripUndefined(obj) {
1417
+ const out = {};
1418
+ for (const [k, v] of Object.entries(obj)) {
1419
+ if (v !== void 0) out[k] = v;
1420
+ }
1421
+ return out;
1444
1422
  }
1445
- async function expandGlob(pattern) {
1446
- if (!isGlob(pattern)) return [pattern];
1447
- const results = /* @__PURE__ */ new Set();
1448
- const abs = isAbsolute(pattern);
1449
- const base = abs ? baseDir(pattern) : baseDir(pattern);
1450
- const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
1451
- async function walk2(dir, pat) {
1452
- let entries;
1453
- try {
1454
- entries = await fs.readdir(dir);
1455
- } catch {
1456
- return;
1457
- }
1458
- const firstGlob = pat.search(/[*?[[]/);
1459
- if (firstGlob < 0) {
1460
- const re = globToRegex(pat);
1461
- for (const e of entries) {
1462
- if (re.test(e)) {
1463
- const full = `${dir}${SEP}${e}`;
1464
- results.add(abs ? resolve(full) : full);
1465
- }
1466
- }
1467
- return;
1468
- }
1469
- const before = pat.slice(0, firstGlob);
1470
- const rest = pat.slice(firstGlob);
1471
- if (before.endsWith("**")) {
1472
- await walk2(dir, rest);
1473
- for (const e of entries) {
1474
- const full = `${dir}${SEP}${e}`;
1475
- try {
1476
- const stat3 = await fs.stat(full);
1477
- if (stat3.isDirectory()) await walk2(full, rest);
1478
- } catch {
1479
- }
1480
- }
1481
- } else if (before === "") {
1482
- const re = globToRegex(rest);
1483
- for (const e of entries) {
1484
- if (re.test(e)) {
1485
- const full = `${dir}${SEP}${e}`;
1486
- results.add(abs ? resolve(full) : full);
1423
+
1424
+ // src/utils/message-invariants.ts
1425
+ function repairToolUseAdjacency(messages) {
1426
+ const removedToolUses = [];
1427
+ const removedToolResults = [];
1428
+ let removedMessages = 0;
1429
+ let changed = false;
1430
+ const out = [];
1431
+ for (let i = 0; i < messages.length; i++) {
1432
+ const original = expectDefined(messages[i]);
1433
+ let msg = original;
1434
+ if (hasToolUse(msg)) {
1435
+ const nextIds = toolResultIds(messages[i + 1]);
1436
+ const filtered = mapContent(msg, (blocks) => {
1437
+ const next = [];
1438
+ for (const block of blocks) {
1439
+ if (block.type === "tool_use" && !nextIds.has(block.id)) {
1440
+ removedToolUses.push(block.id);
1441
+ changed = true;
1442
+ continue;
1443
+ }
1444
+ next.push(block);
1487
1445
  }
1488
- }
1489
- } else {
1490
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
1491
- if (entries.includes(seg)) {
1492
- const full = `${dir}${SEP}${seg}`;
1493
- try {
1494
- const stat3 = await fs.stat(full);
1495
- if (stat3.isDirectory()) await walk2(full, rest);
1496
- } catch {
1446
+ return next;
1447
+ });
1448
+ msg = filtered ?? msg;
1449
+ }
1450
+ if (hasToolResult(msg)) {
1451
+ const allowed = toolUseIds(out[out.length - 1]);
1452
+ const filtered = mapContent(msg, (blocks) => {
1453
+ const next = [];
1454
+ for (const block of blocks) {
1455
+ if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
1456
+ removedToolResults.push(block.tool_use_id);
1457
+ changed = true;
1458
+ continue;
1459
+ }
1460
+ next.push(block);
1497
1461
  }
1462
+ return next;
1463
+ });
1464
+ msg = filtered ?? msg;
1465
+ }
1466
+ if (isEmptyMessage(msg)) {
1467
+ removedMessages++;
1468
+ changed = true;
1469
+ continue;
1470
+ }
1471
+ out.push(msg);
1472
+ }
1473
+ return {
1474
+ messages: changed ? out : messages,
1475
+ report: { changed, removedToolUses, removedToolResults, removedMessages }
1476
+ };
1477
+ }
1478
+ function hasToolUse(msg) {
1479
+ return contentBlocks(msg).some((b) => b.type === "tool_use");
1480
+ }
1481
+ function hasToolResult(msg) {
1482
+ return contentBlocks(msg).some((b) => b.type === "tool_result");
1483
+ }
1484
+ function toolUseIds(msg) {
1485
+ const ids = /* @__PURE__ */ new Set();
1486
+ if (!msg || msg.role !== "assistant") return ids;
1487
+ for (const block of contentBlocks(msg)) {
1488
+ if (block.type === "tool_use") ids.add(block.id);
1489
+ }
1490
+ return ids;
1491
+ }
1492
+ function toolResultIds(msg) {
1493
+ const ids = /* @__PURE__ */ new Set();
1494
+ if (!msg || msg.role !== "user") return ids;
1495
+ for (const block of contentBlocks(msg)) {
1496
+ if (block.type === "tool_result") ids.add(block.tool_use_id);
1497
+ }
1498
+ return ids;
1499
+ }
1500
+ function contentBlocks(msg) {
1501
+ return msg && Array.isArray(msg.content) ? msg.content : [];
1502
+ }
1503
+ function mapContent(msg, fn) {
1504
+ if (!Array.isArray(msg.content)) return msg;
1505
+ const next = fn(msg.content);
1506
+ if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
1507
+ return msg;
1508
+ }
1509
+ return { ...msg, content: next };
1510
+ }
1511
+ function isEmptyMessage(msg) {
1512
+ if (typeof msg.content === "string") return msg.content.trim().length === 0;
1513
+ return msg.content.length === 0;
1514
+ }
1515
+
1516
+ // src/utils/newline-normalize.ts
1517
+ function detectNewlineStyle(text) {
1518
+ let lf = 0;
1519
+ let crlf = 0;
1520
+ let cr = 0;
1521
+ for (let i = 0; i < text.length; i++) {
1522
+ const c = text.charCodeAt(i);
1523
+ if (c === 13) {
1524
+ if (text.charCodeAt(i + 1) === 10) {
1525
+ crlf++;
1526
+ i++;
1527
+ } else {
1528
+ cr++;
1498
1529
  }
1530
+ } else if (c === 10) {
1531
+ lf++;
1532
+ }
1533
+ }
1534
+ if (crlf > lf && crlf > cr) return "crlf";
1535
+ if (cr > lf && cr > crlf) return "cr";
1536
+ return "lf";
1537
+ }
1538
+ function toStyle(text, style) {
1539
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1540
+ if (style === "lf") return normalized;
1541
+ if (style === "crlf") return normalized.replace(/\n/g, "\r\n");
1542
+ return normalized.replace(/\n/g, "\r");
1543
+ }
1544
+ function normalizeToLf(text) {
1545
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1546
+ }
1547
+
1548
+ // src/utils/regex-guard.ts
1549
+ var MAX_PATTERN_LEN = 512;
1550
+ var DANGEROUS_PATTERNS = [
1551
+ /(\([^)]*[+*][^)]*\))[+*]/,
1552
+ // (a+)+, (.*)+, etc
1553
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
1554
+ // same, with non-capturing group
1555
+ ];
1556
+ function compileUserRegex(pattern, flags) {
1557
+ if (typeof pattern !== "string") {
1558
+ return { ok: false, reason: "pattern must be a string" };
1559
+ }
1560
+ if (pattern.length === 0) {
1561
+ return { ok: false, reason: "pattern is empty" };
1562
+ }
1563
+ if (pattern.length > MAX_PATTERN_LEN) {
1564
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
1565
+ }
1566
+ for (const rx of DANGEROUS_PATTERNS) {
1567
+ if (rx.test(pattern)) {
1568
+ return {
1569
+ ok: false,
1570
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
1571
+ };
1572
+ }
1573
+ }
1574
+ try {
1575
+ return { ok: true, regex: new RegExp(pattern, flags) };
1576
+ } catch (err) {
1577
+ return {
1578
+ ok: false,
1579
+ reason: err instanceof Error ? err.message : "invalid regex"
1580
+ };
1581
+ }
1582
+ }
1583
+
1584
+ // src/utils/safe-json.ts
1585
+ function safeParse(input, maxBytes = 5e6) {
1586
+ if (input.length > maxBytes) {
1587
+ return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
1588
+ }
1589
+ try {
1590
+ return { ok: true, value: JSON.parse(input) };
1591
+ } catch (err) {
1592
+ return {
1593
+ ok: false,
1594
+ error: toErrorMessage(err)
1595
+ };
1596
+ }
1597
+ }
1598
+ function safeStringify(value, pretty = false) {
1599
+ const seen = /* @__PURE__ */ new WeakSet();
1600
+ const replacer = (_k, v) => {
1601
+ if (typeof v === "bigint") return v.toString();
1602
+ if (v instanceof Error) {
1603
+ return { name: v.name, message: v.message, stack: v.stack };
1604
+ }
1605
+ if (typeof v === "object" && v !== null) {
1606
+ if (seen.has(v)) return "[Circular]";
1607
+ seen.add(v);
1608
+ }
1609
+ return v;
1610
+ };
1611
+ try {
1612
+ return JSON.stringify(value, replacer, pretty ? 2 : void 0) ?? "null";
1613
+ } catch (err) {
1614
+ return JSON.stringify({
1615
+ __serialization_error: toErrorMessage(err)
1616
+ });
1617
+ }
1618
+ }
1619
+ function sanitizeJsonString(s) {
1620
+ let out = s.trim();
1621
+ out = stripSingleLineComments(out);
1622
+ out = out.replace(/,(\s*[}\]])/g, "$1");
1623
+ out = escapeControlCharsInStrings(out);
1624
+ try {
1625
+ JSON.parse(out);
1626
+ return out;
1627
+ } catch {
1628
+ return null;
1629
+ }
1630
+ }
1631
+ function escapeControlCharsInStrings(s) {
1632
+ let inString = false;
1633
+ let out = "";
1634
+ for (let i = 0; i < s.length; i++) {
1635
+ const c = s.charAt(i);
1636
+ if (c === '"' && (i === 0 || s[i - 1] !== "\\")) {
1637
+ inString = !inString;
1638
+ out += c;
1639
+ continue;
1640
+ }
1641
+ const code = c.charCodeAt(0);
1642
+ if (inString && code < 32) {
1643
+ switch (c) {
1644
+ case "\n":
1645
+ out += "\\n";
1646
+ break;
1647
+ case "\r":
1648
+ out += "\\r";
1649
+ break;
1650
+ case " ":
1651
+ out += "\\t";
1652
+ break;
1653
+ case "\b":
1654
+ out += "\\b";
1655
+ break;
1656
+ case "\f":
1657
+ out += "\\f";
1658
+ break;
1659
+ default:
1660
+ out += `\\u${code.toString(16).padStart(4, "0")}`;
1661
+ }
1662
+ continue;
1663
+ }
1664
+ out += c;
1665
+ }
1666
+ return out;
1667
+ }
1668
+ function stripSingleLineComments(s) {
1669
+ let inString = false;
1670
+ const chars = [];
1671
+ let i = 0;
1672
+ while (i < s.length) {
1673
+ const c = s.charAt(i);
1674
+ if (c === '"' && (i === 0 || s.charAt(i - 1) !== "\\")) {
1675
+ inString = !inString;
1676
+ chars.push(c);
1677
+ } else if (c === "/" && s.charAt(i + 1) === "/" && !inString) {
1678
+ while (i < s.length && s.charAt(i) !== "\n") i++;
1679
+ } else {
1680
+ chars.push(c);
1681
+ }
1682
+ i++;
1683
+ }
1684
+ return chars.join("");
1685
+ }
1686
+
1687
+ // src/utils/sleep.ts
1688
+ function sleep(ms) {
1689
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1690
+ }
1691
+
1692
+ // src/utils/string.ts
1693
+ function truncate(s, max) {
1694
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1695
+ }
1696
+
1697
+ // src/utils/task-format.ts
1698
+ function computeTaskItemProgress(tasks) {
1699
+ let completed = 0;
1700
+ let pending = 0;
1701
+ let inProgress = 0;
1702
+ let blocked = 0;
1703
+ let failed = 0;
1704
+ let review = 0;
1705
+ let estimatedHours = 0;
1706
+ const actualHours = 0;
1707
+ for (const t of tasks) {
1708
+ switch (t.status) {
1709
+ case "completed":
1710
+ completed++;
1711
+ break;
1712
+ case "pending":
1713
+ pending++;
1714
+ break;
1715
+ case "in_progress":
1716
+ inProgress++;
1717
+ break;
1718
+ case "blocked":
1719
+ blocked++;
1720
+ break;
1721
+ case "failed":
1722
+ failed++;
1723
+ break;
1724
+ case "review":
1725
+ review++;
1726
+ break;
1727
+ }
1728
+ estimatedHours += t.estimateHours ?? 0;
1729
+ }
1730
+ return {
1731
+ total: tasks.length,
1732
+ pending,
1733
+ inProgress,
1734
+ blocked,
1735
+ failed,
1736
+ review,
1737
+ completed,
1738
+ percentComplete: tasks.length > 0 ? Math.round(completed / tasks.length * 100) : 0,
1739
+ estimatedHours,
1740
+ actualHours
1741
+ };
1742
+ }
1743
+ var STATUS_ICON = {
1744
+ pending: "\u25CB",
1745
+ in_progress: "\u25D0",
1746
+ blocked: "\u2298",
1747
+ failed: "\u2717",
1748
+ review: "\u25D1",
1749
+ completed: "\u25CF"
1750
+ };
1751
+ var PRIORITY_ICON = {
1752
+ critical: "\u{1F534}",
1753
+ high: "\u{1F7E0}",
1754
+ medium: "\u{1F7E1}",
1755
+ low: "\u{1F7E2}"
1756
+ };
1757
+ var TYPE_ICON = {
1758
+ feature: "\u26A1",
1759
+ bugfix: "\u{1F41B}",
1760
+ refactor: "\u267B\uFE0F",
1761
+ docs: "\u{1F4DD}",
1762
+ test: "\u{1F9EA}",
1763
+ chore: "\u{1F527}"
1764
+ };
1765
+ function formatTaskProgress(tasks) {
1766
+ const p = computeTaskItemProgress(tasks);
1767
+ if (p.total === 0) return "No tasks.";
1768
+ const barWidth = 24;
1769
+ const filled = Math.round(p.percentComplete / 100 * barWidth);
1770
+ const empty = barWidth - filled;
1771
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
1772
+ return [
1773
+ `${color.bold("Tasks")} [${bar}] ${p.percentComplete}%`,
1774
+ ` ${color.green("\u25CF")} ${p.completed} done \u2502 ${color.yellow("\u25D0")} ${p.inProgress} active \u2502 ${color.dim("\u25CB")} ${p.pending} pending \u2502 \u2298 ${p.blocked} blocked \u2502 \u2717 ${p.failed} failed`,
1775
+ p.estimatedHours > 0 ? ` ${color.dim(`est. ${p.estimatedHours}h`)}` : ""
1776
+ ].filter(Boolean).join("\n");
1777
+ }
1778
+ function formatTaskList(tasks) {
1779
+ if (tasks.length === 0) return "No tasks.";
1780
+ const order = ["in_progress", "blocked", "review", "pending", "failed", "completed"];
1781
+ const groups = /* @__PURE__ */ new Map();
1782
+ for (const t of tasks) {
1783
+ const list = groups.get(t.status) ?? [];
1784
+ list.push(t);
1785
+ groups.set(t.status, list);
1786
+ }
1787
+ const lines = [];
1788
+ lines.push(color.dim(`Tasks (${tasks.length} total):`));
1789
+ for (const status of order) {
1790
+ const group = groups.get(status);
1791
+ if (!group || group.length === 0) continue;
1792
+ const icon = STATUS_ICON[status];
1793
+ lines.push(` ${icon} ${status.toUpperCase()} (${group.length})`);
1794
+ for (const t of group) {
1795
+ const prio = PRIORITY_ICON[t.priority];
1796
+ const type = TYPE_ICON[t.type];
1797
+ const deps = t.dependsOn && t.dependsOn.length > 0 ? ` ${color.dim("\u2190")} ${color.dim(t.dependsOn.map((d) => d.slice(0, 8)).join(", "))}` : "";
1798
+ const who = t.assignee ? ` ${color.dim(`@${t.assignee}`)}` : "";
1799
+ const hrs = t.estimateHours ? ` ${color.dim(`${t.estimateHours}h`)}` : "";
1800
+ lines.push(` ${type} ${prio} ${t.title}${deps}${who}${hrs}`);
1499
1801
  }
1500
1802
  }
1501
- await walk2(base === "." ? "." : base, relPat);
1502
- return [...results];
1803
+ return lines.join("\n");
1503
1804
  }
1504
1805
 
1505
- // src/utils/json-repair.ts
1506
- function completePartialObject(s) {
1507
- if (!s.trim().startsWith("{")) return s;
1508
- if (tryParse(s).ok) return s;
1509
- return repairTruncated(s);
1806
+ // src/utils/todos-format.ts
1807
+ function formatTodosList(todos) {
1808
+ if (todos.length === 0) return "No todos.";
1809
+ const lines = [];
1810
+ const done = todos.filter((t) => t.status === "completed").length;
1811
+ lines.push(color.dim(`Todos (${done}/${todos.length} done):`));
1812
+ todos.forEach((t, i) => {
1813
+ const mark = t.status === "completed" ? color.green("[x]") : t.status === "in_progress" ? color.yellow("[~]") : color.dim("[ ]");
1814
+ const text = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
1815
+ const label = t.status === "completed" ? color.dim(text) : text;
1816
+ lines.push(` ${color.dim(String(i + 1).padStart(2))}. ${mark} ${label}`);
1817
+ });
1818
+ return lines.join("\n");
1510
1819
  }
1511
- function repairTruncated(s) {
1512
- const stack = [];
1513
- let inString = false;
1514
- let escaped = false;
1515
- let sawKey = false;
1516
- let prevSig = "";
1517
- let contentEnd = 0;
1518
- let stringBraceDepth = 0;
1519
- for (let i = 0; i < s.length; i++) {
1520
- const ch = expectDefined(s[i]);
1521
- if (inString) {
1522
- contentEnd = i + 1;
1523
- if (escaped) {
1524
- escaped = false;
1525
- continue;
1526
- }
1527
- if (ch === "\\") {
1528
- escaped = true;
1529
- continue;
1530
- }
1531
- if (ch === '"') {
1532
- inString = false;
1533
- prevSig = '"';
1534
- stringBraceDepth = 0;
1535
- continue;
1536
- }
1537
- if (ch === "{") stringBraceDepth++;
1538
- else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
1539
- continue;
1540
- }
1541
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
1542
- contentEnd = i + 1;
1543
- if (ch === '"') {
1544
- inString = true;
1545
- sawKey = true;
1546
- stringBraceDepth = 0;
1547
- prevSig = '"';
1548
- } else if (ch === "{" || ch === "[") {
1549
- stack.push(ch);
1550
- prevSig = ch;
1551
- } else if (ch === "}" || ch === "]") {
1552
- stack.pop();
1553
- prevSig = ch;
1820
+
1821
+ // src/utils/tool-wire-compact.ts
1822
+ var TOOL_DESCRIPTION_MAX_CHARS = 640;
1823
+ var SCHEMA_DESCRIPTION_MAX_CHARS = 180;
1824
+ var compactCache = /* @__PURE__ */ new WeakMap();
1825
+ function compactToolDefinitionForWire(tool, opts = {}) {
1826
+ const useDefaultOptions = opts.descriptionMaxChars === void 0 && opts.schemaDescriptionMaxChars === void 0;
1827
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
1828
+ const cached = compactCache.get(tool);
1829
+ if (cached) return cached;
1830
+ }
1831
+ const compact = {
1832
+ name: tool.name,
1833
+ description: compactDescription(
1834
+ tool.description ?? "",
1835
+ opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
1836
+ ),
1837
+ inputSchema: compactSchemaDescriptions(
1838
+ tool.inputSchema,
1839
+ opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
1840
+ )
1841
+ };
1842
+ if (useDefaultOptions && typeof tool === "object" && tool !== null) {
1843
+ compactCache.set(tool, compact);
1844
+ }
1845
+ return compact;
1846
+ }
1847
+ function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
1848
+ const compact = compactSchemaNode(schema, maxDescriptionChars);
1849
+ return isRecord(compact) ? compact : { type: "object", properties: {} };
1850
+ }
1851
+ function compactSchemaNode(node, maxDescriptionChars) {
1852
+ if (Array.isArray(node)) {
1853
+ return node.map((item) => compactSchemaNode(item, maxDescriptionChars));
1854
+ }
1855
+ if (!isRecord(node)) return node;
1856
+ const out = {};
1857
+ for (const [key, value] of Object.entries(node)) {
1858
+ if (key === "description" && typeof value === "string") {
1859
+ out[key] = compactDescription(value, maxDescriptionChars);
1554
1860
  } else {
1555
- prevSig = ch;
1861
+ out[key] = compactSchemaNode(value, maxDescriptionChars);
1556
1862
  }
1557
1863
  }
1558
- if (!sawKey && !inString) return s;
1559
- let result = s.slice(0, contentEnd);
1560
- if (inString) {
1561
- if (escaped) {
1562
- result = result.slice(0, -1);
1563
- } else if (endsWithInvalidEscape(result)) {
1564
- result = result.slice(0, -2);
1565
- }
1566
- if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
1567
- result += '"';
1568
- } else if (prevSig === ":") {
1569
- result += "null";
1864
+ return out;
1865
+ }
1866
+ function compactDescription(text, maxChars) {
1867
+ const normalized = text.replace(/\s+/g, " ").trim();
1868
+ if (normalized.length <= maxChars) return normalized;
1869
+ if (maxChars <= 20) return normalized.slice(0, maxChars);
1870
+ const hardLimit = maxChars - 12;
1871
+ const boundary = findSemanticBoundary(normalized, hardLimit);
1872
+ const head = normalized.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd();
1873
+ return `${head} ...`;
1874
+ }
1875
+ function findSemanticBoundary(text, limit) {
1876
+ const punctuation = Math.max(
1877
+ text.lastIndexOf(". ", limit),
1878
+ text.lastIndexOf("; ", limit),
1879
+ text.lastIndexOf(": ", limit)
1880
+ );
1881
+ if (punctuation >= Math.floor(limit * 0.45)) return punctuation + 1;
1882
+ const comma = text.lastIndexOf(", ", limit);
1883
+ if (comma >= Math.floor(limit * 0.6)) return comma + 1;
1884
+ const space = text.lastIndexOf(" ", limit);
1885
+ return space >= Math.floor(limit * 0.6) ? space : limit;
1886
+ }
1887
+ function isRecord(value) {
1888
+ return !!value && typeof value === "object" && !Array.isArray(value);
1889
+ }
1890
+
1891
+ // src/utils/token-estimate.ts
1892
+ var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
1893
+ var CAL_ALPHA = 0.3;
1894
+ var CALIBRATION_GLOBAL_KEY = "__global__";
1895
+ var _cals = /* @__PURE__ */ new Map();
1896
+ function calState(key) {
1897
+ let state = _cals.get(key);
1898
+ if (!state) {
1899
+ state = { ratio: 1, count: 0, prevEst: 0 };
1900
+ _cals.set(key, state);
1570
1901
  }
1571
- for (let k = stack.length - 1; k >= 0; k--) {
1572
- result += stack[k] === "{" ? "}" : "]";
1902
+ return state;
1903
+ }
1904
+ var MIN_SAMPLES_FOR_CALIBRATION = 3;
1905
+ var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
1906
+ var ESTIMATE_CACHE_MAX_SIZE = 1e4;
1907
+ function getCachedEstimate(key, compute) {
1908
+ const existing = ESTIMATE_CACHE.get(key);
1909
+ if (existing !== void 0) return existing;
1910
+ if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
1911
+ for (const k of ESTIMATE_CACHE.keys()) {
1912
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
1913
+ ESTIMATE_CACHE.delete(k);
1914
+ }
1573
1915
  }
1574
- if (!tryParse(result).ok) {
1575
- const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
1576
- if (tryParse(patched).ok) result = patched;
1916
+ const estimate = compute(key);
1917
+ ESTIMATE_CACHE.set(key, estimate);
1918
+ return estimate;
1919
+ }
1920
+ function estimateToolInputTokens(input) {
1921
+ if (typeof input === "string") return RoughTokenEstimate(input);
1922
+ if (input === null || typeof input !== "object") {
1923
+ return RoughTokenEstimate(String(input));
1577
1924
  }
1578
- return result;
1925
+ return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
1579
1926
  }
1580
- var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
1581
- function endsWithInvalidEscape(str) {
1582
- const last = str[str.length - 1];
1583
- if (str[str.length - 2] !== "\\" || last === void 0) return false;
1584
- if (VALID_ESCAPE.has(last)) return false;
1585
- let backslashes = 0;
1586
- for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
1587
- return backslashes % 2 === 1;
1927
+ function estimateToolResultTokens(content) {
1928
+ if (typeof content === "string") return RoughTokenEstimate(content);
1929
+ return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
1588
1930
  }
1589
- function tryParse(s) {
1590
- try {
1591
- return { ok: true, value: JSON.parse(s) };
1592
- } catch {
1593
- return { ok: false };
1594
- }
1931
+ function estimateTextTokens(text) {
1932
+ return RoughTokenEstimate(text);
1595
1933
  }
1596
-
1597
- // src/utils/merge-models-payload.ts
1598
- function mergeModelsPayload(base, overlay) {
1599
- const out = {};
1600
- for (const [id, provider] of Object.entries(base)) {
1601
- out[id] = cloneProvider(provider);
1934
+ function computeMessageTokens(msg) {
1935
+ if (typeof msg.content === "string") return estimateTextTokens(msg.content);
1936
+ let total = 0;
1937
+ for (const b of msg.content) {
1938
+ if (b.type === "text") total += estimateTextTokens(b.text);
1939
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
1940
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
1941
+ else total += RoughTokenEstimate(JSON.stringify(b));
1602
1942
  }
1603
- for (const [id, ovProvider] of Object.entries(overlay)) {
1604
- const existing = out[id];
1605
- out[id] = existing ? mergeProvider(existing, ovProvider) : cloneProvider(ovProvider);
1943
+ return total;
1944
+ }
1945
+ function estimateMessageTokens(messages) {
1946
+ let total = 0;
1947
+ for (const m of messages) {
1948
+ if (typeof m._estTokens === "number" && m._estTokens > 0) {
1949
+ total += m._estTokens;
1950
+ continue;
1951
+ }
1952
+ total += computeMessageTokens(m);
1606
1953
  }
1607
- return out;
1954
+ return total;
1608
1955
  }
1609
- function mergeProvider(base, overlay) {
1610
- const models = {};
1611
- for (const [mid, m] of Object.entries(base.models ?? {})) {
1612
- models[mid] = { ...m };
1956
+ function estimateToolDefTokens(tool) {
1957
+ const cached = tool._estDefTokens;
1958
+ if (typeof cached === "number" && cached > 0) return cached;
1959
+ const compact = compactToolDefinitionForWire(tool);
1960
+ return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
1961
+ }
1962
+ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
1963
+ let messagesTokens = 0;
1964
+ if (typeof messages === "string") {
1965
+ messagesTokens = RoughTokenEstimate(messages);
1966
+ } else if (Array.isArray(messages)) {
1967
+ for (const m of messages) {
1968
+ if (typeof m === "object" && m !== null && "content" in m) {
1969
+ const cached = m._estTokens;
1970
+ if (typeof cached === "number" && cached > 0) {
1971
+ messagesTokens += cached;
1972
+ continue;
1973
+ }
1974
+ const content = m.content;
1975
+ if (typeof content === "string") {
1976
+ messagesTokens += RoughTokenEstimate(content);
1977
+ } else if (Array.isArray(content)) {
1978
+ for (const b of content) {
1979
+ if (typeof b === "object" && b !== null) {
1980
+ if (b.type === "text") {
1981
+ messagesTokens += RoughTokenEstimate(b.text);
1982
+ } else {
1983
+ messagesTokens += RoughTokenEstimate(JSON.stringify(b));
1984
+ }
1985
+ }
1986
+ }
1987
+ }
1988
+ }
1989
+ }
1613
1990
  }
1614
- for (const [mid, ovModel] of Object.entries(overlay.models ?? {})) {
1615
- const existing = models[mid];
1616
- models[mid] = existing ? mergeModel(existing, ovModel) : { ...ovModel };
1991
+ let systemTokens = 0;
1992
+ if (typeof systemPrompt === "string") {
1993
+ systemTokens = RoughTokenEstimate(systemPrompt);
1994
+ } else if (Array.isArray(systemPrompt)) {
1995
+ for (const b of systemPrompt) {
1996
+ if (typeof b === "object" && b !== null && b.type === "text") {
1997
+ systemTokens += RoughTokenEstimate(b.text);
1998
+ }
1999
+ }
2000
+ }
2001
+ let toolsTokens = 0;
2002
+ for (const t of tools) {
2003
+ toolsTokens += estimateToolDefTokens(t);
1617
2004
  }
2005
+ const total = messagesTokens + systemTokens + toolsTokens;
2006
+ calState(calibrationKey).prevEst = total;
1618
2007
  return {
1619
- ...base,
1620
- // Overlay scalar fields win when explicitly provided; otherwise keep base.
1621
- ...stripUndefined({
1622
- id: overlay.id,
1623
- name: overlay.name,
1624
- npm: overlay.npm,
1625
- api: overlay.api,
1626
- env: overlay.env,
1627
- doc: overlay.doc
1628
- }),
1629
- models
2008
+ messages: messagesTokens,
2009
+ systemPrompt: systemTokens,
2010
+ tools: toolsTokens,
2011
+ total
1630
2012
  };
1631
2013
  }
1632
- function mergeModel(base, overlay) {
1633
- const merged = { ...base, ...overlay };
1634
- if (base.limit || overlay.limit) {
1635
- merged.limit = { ...base.limit, ...overlay.limit };
1636
- }
1637
- if (base.cost || overlay.cost) {
1638
- merged.cost = { ...base.cost, ...overlay.cost };
1639
- }
1640
- if (base.modalities || overlay.modalities) {
1641
- merged.modalities = { ...base.modalities, ...overlay.modalities };
2014
+ function recordActualUsage(actualInputTokens, estimatedInputTokens, calibrationKey = CALIBRATION_GLOBAL_KEY) {
2015
+ if (actualInputTokens <= 0) return;
2016
+ const cal = calState(calibrationKey);
2017
+ const est = estimatedInputTokens ?? cal.prevEst;
2018
+ if (est <= 0) return;
2019
+ const sampleRatio = actualInputTokens / est;
2020
+ if (cal.count === 0) {
2021
+ cal.ratio = sampleRatio;
2022
+ } else {
2023
+ cal.ratio = CAL_ALPHA * sampleRatio + (1 - CAL_ALPHA) * cal.ratio;
1642
2024
  }
1643
- return merged;
2025
+ cal.ratio = Math.min(1.5, Math.max(0.5, cal.ratio));
2026
+ cal.count++;
1644
2027
  }
1645
- function cloneProvider(p) {
1646
- const models = {};
1647
- for (const [mid, m] of Object.entries(p.models ?? {})) {
1648
- models[mid] = { ...m };
2028
+ function getCalibrationState(calibrationKey = CALIBRATION_GLOBAL_KEY) {
2029
+ const cal = calState(calibrationKey);
2030
+ return {
2031
+ ratio: cal.ratio,
2032
+ count: cal.count,
2033
+ calibrated: cal.count >= MIN_SAMPLES_FOR_CALIBRATION
2034
+ };
2035
+ }
2036
+ function estimateRequestTokensCalibrated(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
2037
+ const result = estimateRequestTokens(messages, systemPrompt, tools, calibrationKey);
2038
+ const cal = calState(calibrationKey);
2039
+ if (cal.count >= MIN_SAMPLES_FOR_CALIBRATION) {
2040
+ const safeRatio = Math.min(1.5, Math.max(0.5, cal.ratio));
2041
+ return {
2042
+ messages: Math.round(result.messages * safeRatio),
2043
+ systemPrompt: Math.round(result.systemPrompt * safeRatio),
2044
+ tools: Math.round(result.tools * safeRatio),
2045
+ total: Math.round(result.total * safeRatio)
2046
+ };
1649
2047
  }
1650
- return { ...p, models };
2048
+ return result;
1651
2049
  }
1652
- function stripUndefined(obj) {
1653
- const out = {};
1654
- for (const [k, v] of Object.entries(obj)) {
1655
- if (v !== void 0) out[k] = v;
2050
+ function resetCalibration(calibrationKey) {
2051
+ if (calibrationKey === void 0) {
2052
+ _cals.clear();
2053
+ return;
1656
2054
  }
1657
- return out;
2055
+ _cals.delete(calibrationKey);
1658
2056
  }
1659
2057
 
1660
- // src/utils/merge-custom-models.ts
1661
- function mergeCustomModelDefs(providerCustomModels, configModels) {
1662
- const out = {};
1663
- if (providerCustomModels) {
1664
- for (const [id, def] of Object.entries(providerCustomModels)) {
1665
- out[id] = { ...def };
2058
+ // src/utils/tool-output-serializer.ts
2059
+ var DEFAULT_LIST_LIMIT = 500;
2060
+ var LOG_ENTRY_LIMIT = 200;
2061
+ var INLINE_LIMIT = 240;
2062
+ var GREP_FILE_LIMIT = 80;
2063
+ var GREP_MATCHES_PER_FILE = 3;
2064
+ var DIFF_INLINE_LINE_LIMIT = 260;
2065
+ var DIFF_HUNK_LIMIT = 8;
2066
+ var DIFF_HUNK_CONTEXT = 14;
2067
+ function createToolOutputSerializer(opts = {}) {
2068
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
2069
+ function serialize(value, context = {}) {
2070
+ if (typeof value === "string") return value;
2071
+ if (value === null || value === void 0) return "";
2072
+ if (typeof value === "object") {
2073
+ if (Array.isArray(value)) return value.map((item) => serialize(item)).join("\n");
2074
+ if (context.toolName) {
2075
+ const compact = renderToolObject(context.toolName, value, context.input);
2076
+ if (compact !== void 0) return compact;
2077
+ return renderGenericToolObject(context.toolName, value);
2078
+ }
2079
+ if ("text" in value) {
2080
+ const t = value.text;
2081
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
2082
+ }
2083
+ try {
2084
+ return JSON.stringify(value, null, 2);
2085
+ } catch {
2086
+ return String(value);
2087
+ }
1666
2088
  }
2089
+ return String(value);
1667
2090
  }
1668
- if (configModels) {
1669
- for (const [id, def] of Object.entries(configModels)) {
1670
- out[id] = { ...def };
2091
+ function enforceCap(text, remainingBudget) {
2092
+ if (remainingBudget <= 0) {
2093
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
2094
+ }
2095
+ const textBytes = Buffer.byteLength(text, "utf8");
2096
+ if (textBytes <= remainingBudget) {
2097
+ return { text, newBudget: remainingBudget - textBytes };
2098
+ }
2099
+ const marker = `
2100
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
2101
+ `;
2102
+ const markerBytes = Buffer.byteLength(marker, "utf8");
2103
+ const available = remainingBudget - markerBytes;
2104
+ if (available <= 0) {
2105
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
1671
2106
  }
2107
+ const half = Math.floor(available / 2);
2108
+ const first = text.slice(0, half);
2109
+ const second = text.slice(text.length - half);
2110
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
1672
2111
  }
1673
- if (Object.keys(out).length === 0) return void 0;
1674
- return out;
2112
+ return { serialize, enforceCap, capBytes };
1675
2113
  }
1676
- function isPrivateIPv4(addr) {
1677
- const parts = addr.split(".").map((p) => Number.parseInt(p, 10));
1678
- if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {
1679
- return true;
2114
+ function renderToolObject(toolName, obj, input) {
2115
+ if (toolName === "read" && typeof obj["text"] === "string") {
2116
+ return joinSections([
2117
+ renderHeader(
2118
+ `read: ${stringFromInput(input, "path") ?? stringField(obj, "path") ?? "<unknown>"}`,
2119
+ {
2120
+ offset: numberFromInput(input, "offset"),
2121
+ limit: numberFromInput(input, "limit"),
2122
+ total_lines: obj["total_lines"],
2123
+ encoding: obj["encoding"],
2124
+ truncated: obj["truncated"],
2125
+ cached: obj["cached"],
2126
+ note: obj["note"]
2127
+ }
2128
+ ),
2129
+ obj["text"]
2130
+ ]);
2131
+ }
2132
+ if (toolName === "grep" && Array.isArray(obj["matches"])) {
2133
+ const matches = stringArrayField(obj, "matches");
2134
+ return joinSections([
2135
+ renderHeader(`grep: ${stringFromInput(input, "pattern") ?? "<pattern>"}`, {
2136
+ path: stringFromInput(input, "path"),
2137
+ glob: stringFromInput(input, "glob"),
2138
+ mode: stringFromInput(input, "output_mode"),
2139
+ count: obj["count"],
2140
+ shown: matches.length,
2141
+ truncated: obj["truncated"],
2142
+ used: obj["used"]
2143
+ }),
2144
+ renderGrepMatches(matches, stringFromInput(input, "output_mode"))
2145
+ ]);
2146
+ }
2147
+ if (toolName === "patch" && Array.isArray(obj["files"])) {
2148
+ const files = stringArrayField(obj, "files");
2149
+ return joinSections([
2150
+ renderHeader("patch", {
2151
+ applied: obj["applied"],
2152
+ rejected: obj["rejected"],
2153
+ files: files.length,
2154
+ dry_run: obj["dry_run"]
2155
+ }),
2156
+ typeof obj["message"] === "string" ? `message:
2157
+ ${obj["message"]}` : void 0,
2158
+ files.length > 0 ? `files:
2159
+ ${renderStringList(files)}` : void 0
2160
+ ]);
2161
+ }
2162
+ if (toolName === "glob" && Array.isArray(obj["files"])) {
2163
+ const files = stringArrayField(obj, "files");
2164
+ return joinSections([
2165
+ renderHeader(
2166
+ `${toolName}: ${stringFromInput(input, "pattern") ?? stringFromInput(input, "files") ?? stringFromInput(input, "path") ?? ""}`.trim(),
2167
+ {
2168
+ path: stringFromInput(input, "path"),
2169
+ files: files.length,
2170
+ truncated: obj["truncated"]
2171
+ }
2172
+ ),
2173
+ renderStringList(files, "(no files)")
2174
+ ]);
2175
+ }
2176
+ if (toolName === "tree" && typeof obj["tree"] === "string") {
2177
+ return joinSections([
2178
+ renderHeader(
2179
+ `tree: ${stringField(obj, "path") ?? stringFromInput(input, "path") ?? "<cwd>"}`,
2180
+ {
2181
+ total_files: obj["total_files"],
2182
+ total_dirs: obj["total_dirs"],
2183
+ truncated: obj["truncated"]
2184
+ }
2185
+ ),
2186
+ obj["tree"]
2187
+ ]);
2188
+ }
2189
+ if (toolName === "fetch" && typeof obj["content"] === "string") {
2190
+ return joinSections([
2191
+ renderHeader(
2192
+ `fetch: ${stringField(obj, "url") ?? stringFromInput(input, "url") ?? "<url>"}`,
2193
+ {
2194
+ status: obj["status"],
2195
+ content_type: obj["content_type"]
2196
+ }
2197
+ ),
2198
+ obj["content"]
2199
+ ]);
2200
+ }
2201
+ if (toolName === "replace" && Array.isArray(obj["results"])) {
2202
+ const results = obj["results"].filter(isRecord2);
2203
+ const sections = [
2204
+ renderHeader("replace", {
2205
+ files_modified: obj["files_modified"],
2206
+ total_replacements: obj["total_replacements"],
2207
+ dry_run: obj["dry_run"]
2208
+ })
2209
+ ];
2210
+ for (const r of results.slice(0, DEFAULT_LIST_LIMIT)) {
2211
+ sections.push(
2212
+ joinSections([
2213
+ renderHeader(`file: ${stringField(r, "path") ?? "<unknown>"}`, {
2214
+ replacements: r["replacements"]
2215
+ }),
2216
+ typeof r["diff"] === "string" ? r["diff"] : void 0
2217
+ ])
2218
+ );
2219
+ }
2220
+ if (results.length > DEFAULT_LIST_LIMIT) {
2221
+ sections.push(`[serializer omitted ${results.length - DEFAULT_LIST_LIMIT} result item(s)]`);
2222
+ }
2223
+ return joinSections(sections);
2224
+ }
2225
+ if (typeof obj["diff"] === "string") {
2226
+ const diff = obj["diff"];
2227
+ return joinSections([
2228
+ renderHeader(toolName, {
2229
+ path: obj["path"],
2230
+ replacements: obj["replacements"],
2231
+ bytes_written: obj["bytes_written"],
2232
+ created: obj["created"],
2233
+ note: obj["note"],
2234
+ files: Array.isArray(obj["files"]) ? obj["files"].length : void 0,
2235
+ truncated: obj["truncated"],
2236
+ mode: obj["mode"]
2237
+ }),
2238
+ compactDiff(diff)
2239
+ ]);
2240
+ }
2241
+ if (toolName === "test" && typeof obj["output"] === "string") {
2242
+ return renderTestOutput(obj, input);
2243
+ }
2244
+ if ((toolName === "typecheck" || toolName === "lint" || toolName === "format") && typeof obj["output"] === "string") {
2245
+ return renderVerifierOutput(toolName, obj, input);
2246
+ }
2247
+ if (hasCommandOutputShape(obj)) {
2248
+ return renderCommandOutput(toolName, obj, input);
2249
+ }
2250
+ if (toolName === "json" && typeof obj["formatted"] === "string") {
2251
+ return joinSections([
2252
+ renderHeader("json", {
2253
+ type: obj["type"],
2254
+ keys: Array.isArray(obj["keys"]) ? obj["keys"].length : void 0,
2255
+ query: stringFromInput(input, "query"),
2256
+ error: obj["error"]
2257
+ }),
2258
+ obj["formatted"]
2259
+ ]);
2260
+ }
2261
+ if (toolName === "logs" && Array.isArray(obj["entries"])) {
2262
+ const entries = obj["entries"].filter(isRecord2);
2263
+ const lines = entries.slice(0, LOG_ENTRY_LIMIT).map((entry) => {
2264
+ const ts = stringField(entry, "timestamp") ?? "";
2265
+ const level = stringField(entry, "level") ?? "info";
2266
+ const message = stringField(entry, "message") ?? "";
2267
+ const source = stringField(entry, "source");
2268
+ return [ts, level, source, message].filter(Boolean).join(" ");
2269
+ });
2270
+ if (entries.length > LOG_ENTRY_LIMIT) {
2271
+ lines.push(`[serializer omitted ${entries.length - LOG_ENTRY_LIMIT} log entry item(s)]`);
2272
+ }
2273
+ return joinSections([
2274
+ renderHeader(`logs: ${stringField(obj, "source") ?? "<source>"}`, {
2275
+ total: obj["total"],
2276
+ shown: Math.min(entries.length, LOG_ENTRY_LIMIT),
2277
+ truncated: obj["truncated"],
2278
+ stream_mode: obj["stream_mode"]
2279
+ }),
2280
+ lines.length > 0 ? lines.join("\n") : "(no log entries)"
2281
+ ]);
2282
+ }
2283
+ if (toolName === "audit" && Array.isArray(obj["vulnerabilities"])) {
2284
+ const vulns = obj["vulnerabilities"].filter(isRecord2);
2285
+ const lines = vulns.slice(0, DEFAULT_LIST_LIMIT).map((v) => {
2286
+ const severity = stringField(v, "severity") ?? "unknown";
2287
+ const pkg = stringField(v, "package") ?? "<package>";
2288
+ const title = stringField(v, "title") ?? "";
2289
+ const url = stringField(v, "url");
2290
+ return [severity, pkg, title, url].filter(Boolean).join(" | ");
2291
+ });
2292
+ if (vulns.length > DEFAULT_LIST_LIMIT) {
2293
+ lines.push(`[serializer omitted ${vulns.length - DEFAULT_LIST_LIMIT} vulnerability item(s)]`);
2294
+ }
2295
+ return joinSections([
2296
+ renderHeader("audit", {
2297
+ exit_code: obj["exit_code"],
2298
+ total: obj["total"],
2299
+ summary: obj["summary"],
2300
+ truncated: obj["truncated"]
2301
+ }),
2302
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
2303
+ ]);
2304
+ }
2305
+ if (toolName === "outdated" && Array.isArray(obj["packages"])) {
2306
+ const packages = obj["packages"].filter(isRecord2);
2307
+ const lines = packages.slice(0, DEFAULT_LIST_LIMIT).map(
2308
+ (p) => [
2309
+ stringField(p, "name") ?? "<package>",
2310
+ `current=${stringField(p, "current") ?? "unknown"}`,
2311
+ `wanted=${stringField(p, "wanted") ?? "unknown"}`,
2312
+ `latest=${stringField(p, "latest") ?? "unknown"}`,
2313
+ stringField(p, "type")
2314
+ ].filter(Boolean).join(" | ")
2315
+ );
2316
+ if (packages.length > DEFAULT_LIST_LIMIT) {
2317
+ lines.push(`[serializer omitted ${packages.length - DEFAULT_LIST_LIMIT} package item(s)]`);
2318
+ }
2319
+ return joinSections([
2320
+ renderHeader("outdated", {
2321
+ exit_code: obj["exit_code"],
2322
+ total: obj["total"],
2323
+ truncated: obj["truncated"]
2324
+ }),
2325
+ lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
2326
+ ]);
2327
+ }
2328
+ return void 0;
2329
+ }
2330
+ function renderTestOutput(obj, input) {
2331
+ const exitCode = numberField(obj, "exit_code") ?? 0;
2332
+ const failed = numberField(obj, "failed") ?? 0;
2333
+ const output = stringField(obj, "output") ?? "";
2334
+ const header = renderHeader(`test: ${stringField(obj, "runner") ?? "runner"}`, {
2335
+ exit_code: obj["exit_code"],
2336
+ tests_run: obj["tests_run"],
2337
+ passed: obj["passed"],
2338
+ failed: obj["failed"],
2339
+ duration_ms: obj["duration_ms"],
2340
+ truncated: obj["truncated"],
2341
+ files: inputListSummary(input, "files"),
2342
+ grep: stringFromInput(input, "grep")
2343
+ });
2344
+ if (exitCode === 0 && failed === 0) {
2345
+ return joinSections([
2346
+ header,
2347
+ joinSections([
2348
+ "report:",
2349
+ `status=passed`,
2350
+ `tests_run=${obj["tests_run"] ?? 0}`,
2351
+ `passed=${obj["passed"] ?? 0}`,
2352
+ `failed=${obj["failed"] ?? 0}`,
2353
+ `duration_ms=${obj["duration_ms"] ?? 0}`,
2354
+ extractSpoolNote(output)
2355
+ ])
2356
+ ]);
2357
+ }
2358
+ return joinSections([
2359
+ header,
2360
+ `error_context:
2361
+ ${compactFailureOutput(output || "(no runner output)")}`
2362
+ ]);
2363
+ }
2364
+ function renderVerifierOutput(toolName, obj, input) {
2365
+ const exitCode = numberField(obj, "exit_code") ?? 0;
2366
+ const errors = numberField(obj, "errors") ?? 0;
2367
+ const warnings = numberField(obj, "warnings") ?? 0;
2368
+ const output = stringField(obj, "output") ?? "";
2369
+ const changed = numberField(obj, "files_changed") ?? 0;
2370
+ const header = renderHeader(toolName, {
2371
+ exit_code: obj["exit_code"],
2372
+ errors: obj["errors"],
2373
+ warnings: obj["warnings"],
2374
+ files_checked: obj["files_checked"],
2375
+ files_changed: obj["files_changed"],
2376
+ fix_applied: obj["fix_applied"],
2377
+ fixer: obj["fixer"],
2378
+ linter: obj["linter"],
2379
+ project: obj["project"],
2380
+ truncated: obj["truncated"],
2381
+ files: inputListSummary(input, "files"),
2382
+ cwd: stringFromInput(input, "cwd")
2383
+ });
2384
+ if (exitCode === 0 && errors === 0 && (toolName !== "format" || changed === 0)) {
2385
+ return joinSections([
2386
+ header,
2387
+ joinSections([
2388
+ "report:",
2389
+ "status=passed",
2390
+ `errors=${errors}`,
2391
+ `warnings=${warnings}`,
2392
+ toolName === "format" ? `files_changed=${changed}` : void 0,
2393
+ extractSpoolNote(output)
2394
+ ])
2395
+ ]);
2396
+ }
2397
+ if (exitCode === 0 && toolName === "format") {
2398
+ return joinSections([
2399
+ header,
2400
+ joinSections([
2401
+ "report:",
2402
+ "status=changed",
2403
+ `files_changed=${changed}`,
2404
+ extractSpoolNote(output)
2405
+ ])
2406
+ ]);
2407
+ }
2408
+ return joinSections([
2409
+ header,
2410
+ `error_context:
2411
+ ${compactFailureOutput(output || "(no verifier output)")}`
2412
+ ]);
2413
+ }
2414
+ function renderGrepMatches(matches, mode) {
2415
+ if (matches.length === 0) return "(no matches)";
2416
+ if (mode === "files_with_matches") return renderStringList(matches, "(no files)");
2417
+ if (mode === "count") return renderStringList(matches, "(no counts)");
2418
+ const groups = /* @__PURE__ */ new Map();
2419
+ const passthrough = [];
2420
+ for (const match of matches) {
2421
+ const parsed = parseGrepContentLine(match);
2422
+ if (!parsed) {
2423
+ passthrough.push(match);
2424
+ continue;
2425
+ }
2426
+ const list = groups.get(parsed.file) ?? [];
2427
+ list.push(`${parsed.line}:${parsed.text}`);
2428
+ groups.set(parsed.file, list);
2429
+ }
2430
+ if (groups.size === 0) return renderStringList(matches, "(no matches)");
2431
+ const sections = [];
2432
+ let fileIndex = 0;
2433
+ for (const [file, lines] of groups) {
2434
+ fileIndex++;
2435
+ if (fileIndex > GREP_FILE_LIMIT) break;
2436
+ const shown = lines.slice(0, GREP_MATCHES_PER_FILE);
2437
+ sections.push(
2438
+ `${file} (${lines.length} match(es), showing ${shown.length})
2439
+ ${shown.join("\n")}`
2440
+ );
1680
2441
  }
1681
- const [a, b, c] = parts;
1682
- if (a === 0) return true;
1683
- if (a === 10) return true;
1684
- if (a === 127) return true;
1685
- if (a === 169 && b === 254) return true;
1686
- if (a === 172 && b >= 16 && b <= 31) return true;
1687
- if (a === 192 && b === 168) return true;
1688
- if (a === 192 && b === 0 && c === 0) return true;
1689
- if (a === 100 && b >= 64 && b <= 127) return true;
1690
- if (a >= 224) return true;
1691
- return false;
1692
- }
1693
- function isPrivateIPv6(raw) {
1694
- const lower = raw.toLowerCase();
1695
- if (lower === "::" || lower === "::1") return true;
1696
- const groups = expandIPv6(lower);
1697
- if (!groups) return true;
1698
- if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) {
1699
- const a = (groups[6] ?? 0) >> 8;
1700
- const b = (groups[6] ?? 0) & 255;
1701
- const c = (groups[7] ?? 0) >> 8;
1702
- const d = (groups[7] ?? 0) & 255;
1703
- return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
2442
+ if (groups.size > GREP_FILE_LIMIT) {
2443
+ sections.push(`[serializer omitted ${groups.size - GREP_FILE_LIMIT} file group(s)]`);
2444
+ }
2445
+ if (passthrough.length > 0) {
2446
+ sections.push(`ungrouped:
2447
+ ${renderStringList(passthrough, "", 50)}`);
2448
+ }
2449
+ return sections.join("\n");
2450
+ }
2451
+ function parseGrepContentLine(line) {
2452
+ const match = /^(.+?):(\d+):(.*)$/.exec(line);
2453
+ if (!match?.[1] || !match[2]) return void 0;
2454
+ return { file: match[1], line: match[2], text: match[3] ?? "" };
2455
+ }
2456
+ function compactDiff(diff) {
2457
+ const lines = diff.split(/\r?\n/);
2458
+ if (lines.length <= DIFF_INLINE_LINE_LIMIT) return diff;
2459
+ const fileCount = Math.max(
2460
+ new Set(
2461
+ lines.map(
2462
+ (line) => /^diff --git\s+a\/(.+?)\s+b\//.exec(line)?.[1] ?? /^---\s+(.+)/.exec(line)?.[1]
2463
+ ).filter(Boolean)
2464
+ ).size,
2465
+ 0
2466
+ );
2467
+ const hunks = lines.filter((line) => line.startsWith("@@")).length;
2468
+ const added = lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
2469
+ const removed = lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
2470
+ const selected = /* @__PURE__ */ new Set();
2471
+ let hunkCount = 0;
2472
+ for (let i = 0; i < lines.length; i++) {
2473
+ const line = lines[i] ?? "";
2474
+ if (line.startsWith("diff --git") || line.startsWith("--- ") || line.startsWith("+++ ")) {
2475
+ selected.add(i);
2476
+ continue;
2477
+ }
2478
+ if (!line.startsWith("@@")) continue;
2479
+ if (hunkCount >= DIFF_HUNK_LIMIT) continue;
2480
+ hunkCount++;
2481
+ for (let j = i; j <= Math.min(lines.length - 1, i + DIFF_HUNK_CONTEXT); j++) {
2482
+ selected.add(j);
2483
+ }
1704
2484
  }
1705
- const high = groups[0] ?? 0;
1706
- if ((high & 65024) === 64512) return true;
1707
- if ((high & 65472) === 65152) return true;
1708
- if ((high & 65280) === 65280) return true;
1709
- return false;
1710
- }
1711
- function expandIPv6(addr) {
1712
- const parts = addr.split("::");
1713
- if (parts.length > 2) return null;
1714
- const parseGroups = (s) => {
1715
- if (s === "") return [];
1716
- const out = [];
1717
- for (const g of s.split(":")) {
1718
- if (g.length === 0 || g.length > 4) return null;
1719
- const n = Number.parseInt(g, 16);
1720
- if (Number.isNaN(n) || n < 0 || n > 65535) return null;
1721
- out.push(n);
2485
+ if (selected.size === 0) {
2486
+ return joinSections([
2487
+ renderHeader("diff_summary", {
2488
+ files: fileCount,
2489
+ hunks,
2490
+ added,
2491
+ removed,
2492
+ lines: lines.length
2493
+ }),
2494
+ lines.slice(0, DIFF_INLINE_LINE_LIMIT).join("\n"),
2495
+ `[serializer omitted ${Math.max(0, lines.length - DIFF_INLINE_LINE_LIMIT)} diff line(s)]`
2496
+ ]);
2497
+ }
2498
+ const excerpt = [];
2499
+ let previous = -1;
2500
+ for (const index of [...selected].sort((a, b) => a - b)) {
2501
+ if (index > previous + 1) {
2502
+ const omitted = previous === -1 ? index : index - previous - 1;
2503
+ excerpt.push(`[serializer omitted ${omitted} diff line(s)]`);
2504
+ }
2505
+ excerpt.push(lines[index] ?? "");
2506
+ previous = index;
2507
+ }
2508
+ const trailing = lines.length - previous - 1;
2509
+ if (trailing > 0) excerpt.push(`[serializer omitted ${trailing} trailing diff line(s)]`);
2510
+ return joinSections([
2511
+ renderHeader("diff_summary", {
2512
+ files: fileCount,
2513
+ hunks,
2514
+ shown_hunks: Math.min(hunks, DIFF_HUNK_LIMIT),
2515
+ added,
2516
+ removed,
2517
+ lines: lines.length
2518
+ }),
2519
+ excerpt.join("\n")
2520
+ ]);
2521
+ }
2522
+ function compactFailureOutput(output) {
2523
+ const lines = output.split(/\r?\n/);
2524
+ if (lines.length <= 260) return output.trimEnd();
2525
+ const selected = /* @__PURE__ */ new Set();
2526
+ const marker = /\b(fail|failed|failure|error|exception|assertionerror|expected|received|actual|timeout|stack)\b/i;
2527
+ let markerHits = 0;
2528
+ for (let i = 0; i < lines.length; i++) {
2529
+ if (!marker.test(lines[i] ?? "")) continue;
2530
+ markerHits++;
2531
+ for (let j = Math.max(0, i - 4); j <= Math.min(lines.length - 1, i + 10); j++) {
2532
+ selected.add(j);
1722
2533
  }
1723
- return out;
1724
- };
1725
- if (parts.length === 1) {
1726
- const groups = parseGroups(parts[0] ?? "");
1727
- if (!groups || groups.length !== 8) return null;
1728
- return groups;
1729
2534
  }
1730
- const head = parseGroups(parts[0] ?? "");
1731
- const tail = parseGroups(parts[1] ?? "");
1732
- if (!head || !tail) return null;
1733
- const fill = 8 - head.length - tail.length;
1734
- if (fill < 0) return null;
1735
- return [...head, ...new Array(fill).fill(0), ...tail];
1736
- }
1737
- async function assertNotPrivateHost(hostname) {
1738
- const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1739
- if (host === "localhost" || host.endsWith(".localhost")) {
1740
- throw new Error("fetch: blocked localhost target");
2535
+ if (markerHits === 0) {
2536
+ return lines.slice(-220).join("\n").trimEnd();
1741
2537
  }
1742
- const ipVersion = net.isIP(host);
1743
- if (ipVersion === 4) {
1744
- if (isPrivateIPv4(host)) {
1745
- throw new Error(`fetch: blocked private/loopback address "${host}"`);
2538
+ const ordered = [...selected].sort((a, b) => a - b);
2539
+ const out = [];
2540
+ let previous = -1;
2541
+ for (const index of ordered) {
2542
+ if (index > previous + 1) {
2543
+ const omitted = previous === -1 ? index : index - previous - 1;
2544
+ out.push(`[serializer omitted ${omitted} line(s)]`);
1746
2545
  }
1747
- } else if (ipVersion === 6) {
1748
- if (isPrivateIPv6(host)) {
1749
- throw new Error(`fetch: blocked private/loopback address "${host}"`);
2546
+ out.push(lines[index] ?? "");
2547
+ previous = index;
2548
+ }
2549
+ return out.join("\n").trimEnd();
2550
+ }
2551
+ function extractSpoolNote(output) {
2552
+ return output.split(/\r?\n/).find((line) => line.startsWith("[output truncated") && line.includes("full"));
2553
+ }
2554
+ function hasCommandOutputShape(obj) {
2555
+ return typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string" || typeof obj["output"] === "string" || typeof obj["exitCode"] === "number" || typeof obj["exit_code"] === "number";
2556
+ }
2557
+ function renderCommandOutput(toolName, obj, input) {
2558
+ const command = stringField(obj, "command") ?? stringFromInput(input, "command");
2559
+ const args = stringArrayField(obj, "args");
2560
+ const commandLine = command ? [command, ...args].join(" ") : void 0;
2561
+ const output = stringField(obj, "output");
2562
+ const stdout = stringField(obj, "stdout");
2563
+ const stderr = stringField(obj, "stderr");
2564
+ return joinSections([
2565
+ renderHeader(commandLine ? `${toolName}: ${commandLine}` : toolName, {
2566
+ exit_code: obj["exit_code"] ?? obj["exitCode"],
2567
+ timed_out: obj["timed_out"],
2568
+ pid: obj["pid"],
2569
+ allowed: obj["allowed"],
2570
+ truncated: obj["truncated"],
2571
+ runner: obj["runner"],
2572
+ linter: obj["linter"],
2573
+ fixer: obj["fixer"],
2574
+ project: obj["project"],
2575
+ tests_run: obj["tests_run"],
2576
+ passed: obj["passed"],
2577
+ failed: obj["failed"],
2578
+ duration_ms: obj["duration_ms"],
2579
+ errors: obj["errors"],
2580
+ warnings: obj["warnings"],
2581
+ files_checked: obj["files_checked"],
2582
+ files_changed: obj["files_changed"],
2583
+ fix_applied: obj["fix_applied"]
2584
+ }),
2585
+ stringField(obj, "error") ? `error:
2586
+ ${stringField(obj, "error")}` : void 0,
2587
+ output ? `output:
2588
+ ${output}` : void 0,
2589
+ stdout ? `stdout:
2590
+ ${stdout}` : void 0,
2591
+ stderr ? `stderr:
2592
+ ${stderr}` : void 0
2593
+ ]);
2594
+ }
2595
+ function renderGenericToolObject(toolName, obj) {
2596
+ const scalars = {};
2597
+ const blocks = [];
2598
+ for (const [key, value] of Object.entries(obj)) {
2599
+ if (value === void 0) continue;
2600
+ if (isScalar(value)) {
2601
+ const inline = String(value);
2602
+ if (inline.length <= INLINE_LIMIT && !inline.includes("\n")) {
2603
+ scalars[key] = value;
2604
+ } else {
2605
+ blocks.push(`${key}:
2606
+ ${inline}`);
2607
+ }
2608
+ continue;
1750
2609
  }
1751
- } else {
1752
- try {
1753
- const records = await dns.lookup(host, { all: true });
1754
- for (const r of records) {
1755
- const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
1756
- if (bad) {
1757
- throw new Error(`fetch: resolved to private address ${r.address}`);
1758
- }
2610
+ if (Array.isArray(value)) {
2611
+ if (value.every((item) => typeof item === "string")) {
2612
+ blocks.push(`${key}:
2613
+ ${renderStringList(value)}`);
2614
+ } else {
2615
+ blocks.push(`${key}:
2616
+ ${renderUnknownList(value)}`);
1759
2617
  }
1760
- } catch (err) {
1761
- if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
2618
+ continue;
1762
2619
  }
2620
+ blocks.push(`${key}: ${clipInline(oneLineJson(value))}`);
2621
+ }
2622
+ return joinSections([renderHeader(toolName, scalars), ...blocks]);
2623
+ }
2624
+ function renderHeader(label, fields) {
2625
+ const parts = Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${key}=${clipInline(formatInlineValue(value))}`);
2626
+ return parts.length > 0 ? `${label} (${parts.join(" ")})` : label;
2627
+ }
2628
+ function renderStringList(items, empty = "", limit = DEFAULT_LIST_LIMIT) {
2629
+ if (items.length === 0) return empty;
2630
+ const shown = items.slice(0, limit);
2631
+ const omitted = items.length - shown.length;
2632
+ return [
2633
+ ...shown,
2634
+ ...omitted > 0 ? [`[serializer omitted ${omitted} item(s); narrow the request for more]`] : []
2635
+ ].join("\n");
2636
+ }
2637
+ function renderUnknownList(items, limit = DEFAULT_LIST_LIMIT) {
2638
+ const shown = items.slice(0, limit).map((item) => clipInline(oneLineJson(item), 1e3));
2639
+ const omitted = items.length - shown.length;
2640
+ if (omitted > 0)
2641
+ shown.push(`[serializer omitted ${omitted} item(s); narrow the request for more]`);
2642
+ return shown.join("\n");
2643
+ }
2644
+ function joinSections(sections) {
2645
+ return sections.map((section) => typeof section === "string" ? section.trimEnd() : void 0).filter((section) => !!section).join("\n");
2646
+ }
2647
+ function formatInlineValue(value) {
2648
+ if (Array.isArray(value)) return `[${value.map(formatInlineValue).join(",")}]`;
2649
+ if (isScalar(value)) return String(value);
2650
+ return oneLineJson(value);
2651
+ }
2652
+ function clipInline(value, max = INLINE_LIMIT) {
2653
+ const compact = value.replace(/\s+/g, " ").trim();
2654
+ return compact.length <= max ? compact : `${compact.slice(0, max - 15)}...(${compact.length} chars)`;
2655
+ }
2656
+ function oneLineJson(value) {
2657
+ try {
2658
+ return JSON.stringify(value);
2659
+ } catch {
2660
+ return String(value);
1763
2661
  }
1764
2662
  }
2663
+ function stringField(obj, key) {
2664
+ const value = obj[key];
2665
+ return typeof value === "string" ? value : void 0;
2666
+ }
2667
+ function numberField(obj, key) {
2668
+ const value = obj[key];
2669
+ return typeof value === "number" ? value : void 0;
2670
+ }
2671
+ function stringArrayField(obj, key) {
2672
+ const value = obj[key];
2673
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2674
+ }
2675
+ function stringFromInput(input, key) {
2676
+ if (!isRecord2(input)) return void 0;
2677
+ const value = input[key];
2678
+ return typeof value === "string" ? value : void 0;
2679
+ }
2680
+ function numberFromInput(input, key) {
2681
+ if (!isRecord2(input)) return void 0;
2682
+ const value = input[key];
2683
+ return typeof value === "number" ? value : void 0;
2684
+ }
2685
+ function inputListSummary(input, key) {
2686
+ if (!isRecord2(input)) return void 0;
2687
+ const value = input[key];
2688
+ if (typeof value === "string") return value;
2689
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string").join(",");
2690
+ return void 0;
2691
+ }
2692
+ function isRecord2(value) {
2693
+ return !!value && typeof value === "object" && !Array.isArray(value);
2694
+ }
2695
+ function isScalar(value) {
2696
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
2697
+ }
2698
+ function projectHash(absRoot) {
2699
+ return createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 12);
2700
+ }
2701
+ function projectSlug(absRoot) {
2702
+ const base = slugify(path3.basename(absRoot));
2703
+ const hash = createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 6);
2704
+ return `${base}-${hash}`;
2705
+ }
2706
+ function slugify(name) {
2707
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
2708
+ }
2709
+ function wstackGlobalRoot() {
2710
+ const fromEnv = process.env["WRONGSTACK_HOME"];
2711
+ if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
2712
+ return path3.join(os.homedir(), ".wrongstack");
2713
+ }
2714
+ function resolveWstackPaths(opts) {
2715
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path3.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2716
+ const hash = projectHash(opts.projectRoot);
2717
+ const slug = projectSlug(opts.projectRoot);
2718
+ const projectDir = path3.join(globalRoot, "projects", slug);
2719
+ return {
2720
+ globalRoot,
2721
+ configDir: globalRoot,
2722
+ globalConfig: path3.join(globalRoot, "config.json"),
2723
+ secretsKey: path3.join(globalRoot, ".key"),
2724
+ globalMemory: path3.join(globalRoot, "memory.md"),
2725
+ globalSkills: path3.join(globalRoot, "skills"),
2726
+ globalPrompts: path3.join(globalRoot, "prompts"),
2727
+ cacheDir: path3.join(globalRoot, "cache"),
2728
+ modelsCache: path3.join(globalRoot, "cache", "models.dev.json"),
2729
+ modelsOverlayCache: path3.join(globalRoot, "cache", "models-overlay.json"),
2730
+ historyFile: path3.join(globalRoot, "history"),
2731
+ logFile: path3.join(globalRoot, "logs", "wrongstack.log"),
2732
+ projectDir,
2733
+ projectCodebaseIndex: path3.join(projectDir, "codebase-index"),
2734
+ projectMemory: path3.join(projectDir, "memory.md"),
2735
+ projectSessions: path3.join(projectDir, "sessions"),
2736
+ projectTrust: path3.join(projectDir, "trust.json"),
2737
+ projectMeta: path3.join(projectDir, "meta.json"),
2738
+ projectLocalConfig: path3.join(projectDir, "config.local.json"),
2739
+ inProjectConfig: path3.join(opts.projectRoot, ".wrongstack", "config.json"),
2740
+ inProjectAgentsFile: path3.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2741
+ inProjectSkills: path3.join(opts.projectRoot, ".wrongstack", "skills"),
2742
+ inProjectWorktrees: path3.join(opts.projectRoot, ".wrongstack", "worktrees"),
2743
+ projectHash: hash,
2744
+ projectSlug: slug,
2745
+ projectGoal: path3.join(projectDir, "goal.json"),
2746
+ projectSpecs: path3.join(projectDir, "specs"),
2747
+ projectTaskGraphs: path3.join(projectDir, "task-graphs"),
2748
+ projectSddSession: path3.join(projectDir, "sdd-session.json"),
2749
+ projectPlan: path3.join(projectDir, "plan.json"),
2750
+ projectAutophase: path3.join(projectDir, "autophase"),
2751
+ syncConfig: path3.join(globalRoot, "sync.json"),
2752
+ projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
2753
+ };
2754
+ }
1765
2755
 
1766
- export { FORBIDDEN_PROTO_KEYS, assertNever, assertNotPrivateHost, atomicWrite, buildChildEnv, color, compileGlob, compileUserRegex, completePartialObject, computeMessageTokens, computeTaskItemProgress, createToolOutputSerializer, deepMerge, detectNewlineStyle, ensureDir, estimateMessageTokens, estimateRequestTokens, estimateRequestTokensCalibrated, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, expandGlob, expandIPv6, expectDefined, formatTaskList, formatTaskProgress, formatTodosList, getCalibrationState, getTermSize, isInteractive, isPrimitiveArray, isPrivateIPv4, isPrivateIPv6, isStdinTTY, isStdoutTTY, matchAny, matchGlob, mergeCustomModelDefs, mergeModelsPayload, normalizeToLf, onResize, projectHash, projectSlug, recordActualUsage, repairToolUseAdjacency, resetCalibration, resolveWstackPaths, safeParse, safeStringify, sanitizeJsonString, setOutputLineGuard, setRawMode, sleep, stripAnsi, toErrorMessage, toStyle, truncate, unifiedDiff, validateAgainstSchema, withFileLock, writeErr, writeOut, wstackGlobalRoot };
2756
+ export { FORBIDDEN_PROTO_KEYS, assertNever, assertNotPrivateHost, atomicWrite, buildChildEnv, buildContextEvidenceDigest, color, compactSchemaDescriptions, compactToolDefinitionForWire, compileGlob, compileUserRegex, completePartialObject, computeMessageTokens, computeTaskItemProgress, createContextEvidenceState, createToolOutputSerializer, deepMerge, detectNewlineStyle, ensureDir, estimateMessageTokens, estimateRequestTokens, estimateRequestTokensCalibrated, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, expandGlob, expandIPv6, expectDefined, formatTaskList, formatTaskProgress, formatTodosList, getCalibrationState, getTermSize, isInteractive, isPrimitiveArray, isPrivateIPv4, isPrivateIPv6, isStdinTTY, isStdoutTTY, markAssistantReferencedEvidence, matchAny, matchGlob, mergeCustomModelDefs, mergeModelsPayload, normalizeToLf, onResize, projectHash, projectSlug, recordActualUsage, recordToolOutputEvidence, recordUserIntentEvidence, repairToolUseAdjacency, repeatedReadPressure, resetCalibration, resolveWstackPaths, safeParse, safeStringify, sanitizeJsonString, setOutputLineGuard, setRawMode, sleep, stripAnsi, toErrorMessage, toStyle, truncate, unifiedDiff, validateAgainstSchema, withFileLock, writeErr, writeOut, wstackGlobalRoot };
1767
2757
  //# sourceMappingURL=index.js.map
1768
2758
  //# sourceMappingURL=index.js.map