@wrongstack/tools 0.283.0 → 0.284.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 (59) hide show
  1. package/dist/audit.js.map +1 -1
  2. package/dist/bash.js +4 -0
  3. package/dist/bash.js.map +1 -1
  4. package/dist/builtin.js +710 -259
  5. package/dist/builtin.js.map +1 -1
  6. package/dist/codebase-index/index.d.ts +11 -0
  7. package/dist/codebase-index/index.js +27 -8
  8. package/dist/codebase-index/index.js.map +1 -1
  9. package/dist/codebase-index/worker.js +25 -6
  10. package/dist/codebase-index/worker.js.map +1 -1
  11. package/dist/diff.js.map +1 -1
  12. package/dist/document.js.map +1 -1
  13. package/dist/edit.d.ts +43 -0
  14. package/dist/edit.js +386 -60
  15. package/dist/edit.js.map +1 -1
  16. package/dist/{exec-Ca3fnpUh.d.ts → exec-2OKoT6tk.d.ts} +1 -1
  17. package/dist/exec.d.ts +1 -1
  18. package/dist/exec.js +13 -14
  19. package/dist/exec.js.map +1 -1
  20. package/dist/fetch.js.map +1 -1
  21. package/dist/format.js.map +1 -1
  22. package/dist/git.js.map +1 -1
  23. package/dist/glob.js +10 -1
  24. package/dist/glob.js.map +1 -1
  25. package/dist/grep.js +4 -0
  26. package/dist/grep.js.map +1 -1
  27. package/dist/index.d.ts +13 -2
  28. package/dist/index.js +755 -274
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js.map +1 -1
  31. package/dist/json.js.map +1 -1
  32. package/dist/lint.js.map +1 -1
  33. package/dist/logs.js.map +1 -1
  34. package/dist/next-steps.d.ts +66 -0
  35. package/dist/next-steps.js +116 -0
  36. package/dist/next-steps.js.map +1 -0
  37. package/dist/outdated.js.map +1 -1
  38. package/dist/pack.js +710 -259
  39. package/dist/pack.js.map +1 -1
  40. package/dist/patch.js +43 -0
  41. package/dist/patch.js.map +1 -1
  42. package/dist/read.js +13 -4
  43. package/dist/read.js.map +1 -1
  44. package/dist/replace.js +14 -0
  45. package/dist/replace.js.map +1 -1
  46. package/dist/scaffold.js.map +1 -1
  47. package/dist/test.js.map +1 -1
  48. package/dist/tool-diff.d.ts +88 -0
  49. package/dist/tool-diff.js +229 -0
  50. package/dist/tool-diff.js.map +1 -0
  51. package/dist/tool-summary.d.ts +24 -0
  52. package/dist/tool-summary.js +208 -0
  53. package/dist/tool-summary.js.map +1 -0
  54. package/dist/tree.js.map +1 -1
  55. package/dist/typecheck.js.map +1 -1
  56. package/dist/write.d.ts +6 -0
  57. package/dist/write.js +118 -19
  58. package/dist/write.js.map +1 -1
  59. package/package.json +15 -2
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import * as fs from 'node:fs';
2
2
  import { statSync, mkdirSync, createWriteStream } from 'node:fs';
3
- import * as path3 from 'node:path';
3
+ import * as path12 from 'node:path';
4
4
  import { resolve, sep, dirname, join } from 'node:path';
5
5
  import { spawn, execFileSync } from 'node:child_process';
6
6
  import * as Core from '@wrongstack/core';
7
- import { SKILL_LIMITS, buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge, addLinkToTask, addNoteToTask, updateCheckOnTask, addCheckToTask, updateGoalMetricOnTask, addGoalMetricToTask, addDependency, getKanbanQueueHealth, listKanbanEvents, recoverStaleTaskAssignments, heartbeatTaskAssignment, updateTaskAssignment, assignTask, releaseTaskClaim, claimReadyTask, getTaskChain, setTaskChain, removeTask, moveTask, updateTask, getTask, transferTaskToBoard, copyTaskToBoard, mergeTasks, splitTask, addTask, removeColumn, updateColumn, addColumn, getKanbanOrchestrationSnapshot, listReadyTasks, searchKanban, deserializeTaskGraph, syncBoardFromTaskGraph, exportBoardToTaskGraph, serializeTaskGraph, exportBoardAsMarkdown, generateBoardFromDescription, createBoard, parseLinesIntoTasks, getBoard, removeBoard, duplicateBoard, updateBoard, listBoards, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, FsError, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, stripFrontmatter, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
7
+ import { SKILL_LIMITS, buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge, deserializeTaskGraph, serializeTaskGraph, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, FsError, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, stripFrontmatter, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
8
8
  import * as fs2 from 'node:fs/promises';
9
9
  import * as os2 from 'node:os';
10
+ import { randomUUID, createHash } from 'node:crypto';
10
11
  import { toErrorMessage as toErrorMessage$3 } from '@wrongstack/core/utils';
11
12
  import { createRequire } from 'node:module';
12
13
  import { fileURLToPath } from 'node:url';
@@ -17,7 +18,7 @@ import * as dns from 'node:dns/promises';
17
18
  import * as net from 'node:net';
18
19
  import { Agent, fetch } from 'undici';
19
20
  import TurndownService from 'turndown';
20
- import { randomUUID } from 'node:crypto';
21
+ import { addLinkToTask, addNoteToTask, updateCheckOnTask, addCheckToTask, updateGoalMetricOnTask, addGoalMetricToTask, addDependency, getKanbanQueueHealth, listKanbanEvents, recoverStaleTaskAssignments, heartbeatTaskAssignment, updateTaskAssignment, assignTask, releaseTaskClaim, claimReadyTask, getTaskChain, setTaskChain, removeTask, moveTask, updateTask, getTask, transferTaskToBoard, copyTaskToBoard, mergeTasks, splitTask, addTask, removeColumn, updateColumn, addColumn, getKanbanOrchestrationSnapshot, listReadyTasks, searchKanban, syncBoardFromTaskGraph, exportBoardToTaskGraph, exportBoardAsMarkdown, generateBoardFromDescription, createBoard, parseLinesIntoTasks, getBoard, removeBoard, duplicateBoard, updateBoard, listBoards } from '@wrongstack/kanban';
21
22
 
22
23
  // src/_danger-detect.ts
23
24
  var argHas = (args, value) => args.includes(value);
@@ -230,23 +231,18 @@ var RULES = [
230
231
  },
231
232
  reason: "inline script evaluation (-c / -e / --eval)"
232
233
  },
233
- // ----- pipe-to-shell (cautionwell-known exfil pattern) -----
234
+ // ----- pipe-to-shell (destructivedownload-and-run pattern) -----
234
235
  // The classic `curl https://... | sh` download-and-run vector. Detected by
235
- // looking for a known fetcher followed by a shell sink. We use a simple
236
- // substring scan; false positives are limited because both tokens must
237
- // appear in the same argv.
236
+ // looking for a known fetcher piped into a shell or expression evaluator.
237
+ // A shell command passed through `bash -c` / `pwsh -Command` arrives as one
238
+ // argv string, so scan the reconstructed argv text rather than individual
239
+ // tokens only.
238
240
  {
239
241
  id: "pipe-to-shell",
240
- level: "caution",
241
- test: (_cmd, args) => {
242
- const hasFetcher = args.some(
243
- (a) => /^(curl|wget|fetch|httpie|http)$/i.test(a) || a.startsWith("curl") || a.startsWith("wget")
244
- );
245
- const hasShellSink = args.some(
246
- (a) => a === "sh" || a === "bash" || a === "zsh" || a === "fish" || a === "pwsh" || a === "powershell" || a.endsWith("/sh") || a.endsWith("/bash") || a.endsWith("/zsh") || a.endsWith("/pwsh")
247
- );
248
- return hasFetcher && hasShellSink;
249
- },
242
+ level: "destructive",
243
+ test: (cmd, args) => /\b(?:curl|wget|fetch|httpie|http|irm|iwr|Invoke-WebRequest|Invoke-RestMethod)\b[\s\S]{0,300}\|\s*(?:sudo\s+)?(?:sh|bash|zsh|fish|pwsh|powershell|iex|Invoke-Expression)\b/i.test(
244
+ [cmd, ...args].join(" ")
245
+ ),
250
246
  reason: "network fetch piped to a shell (download-and-run pattern)"
251
247
  },
252
248
  // ----- privilege escalation (caution) -----
@@ -306,13 +302,13 @@ function levelRank(level) {
306
302
  }
307
303
  function resolveWin32Command(cmd) {
308
304
  if (process.platform !== "win32") return cmd;
309
- if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd.replace(/\//g, "\\"))) {
305
+ if (cmd.includes("/") || cmd.includes("\\") || path12.extname(cmd.replace(/\//g, "\\"))) {
310
306
  return cmd;
311
307
  }
312
308
  const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
313
- const pathDirs = (process.env["PATH"] ?? "").split(path3.delimiter);
309
+ const pathDirs = (process.env["PATH"] ?? "").split(path12.delimiter);
314
310
  for (const dir of pathDirs) {
315
- const base = path3.join(dir, cmd);
311
+ const base = path12.join(dir, cmd);
316
312
  for (const ext of pathext) {
317
313
  const full = `${base}${ext}`;
318
314
  try {
@@ -393,7 +389,7 @@ var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
393
389
  var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
394
390
  var sweepStarted = false;
395
391
  function toolOutputDir() {
396
- return path3.join(wstackGlobalRoot(), "tool-output");
392
+ return path12.join(wstackGlobalRoot(), "tool-output");
397
393
  }
398
394
  function sweepOldSpoolFiles(dir) {
399
395
  if (sweepStarted) return;
@@ -403,7 +399,7 @@ function sweepOldSpoolFiles(dir) {
403
399
  const now2 = Date.now();
404
400
  for (const name of await fs2.readdir(dir)) {
405
401
  if (!name.endsWith(".log")) continue;
406
- const p = path3.join(dir, name);
402
+ const p = path12.join(dir, name);
407
403
  try {
408
404
  const st = await fs2.stat(p);
409
405
  if (now2 - st.mtimeMs > SPOOL_RETENTION_MS) await fs2.unlink(p);
@@ -438,7 +434,7 @@ function createOutputSpool(opts) {
438
434
  sweepOldSpoolFiles(dir);
439
435
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
440
436
  const rand = Math.random().toString(36).slice(2, 6);
441
- filePath = path3.join(dir, `${stamp}-${safeTool}-${rand}.log`);
437
+ filePath = path12.join(dir, `${stamp}-${safeTool}-${rand}.log`);
442
438
  stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
443
439
  stream.on("error", () => {
444
440
  failed = true;
@@ -1237,37 +1233,40 @@ async function* spawnStream(opts) {
1237
1233
  }
1238
1234
  }
1239
1235
  }
1236
+ function sha256hex(content) {
1237
+ return createHash("sha256").update(content, "utf8").digest("hex");
1238
+ }
1240
1239
  async function detectPackageManager(cwd) {
1241
- const { stat: stat12 } = await import('node:fs/promises');
1240
+ const { stat: stat13 } = await import('node:fs/promises');
1242
1241
  try {
1243
- await stat12(`${cwd}/pnpm-lock.yaml`);
1242
+ await stat13(`${cwd}/pnpm-lock.yaml`);
1244
1243
  return "pnpm";
1245
1244
  } catch {
1246
1245
  }
1247
1246
  try {
1248
- await stat12(`${cwd}/yarn.lock`);
1247
+ await stat13(`${cwd}/yarn.lock`);
1249
1248
  return "yarn";
1250
1249
  } catch {
1251
1250
  }
1252
1251
  return "npm";
1253
1252
  }
1254
1253
  function resolvePath(input, ctx) {
1255
- return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
1254
+ return path12.isAbsolute(input) ? path12.normalize(input) : path12.resolve(ctx.workingDir ?? ctx.cwd, input);
1256
1255
  }
1257
1256
  function allowedRoots(ctx) {
1258
- return [path3.resolve(ctx.projectRoot), path3.resolve(Core.wstackGlobalRoot())];
1257
+ return [path12.resolve(ctx.projectRoot), path12.resolve(Core.wstackGlobalRoot())];
1259
1258
  }
1260
1259
  function isInsideAny(target, roots) {
1261
1260
  return roots.some((root) => {
1262
- const rel = path3.relative(root, target);
1263
- return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
1261
+ const rel = path12.relative(root, target);
1262
+ return rel === "" || !rel.startsWith("..") && !path12.isAbsolute(rel);
1264
1263
  });
1265
1264
  }
1266
1265
  function ensureInsideRoot(absPath, ctx) {
1267
- const target = path3.resolve(absPath);
1266
+ const target = path12.resolve(absPath);
1268
1267
  if (ctx.allowOutsideProjectRoot) return target;
1269
1268
  if (isInsideAny(target, allowedRoots(ctx))) return target;
1270
- throw new Error(`Path "${absPath}" is outside project root "${path3.resolve(ctx.projectRoot)}"`);
1269
+ throw new Error(`Path "${absPath}" is outside project root "${path12.resolve(ctx.projectRoot)}"`);
1271
1270
  }
1272
1271
  function safeResolve(input, ctx) {
1273
1272
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
@@ -1275,7 +1274,7 @@ function safeResolve(input, ctx) {
1275
1274
  async function assertRealInsideRoot(absPath, ctx) {
1276
1275
  if (ctx.allowOutsideProjectRoot) return;
1277
1276
  const realRoots = await Promise.all(
1278
- allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path3.resolve(r)))
1277
+ allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path12.resolve(r)))
1279
1278
  );
1280
1279
  let probe = absPath;
1281
1280
  for (; ; ) {
@@ -1284,7 +1283,7 @@ async function assertRealInsideRoot(absPath, ctx) {
1284
1283
  real = await fs2.realpath(probe);
1285
1284
  } catch (err) {
1286
1285
  if (err.code === "ENOENT") {
1287
- const parent = path3.dirname(probe);
1286
+ const parent = path12.dirname(probe);
1288
1287
  if (parent === probe) return;
1289
1288
  probe = parent;
1290
1289
  continue;
@@ -1597,15 +1596,15 @@ var PersistentProcessRegistry = class {
1597
1596
  constructor(baseRegistry) {
1598
1597
  this.instanceId = generateInstanceId();
1599
1598
  const homeDir = os2.homedir();
1600
- this.registryPath = path3.join(homeDir, REGISTRY_FILE);
1601
- this.lockPath = path3.join(homeDir, LOCKFILE);
1599
+ this.registryPath = path12.join(homeDir, REGISTRY_FILE);
1600
+ this.lockPath = path12.join(homeDir, LOCKFILE);
1602
1601
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
1603
1602
  this.ensureDirectory().catch((err) => {
1604
1603
  emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
1605
1604
  });
1606
1605
  }
1607
1606
  async ensureDirectory() {
1608
- const dir = path3.dirname(this.registryPath);
1607
+ const dir = path12.dirname(this.registryPath);
1609
1608
  try {
1610
1609
  await fs2.mkdir(dir, { recursive: true });
1611
1610
  } catch (err) {
@@ -2191,6 +2190,10 @@ var bashTool = {
2191
2190
  category: "Shell",
2192
2191
  description: "Execute an arbitrary command in the user's default shell (bash/zsh/pwsh/cmd). stdout and stderr are merged into one stream. This is the most powerful and dangerous tool \u2014 it gives the model full access to the developer's machine. Prefer specialized tools whenever possible.",
2193
2192
  usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the project root.\n- Output may be truncated in the middle for very large results.",
2193
+ selection: {
2194
+ doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
2195
+ useInstead: ["exec"]
2196
+ },
2194
2197
  permission: "confirm",
2195
2198
  mutating: true,
2196
2199
  riskTier: "destructive",
@@ -2999,6 +3002,24 @@ var IndexStore = class {
2999
3002
  * When false, ranked search falls back to the LIKE + in-process BM25 path.
3000
3003
  */
3001
3004
  ftsAvailable = false;
3005
+ /**
3006
+ * Cache of prepared statements keyed by their SQL text. `DatabaseSync`
3007
+ * compiles SQL on every `.prepare()` call; for the fixed-SQL methods
3008
+ * (upsertFile, getFileMeta, deleteFile, insertRefs, …) that runs thousands
3009
+ * of times during a full reindex. `StatementSync` objects are reusable
3010
+ * across calls on the same connection, so we compile each distinct SQL once
3011
+ * and reuse it. Cleared in {@link close} when the connection is torn down.
3012
+ */
3013
+ stmtCache = /* @__PURE__ */ new Map();
3014
+ /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
3015
+ stmt(sql) {
3016
+ let s = this.stmtCache.get(sql);
3017
+ if (s === void 0) {
3018
+ s = this.db.prepare(sql);
3019
+ this.stmtCache.set(sql, s);
3020
+ }
3021
+ return s;
3022
+ }
3002
3023
  /**
3003
3024
  * Execute a SQLite write operation with automatic retry on lock conflicts.
3004
3025
  *
@@ -3037,7 +3058,7 @@ var IndexStore = class {
3037
3058
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
3038
3059
  fs.mkdirSync(this.indexDir, { recursive: true });
3039
3060
  const Database = loadDatabaseSync();
3040
- this.db = new Database(path3.join(this.indexDir, DB_FILE));
3061
+ this.db = new Database(path12.join(this.indexDir, DB_FILE));
3041
3062
  try {
3042
3063
  this.db.exec("PRAGMA journal_mode = WAL");
3043
3064
  this.db.exec("PRAGMA busy_timeout = 5000");
@@ -3168,9 +3189,9 @@ var IndexStore = class {
3168
3189
  deleteSymbolsForFile(file) {
3169
3190
  this.runWithRetry(() => {
3170
3191
  if (this.ftsAvailable) {
3171
- this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
3192
+ this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
3172
3193
  }
3173
- this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
3194
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
3174
3195
  });
3175
3196
  }
3176
3197
  /**
@@ -3182,13 +3203,13 @@ var IndexStore = class {
3182
3203
  this.runWithRetry(() => {
3183
3204
  this.deleteRefsForFile(file);
3184
3205
  this.deleteSymbolsForFile(file);
3185
- this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
3206
+ this.stmt("DELETE FROM files WHERE file = ?").run(file);
3186
3207
  });
3187
3208
  }
3188
3209
  // ─── File metadata ──────────────────────────────────────────────────────────
3189
3210
  upsertFile(meta) {
3190
3211
  this.runWithRetry(() => {
3191
- this.db.prepare(
3212
+ this.stmt(
3192
3213
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
3193
3214
  VALUES (?, ?, ?, ?, ?)
3194
3215
  ON CONFLICT(file) DO UPDATE SET
@@ -3200,7 +3221,7 @@ var IndexStore = class {
3200
3221
  });
3201
3222
  }
3202
3223
  getFileMeta(file) {
3203
- const rows = this.db.prepare(
3224
+ const rows = this.stmt(
3204
3225
  "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files WHERE file = ?"
3205
3226
  ).all(file);
3206
3227
  if (!rows.length) return null;
@@ -3423,7 +3444,7 @@ var IndexStore = class {
3423
3444
  */
3424
3445
  insertRefs(fromId, refs) {
3425
3446
  this.runWithRetry(() => {
3426
- this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
3447
+ this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
3427
3448
  if (refs.length === 0) return;
3428
3449
  const stmt = this.db.prepare(
3429
3450
  `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
@@ -3616,7 +3637,7 @@ var IndexStore = class {
3616
3637
  }));
3617
3638
  }
3618
3639
  sizeBytes() {
3619
- const dbPath = path3.join(this.indexDir, DB_FILE);
3640
+ const dbPath = path12.join(this.indexDir, DB_FILE);
3620
3641
  try {
3621
3642
  return fs.statSync(dbPath).size;
3622
3643
  } catch {
@@ -3624,6 +3645,7 @@ var IndexStore = class {
3624
3645
  }
3625
3646
  }
3626
3647
  close() {
3648
+ this.stmtCache.clear();
3627
3649
  try {
3628
3650
  this.db.close();
3629
3651
  } catch {
@@ -4114,9 +4136,9 @@ func formatType(t ast.Expr) string {
4114
4136
  }
4115
4137
  `;
4116
4138
  async function syncGoParse(filePath, content, lang) {
4117
- const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), "ws-go-parse-"));
4139
+ const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), "ws-go-parse-"));
4118
4140
  try {
4119
- const scriptPath = path3.join(tmpDir, "parse.go");
4141
+ const scriptPath = path12.join(tmpDir, "parse.go");
4120
4142
  await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
4121
4143
  const proc = spawn("go", ["run", scriptPath], {
4122
4144
  stdio: ["pipe", "pipe", "pipe"],
@@ -4378,9 +4400,9 @@ var _cachedScriptPath = null;
4378
4400
  async function syncPyParse(filePath, content, lang) {
4379
4401
  try {
4380
4402
  if (!_cachedScriptPath) {
4381
- const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
4403
+ const tmpDir = path12.join(os2.tmpdir(), "ws-py-parse");
4382
4404
  await fs2.mkdir(tmpDir, { recursive: true });
4383
- _cachedScriptPath = path3.join(tmpDir, "parse.py");
4405
+ _cachedScriptPath = path12.join(tmpDir, "parse.py");
4384
4406
  await fs2.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
4385
4407
  }
4386
4408
  const proc = spawn("python", [_cachedScriptPath, filePath], {
@@ -4438,7 +4460,7 @@ async function parseSymbols4(opts) {
4438
4460
  function checkNativeParser() {
4439
4461
  try {
4440
4462
  execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
4441
- const toolsDir = path3.join(process.cwd(), "tools");
4463
+ const toolsDir = path12.join(process.cwd(), "tools");
4442
4464
  try {
4443
4465
  execFileSync(
4444
4466
  "cargo",
@@ -4448,7 +4470,7 @@ function checkNativeParser() {
4448
4470
  "--format-version",
4449
4471
  "1",
4450
4472
  "--manifest-path",
4451
- path3.join(toolsDir, "Cargo.toml")
4473
+ path12.join(toolsDir, "Cargo.toml")
4452
4474
  ],
4453
4475
  { stdio: "pipe", windowsHide: true }
4454
4476
  );
@@ -4462,13 +4484,13 @@ function checkNativeParser() {
4462
4484
  }
4463
4485
  async function tryNativeParse(file, content) {
4464
4486
  try {
4465
- const toolsDir = path3.join(process.cwd(), "tools");
4466
- const crateDir = path3.join(toolsDir, "syn-parser");
4467
- const tmpFile = path3.join(crateDir, "src", "input.rs");
4487
+ const toolsDir = path12.join(process.cwd(), "tools");
4488
+ const crateDir = path12.join(toolsDir, "syn-parser");
4489
+ const tmpFile = path12.join(crateDir, "src", "input.rs");
4468
4490
  await fs2.writeFile(tmpFile, content, "utf8");
4469
4491
  const proc = spawn(
4470
4492
  "cargo",
4471
- ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")],
4493
+ ["run", "--manifest-path", path12.join(toolsDir, "Cargo.toml")],
4472
4494
  {
4473
4495
  cwd: process.cwd(),
4474
4496
  stdio: ["pipe", "pipe", "pipe"],
@@ -4518,16 +4540,16 @@ function regexParse(opts) {
4518
4540
  const { file, content, lang } = opts;
4519
4541
  const symbols = [];
4520
4542
  const lines = content.split("\n");
4521
- const lineOffsets = [0];
4543
+ const lineOffsets2 = [0];
4522
4544
  for (let i = 0; i < lines.length; i++) {
4523
- lineOffsets.push((lineOffsets[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
4545
+ lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
4524
4546
  }
4525
4547
  function lineFromOffset(offset) {
4526
4548
  let lo = 0;
4527
- let hi = lineOffsets.length - 1;
4549
+ let hi = lineOffsets2.length - 1;
4528
4550
  while (lo < hi) {
4529
4551
  const mid = lo + hi + 1 >>> 1;
4530
- if (expectDefined(lineOffsets[mid]) <= offset) lo = mid;
4552
+ if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
4531
4553
  else hi = mid - 1;
4532
4554
  }
4533
4555
  return lo + 1;
@@ -4542,7 +4564,7 @@ function regexParse(opts) {
4542
4564
  const name = expectDefined(match[1]);
4543
4565
  const offset = match.index ?? 0;
4544
4566
  const line = lineFromOffset(offset);
4545
- const col = offset - (lineOffsets[line - 1] ?? 0);
4567
+ const col = offset - (lineOffsets2[line - 1] ?? 0);
4546
4568
  const lineIdx = line - 1;
4547
4569
  const signature = extractDeclaration(lineIdx);
4548
4570
  symbols.push({
@@ -4580,22 +4602,22 @@ function parseSymbols5(opts) {
4580
4602
  function regexParse2(opts) {
4581
4603
  const { file, content, lang } = opts;
4582
4604
  const symbols = [];
4583
- const basename2 = path3.basename(file).toLowerCase();
4584
- const isPackageJson = basename2 === "package.json";
4585
- const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
4605
+ const basename4 = path12.basename(file).toLowerCase();
4606
+ const isPackageJson = basename4 === "package.json";
4607
+ const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
4586
4608
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
4587
4609
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
4588
4610
  const lines = content.split("\n");
4589
- const lineOffsets = [0];
4611
+ const lineOffsets2 = [0];
4590
4612
  for (let i = 0; i < lines.length; i++) {
4591
- lineOffsets.push((lineOffsets[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
4613
+ lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
4592
4614
  }
4593
4615
  function lineFromOffset(offset) {
4594
4616
  let lo = 0;
4595
- let hi = lineOffsets.length - 1;
4617
+ let hi = lineOffsets2.length - 1;
4596
4618
  while (lo < hi) {
4597
4619
  const mid = lo + hi + 1 >>> 1;
4598
- if (expectDefined(lineOffsets[mid]) <= offset) lo = mid;
4620
+ if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
4599
4621
  else hi = mid - 1;
4600
4622
  }
4601
4623
  return lo + 1;
@@ -4606,11 +4628,11 @@ function regexParse2(opts) {
4606
4628
  const line = lineFromOffset(offset);
4607
4629
  symbols.push(
4608
4630
  makeSymbol({
4609
- name: path3.basename(file),
4631
+ name: path12.basename(file),
4610
4632
  kind: "object",
4611
4633
  line,
4612
4634
  col: 0,
4613
- signature: `"${path3.basename(file)}" = { ... }`,
4635
+ signature: `"${path12.basename(file)}" = { ... }`,
4614
4636
  file,
4615
4637
  lang
4616
4638
  })
@@ -4621,7 +4643,7 @@ function regexParse2(opts) {
4621
4643
  const key = expectDefined(match[1]);
4622
4644
  const offset = match.index ?? 0;
4623
4645
  const line = lineFromOffset(offset);
4624
- const col = offset - (lineOffsets[line - 1] ?? 0);
4646
+ const col = offset - (lineOffsets2[line - 1] ?? 0);
4625
4647
  let kind = "property";
4626
4648
  let signature = `"${key}": ..."`;
4627
4649
  if (isPackageJson) {
@@ -4656,10 +4678,10 @@ function regexParse2(opts) {
4656
4678
  })
4657
4679
  );
4658
4680
  if (isPackageJson && key === "scripts") {
4659
- extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFromOffset);
4681
+ extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset);
4660
4682
  }
4661
4683
  if (isTsconfig && key === "compilerOptions") {
4662
- extractCompilerOptions(content, symbols, file, lang, lineOffsets, line, lineFromOffset);
4684
+ extractCompilerOptions(content, symbols, file, lang, lineOffsets2, line, lineFromOffset);
4663
4685
  }
4664
4686
  }
4665
4687
  const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
@@ -4672,7 +4694,7 @@ function regexParse2(opts) {
4672
4694
  name: "$defs",
4673
4695
  kind: "property",
4674
4696
  line,
4675
- col: offset - (lineOffsets[line - 1] ?? 0),
4697
+ col: offset - (lineOffsets2[line - 1] ?? 0),
4676
4698
  signature: '"$defs": { ... }',
4677
4699
  file,
4678
4700
  lang
@@ -4696,7 +4718,7 @@ function regexParse2(opts) {
4696
4718
  name: key,
4697
4719
  kind: "property",
4698
4720
  line,
4699
- col: offset - (lineOffsets[line - 1] ?? 0),
4721
+ col: offset - (lineOffsets2[line - 1] ?? 0),
4700
4722
  signature: `"${key}": { ... }`,
4701
4723
  file,
4702
4724
  lang
@@ -4706,7 +4728,7 @@ function regexParse2(opts) {
4706
4728
  }
4707
4729
  return { file, lang, symbols, mtimeMs: Date.now() };
4708
4730
  }
4709
- function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFromOffset) {
4731
+ function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset) {
4710
4732
  const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
4711
4733
  for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
4712
4734
  const blockContent = expectDefined(match[0]);
@@ -4721,7 +4743,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
4721
4743
  name: key,
4722
4744
  kind: "function",
4723
4745
  line,
4724
- col: keyOffset - (lineOffsets[line - 1] ?? 0),
4746
+ col: keyOffset - (lineOffsets2[line - 1] ?? 0),
4725
4747
  signature: `"${key}": "..."`,
4726
4748
  file,
4727
4749
  lang
@@ -4730,7 +4752,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
4730
4752
  }
4731
4753
  }
4732
4754
  }
4733
- function extractCompilerOptions(content, symbols, file, lang, lineOffsets, parentLine, lineFromOffset) {
4755
+ function extractCompilerOptions(content, symbols, file, lang, lineOffsets2, parentLine, lineFromOffset) {
4734
4756
  const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
4735
4757
  for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
4736
4758
  const blockContent = expectDefined(match[0]);
@@ -4746,7 +4768,7 @@ function extractCompilerOptions(content, symbols, file, lang, lineOffsets, paren
4746
4768
  name: key,
4747
4769
  kind: "property",
4748
4770
  line,
4749
- col: keyOffset - (lineOffsets[line - 1] ?? 0),
4771
+ col: keyOffset - (lineOffsets2[line - 1] ?? 0),
4750
4772
  signature: `"${key}": ...`,
4751
4773
  file,
4752
4774
  lang
@@ -4782,16 +4804,16 @@ function regexParse3(opts) {
4782
4804
  const { file, content, lang } = opts;
4783
4805
  const symbols = [];
4784
4806
  const lines = content.split("\n");
4785
- const lineOffsets = [0];
4807
+ const lineOffsets2 = [0];
4786
4808
  for (let i = 0; i < lines.length; i++) {
4787
- lineOffsets.push((lineOffsets[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
4809
+ lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
4788
4810
  }
4789
4811
  function lineFromOffset(offset) {
4790
4812
  let lo = 0;
4791
- let hi = lineOffsets.length - 1;
4813
+ let hi = lineOffsets2.length - 1;
4792
4814
  while (lo < hi) {
4793
4815
  const mid = lo + hi + 1 >>> 1;
4794
- if (expectDefined(lineOffsets[mid]) <= offset) lo = mid;
4816
+ if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
4795
4817
  else hi = mid - 1;
4796
4818
  }
4797
4819
  return lo + 1;
@@ -4801,7 +4823,7 @@ function regexParse3(opts) {
4801
4823
  const name = expectDefined(match[1]);
4802
4824
  const offset = match.index ?? 0;
4803
4825
  const line = lineFromOffset(offset);
4804
- const col = offset - (lineOffsets[line - 1] ?? 0);
4826
+ const col = offset - (lineOffsets2[line - 1] ?? 0);
4805
4827
  symbols.push(
4806
4828
  makeSymbol2({
4807
4829
  name,
@@ -4819,7 +4841,7 @@ function regexParse3(opts) {
4819
4841
  const name = expectDefined(match[1]);
4820
4842
  const offset = match.index ?? 0;
4821
4843
  const line = lineFromOffset(offset);
4822
- const col = offset - (lineOffsets[line - 1] ?? 0);
4844
+ const col = offset - (lineOffsets2[line - 1] ?? 0);
4823
4845
  symbols.push(
4824
4846
  makeSymbol2({
4825
4847
  name,
@@ -4839,7 +4861,7 @@ function regexParse3(opts) {
4839
4861
  if (!key) continue;
4840
4862
  const offset = match.index ?? 0;
4841
4863
  const line = lineFromOffset(offset);
4842
- const col = offset - (lineOffsets[line - 1] ?? 0);
4864
+ const col = offset - (lineOffsets2[line - 1] ?? 0);
4843
4865
  const lineContent = lines[line - 1] ?? "";
4844
4866
  if (/^[|&>]/.test(lineContent.trim())) continue;
4845
4867
  if (key === "---" || key === "...") continue;
@@ -4854,7 +4876,7 @@ function regexParse3(opts) {
4854
4876
  const key = expectDefined(match[2]);
4855
4877
  const offset = match.index ?? 0;
4856
4878
  const line = lineFromOffset(offset);
4857
- const col = offset - (lineOffsets[line - 1] ?? 0);
4879
+ const col = offset - (lineOffsets2[line - 1] ?? 0);
4858
4880
  const value = extractValue(content, offset + match[0]?.length);
4859
4881
  const kind = isScalar(value) ? "literal" : "property";
4860
4882
  symbols.push(
@@ -4874,7 +4896,7 @@ function regexParse3(opts) {
4874
4896
  const key = expectDefined(match[2]);
4875
4897
  const offset = match.index ?? 0;
4876
4898
  const line = lineFromOffset(offset);
4877
- const col = offset - (lineOffsets[line - 1] ?? 0);
4899
+ const col = offset - (lineOffsets2[line - 1] ?? 0);
4878
4900
  symbols.push(
4879
4901
  makeSymbol2({
4880
4902
  name: key,
@@ -4960,7 +4982,7 @@ function compileGitignore(lines) {
4960
4982
  async function loadGitignoreMatcher(projectRoot) {
4961
4983
  let lines = [];
4962
4984
  try {
4963
- const raw = await fs2.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
4985
+ const raw = await fs2.readFile(path12.join(projectRoot, ".gitignore"), "utf8");
4964
4986
  lines = raw.split("\n");
4965
4987
  } catch {
4966
4988
  }
@@ -5025,14 +5047,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
5025
5047
  dirCount++;
5026
5048
  for (const e of entries) {
5027
5049
  if (ignoreSet.has(e.name)) continue;
5028
- const full = path3.join(dir, e.name);
5029
- const rel = path3.relative(projectRoot, full).replace(/\\/g, "/");
5050
+ const full = path12.join(dir, e.name);
5051
+ const rel = path12.relative(projectRoot, full).replace(/\\/g, "/");
5030
5052
  if (e.isDirectory()) {
5031
5053
  if (isGitIgnored(rel, true)) continue;
5032
5054
  await walk2(full);
5033
5055
  } else if (e.isFile()) {
5034
5056
  if (isGitIgnored(rel, false)) continue;
5035
- const ext = path3.extname(e.name);
5057
+ const ext = path12.extname(e.name);
5036
5058
  for (const { ext: extName, pat } of globs) {
5037
5059
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
5038
5060
  results.push(full);
@@ -5088,7 +5110,7 @@ async function runIndexerWithStore(store, opts) {
5088
5110
  let files;
5089
5111
  let discoveredFiles = null;
5090
5112
  if (opts.files && opts.files.length > 0) {
5091
- files = opts.files.map((f) => path3.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path3.relative(projectRoot, f).replace(/\\/g, "/"), false));
5113
+ files = opts.files.map((f) => path12.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path12.relative(projectRoot, f).replace(/\\/g, "/"), false));
5092
5114
  } else {
5093
5115
  files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
5094
5116
  discoveredFiles = new Set(files);
@@ -5116,34 +5138,34 @@ async function runIndexerWithStore(store, opts) {
5116
5138
  const statOpts = signal ? { signal } : {};
5117
5139
  const statReadParse = await Promise.allSettled(
5118
5140
  batchFiles.map(async (file) => {
5119
- let stat12;
5141
+ let stat13;
5120
5142
  try {
5121
- stat12 = await fs2.stat(file, statOpts);
5143
+ stat13 = await fs2.stat(file, statOpts);
5122
5144
  } catch (e) {
5123
5145
  if (isAbortError(e)) throw e;
5124
5146
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
5125
5147
  }
5126
- if (!stat12.isFile()) return { file, stat: stat12, lang: "", parsed: null };
5148
+ if (!stat13.isFile()) return { file, stat: stat13, lang: "", parsed: null };
5127
5149
  const lang = detectLang(file);
5128
- if (!lang) return { file, stat: stat12, lang: "", parsed: null };
5150
+ if (!lang) return { file, stat: stat13, lang: "", parsed: null };
5129
5151
  const meta = existingMeta.get(file);
5130
- if (!force && meta && meta.mtimeMs === Math.floor(stat12.mtimeMs)) {
5131
- return { file, stat: stat12, lang, parsed: null, skippedMeta: meta };
5152
+ if (!force && meta && meta.mtimeMs === Math.floor(stat13.mtimeMs)) {
5153
+ return { file, stat: stat13, lang, parsed: null, skippedMeta: meta };
5132
5154
  }
5133
5155
  let content;
5134
5156
  try {
5135
5157
  content = await fs2.readFile(file, { encoding: "utf8", signal });
5136
5158
  } catch (e) {
5137
5159
  if (isAbortError(e)) throw e;
5138
- return { file, stat: stat12, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
5160
+ return { file, stat: stat13, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
5139
5161
  }
5140
5162
  let parsed;
5141
5163
  try {
5142
5164
  parsed = await parseFile(file, content, lang);
5143
5165
  } catch (e) {
5144
- return { file, stat: stat12, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
5166
+ return { file, stat: stat13, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
5145
5167
  }
5146
- return { file, stat: stat12, lang, parsed, content };
5168
+ return { file, stat: stat13, lang, parsed, content };
5147
5169
  })
5148
5170
  );
5149
5171
  const batchEntries = [];
@@ -5163,7 +5185,7 @@ async function runIndexerWithStore(store, opts) {
5163
5185
  if (result.error.includes("error:")) errors.push(result.error);
5164
5186
  continue;
5165
5187
  }
5166
- const { stat: stat12, lang, parsed } = result;
5188
+ const { stat: stat13, lang, parsed } = result;
5167
5189
  if (result.skippedMeta) {
5168
5190
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
5169
5191
  symbolsIndexed += result.skippedMeta.symbolCount;
@@ -5175,7 +5197,7 @@ async function runIndexerWithStore(store, opts) {
5175
5197
  store.upsertFile({
5176
5198
  file,
5177
5199
  lang,
5178
- mtimeMs: Math.floor(stat12.mtimeMs),
5200
+ mtimeMs: Math.floor(stat13.mtimeMs),
5179
5201
  symbolCount: 0,
5180
5202
  lastIndexed: Date.now()
5181
5203
  });
@@ -5187,7 +5209,7 @@ async function runIndexerWithStore(store, opts) {
5187
5209
  store.upsertFile({
5188
5210
  file,
5189
5211
  lang,
5190
- mtimeMs: Math.floor(stat12.mtimeMs),
5212
+ mtimeMs: Math.floor(stat13.mtimeMs),
5191
5213
  symbolCount: 0,
5192
5214
  lastIndexed: Date.now()
5193
5215
  });
@@ -5203,7 +5225,7 @@ async function runIndexerWithStore(store, opts) {
5203
5225
  lang,
5204
5226
  symbols: parsed.symbols,
5205
5227
  refs,
5206
- mtimeMs: Math.floor(stat12.mtimeMs),
5228
+ mtimeMs: Math.floor(stat13.mtimeMs),
5207
5229
  symbolCount: parsed.symbols.length
5208
5230
  });
5209
5231
  deleteForFiles.push(file);
@@ -5352,8 +5374,8 @@ function statsService(args) {
5352
5374
  }
5353
5375
 
5354
5376
  // src/codebase-index/background-indexer.ts
5355
- var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
5356
- var DEFAULT_INCREMENTAL_TIMEOUT_MS = 3e4;
5377
+ var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
5378
+ var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
5357
5379
  var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
5358
5380
  var _ready = false;
5359
5381
  var _indexing = false;
@@ -5868,6 +5890,25 @@ var codebaseStatsTool = {
5868
5890
  };
5869
5891
  }
5870
5892
  };
5893
+ async function resolveReal(p) {
5894
+ const resolved = path12.resolve(p);
5895
+ let probe = resolved;
5896
+ const missing = [];
5897
+ for (; ; ) {
5898
+ try {
5899
+ return path12.resolve(await fs2.realpath(probe), ...missing);
5900
+ } catch (err) {
5901
+ if (err.code === "ENOENT") {
5902
+ const parent = path12.dirname(probe);
5903
+ if (parent === probe) return resolved;
5904
+ missing.unshift(path12.basename(probe));
5905
+ probe = parent;
5906
+ continue;
5907
+ }
5908
+ return resolved;
5909
+ }
5910
+ }
5911
+ }
5871
5912
  function normalizeOverrides(set) {
5872
5913
  const out = {};
5873
5914
  if (set && typeof set === "object") {
@@ -6025,10 +6066,12 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
6025
6066
  kitId: active.kit,
6026
6067
  outPath: input.out
6027
6068
  });
6028
- const root = path3.resolve(ctx.projectRoot);
6029
- const abs = path3.resolve(path3.join(ctx.projectRoot, result.path));
6030
- const rel = path3.relative(root, abs);
6031
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
6069
+ const root = await resolveReal(ctx.projectRoot);
6070
+ const absResolved = path12.isAbsolute(result.path) ? path12.resolve(result.path) : path12.resolve(path12.join(ctx.projectRoot, result.path));
6071
+ const absParent = await resolveReal(path12.dirname(absResolved));
6072
+ const abs = path12.join(absParent, path12.basename(absResolved));
6073
+ const rel = path12.relative(root, abs);
6074
+ if (rel.startsWith("..") || path12.isAbsolute(rel)) {
6032
6075
  throw new Error(
6033
6076
  `design: materialize path "${result.path}" would escape the project root`
6034
6077
  );
@@ -6052,7 +6095,7 @@ ${result.content}
6052
6095
  \`\`\``
6053
6096
  };
6054
6097
  }
6055
- await fs2.mkdir(path3.dirname(abs), { recursive: true });
6098
+ await fs2.mkdir(path12.dirname(abs), { recursive: true });
6056
6099
  await fs2.writeFile(abs, result.content);
6057
6100
  return {
6058
6101
  action,
@@ -6180,11 +6223,11 @@ function findGitDir(cwd) {
6180
6223
  let dir = cwd;
6181
6224
  for (let i = 0; i < 20; i++) {
6182
6225
  try {
6183
- const stat12 = statSync(path3.join(dir, ".git"));
6184
- if (stat12.isDirectory()) return dir;
6226
+ const stat13 = statSync(path12.join(dir, ".git"));
6227
+ if (stat13.isDirectory()) return dir;
6185
6228
  } catch {
6186
6229
  }
6187
- const parent = path3.dirname(dir);
6230
+ const parent = path12.dirname(dir);
6188
6231
  if (parent === dir) break;
6189
6232
  dir = parent;
6190
6233
  }
@@ -6225,8 +6268,8 @@ async function fileDiff(input, ctx, _signal) {
6225
6268
  const results = [];
6226
6269
  for (const file of files) {
6227
6270
  const absPath = safeResolve(file, ctx);
6228
- const stat12 = await fs2.stat(absPath).catch(() => null);
6229
- if (!stat12?.isFile()) continue;
6271
+ const stat13 = await fs2.stat(absPath).catch(() => null);
6272
+ if (!stat13?.isFile()) continue;
6230
6273
  const content = await fs2.readFile(absPath, "utf8");
6231
6274
  const lines = content.split(/\r?\n/);
6232
6275
  results.push(formatWithLineNumbers(file, lines));
@@ -6323,8 +6366,8 @@ async function resolveFiles(filesInput, cwd) {
6323
6366
  for (const f of files) {
6324
6367
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
6325
6368
  try {
6326
- const stat12 = await fs2.stat(absPath);
6327
- if (stat12.isFile()) resolved.push(absPath);
6369
+ const stat13 = await fs2.stat(absPath);
6370
+ if (stat13.isFile()) resolved.push(absPath);
6328
6371
  } catch {
6329
6372
  }
6330
6373
  }
@@ -6390,11 +6433,303 @@ function processFile(content, absPath, _style, _overwrite, target) {
6390
6433
  }
6391
6434
  return results;
6392
6435
  }
6436
+
6437
+ // src/_edit-match.ts
6438
+ var TIER_LABEL = {
6439
+ exact: "exact match",
6440
+ "trailing-whitespace": "whitespace-normalized match (trailing whitespace ignored)",
6441
+ "whitespace-normalized": "whitespace-normalized match (indentation ignored, replacement re-indented)",
6442
+ fuzzy: "fuzzy match (block anchors exact, interior \u226590% similar)"
6443
+ };
6444
+ var TIER_CONFIDENCE = {
6445
+ exact: "exact",
6446
+ "trailing-whitespace": "high",
6447
+ "whitespace-normalized": "medium",
6448
+ fuzzy: "low"
6449
+ };
6450
+ var MIN_NORMALIZED_NEEDLE_CHARS = 16;
6451
+ var FUZZY_MIN_SIMILARITY = 0.9;
6452
+ var FUZZY_AMBIGUITY_MARGIN = 0.05;
6453
+ var FUZZY_MAX_INTERIOR_CHARS = 2e3;
6454
+ function findLadderMatches(fileLf, oldLf) {
6455
+ const exact = [];
6456
+ let idx = fileLf.indexOf(oldLf);
6457
+ while (idx !== -1) {
6458
+ exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
6459
+ idx = fileLf.indexOf(oldLf, idx + 1);
6460
+ }
6461
+ if (exact.length > 0) return { tier: "exact", matches: exact };
6462
+ const fileLines = fileLf.split("\n");
6463
+ const needleLines = oldLf.split("\n");
6464
+ if (needleLines.length > fileLines.length) return void 0;
6465
+ const offsets = lineOffsets(fileLines);
6466
+ const trailing = windowScan(
6467
+ fileLines,
6468
+ needleLines,
6469
+ offsets,
6470
+ (a, b) => a.trimEnd() === b.trimEnd()
6471
+ );
6472
+ if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
6473
+ const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
6474
+ if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
6475
+ const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
6476
+ if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
6477
+ return fuzzyScan(fileLines, needleLines, offsets);
6478
+ }
6479
+ function lineAt(text, pos) {
6480
+ let line = 1;
6481
+ for (let i = 0; i < pos; i++) {
6482
+ if (text.charCodeAt(i) === 10) line++;
6483
+ }
6484
+ return line;
6485
+ }
6486
+ function lineOffsets(lines) {
6487
+ const out = new Array(lines.length);
6488
+ let pos = 0;
6489
+ for (let i = 0; i < lines.length; i++) {
6490
+ out[i] = pos;
6491
+ pos += lines[i].length + 1;
6492
+ }
6493
+ return out;
6494
+ }
6495
+ function windowToMatch(fileLines, offsets, start, windowLen) {
6496
+ const lastLine = start + windowLen - 1;
6497
+ return {
6498
+ start: offsets[start],
6499
+ end: offsets[lastLine] + fileLines[lastLine].length,
6500
+ startLine: start + 1
6501
+ };
6502
+ }
6503
+ function windowScan(fileLines, needleLines, offsets, eq) {
6504
+ const n = needleLines.length;
6505
+ const out = [];
6506
+ for (let i = 0; i + n <= fileLines.length; i++) {
6507
+ let all = true;
6508
+ for (let j = 0; j < n; j++) {
6509
+ if (!eq(fileLines[i + j], needleLines[j])) {
6510
+ all = false;
6511
+ break;
6512
+ }
6513
+ }
6514
+ if (all) {
6515
+ out.push(windowToMatch(fileLines, offsets, i, n));
6516
+ i += n - 1;
6517
+ }
6518
+ }
6519
+ return out;
6520
+ }
6521
+ function fuzzyScan(fileLines, needleLines, offsets) {
6522
+ const n = needleLines.length;
6523
+ if (n < 3) return void 0;
6524
+ const firstNeedle = needleLines[0].trim();
6525
+ const lastNeedle = needleLines[n - 1].trim();
6526
+ const needleInterior = needleLines.slice(1, -1).map((l) => l.trim()).join("\n");
6527
+ if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return void 0;
6528
+ const candidates = [];
6529
+ for (let i = 0; i + n <= fileLines.length; i++) {
6530
+ if (fileLines[i].trim() !== firstNeedle) continue;
6531
+ if (fileLines[i + n - 1].trim() !== lastNeedle) continue;
6532
+ const windowInterior = fileLines.slice(i + 1, i + n - 1).map((l) => l.trim()).join("\n");
6533
+ if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;
6534
+ const score = similarity(needleInterior, windowInterior);
6535
+ if (score >= FUZZY_MIN_SIMILARITY) {
6536
+ candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });
6537
+ }
6538
+ }
6539
+ if (candidates.length === 0) return void 0;
6540
+ candidates.sort((a, b) => b.score - a.score);
6541
+ const best = candidates[0];
6542
+ const runnerUp = candidates[1];
6543
+ if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {
6544
+ return {
6545
+ tier: "fuzzy",
6546
+ matches: candidates.map((c) => c.match),
6547
+ score: best.score,
6548
+ ambiguous: true
6549
+ };
6550
+ }
6551
+ return { tier: "fuzzy", matches: [best.match], score: best.score };
6552
+ }
6553
+ function similarity(a, b) {
6554
+ if (a === b) return 1;
6555
+ const max = Math.max(a.length, b.length);
6556
+ if (max === 0) return 1;
6557
+ if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {
6558
+ return 1 - Math.abs(a.length - b.length) / max;
6559
+ }
6560
+ return 1 - levenshtein(a, b) / max;
6561
+ }
6562
+ function levenshtein(a, b) {
6563
+ if (a.length === 0) return b.length;
6564
+ if (b.length === 0) return a.length;
6565
+ let prev = new Array(b.length + 1);
6566
+ let cur = new Array(b.length + 1);
6567
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
6568
+ for (let i = 1; i <= a.length; i++) {
6569
+ cur[0] = i;
6570
+ const ca = a.charCodeAt(i - 1);
6571
+ for (let j = 1; j <= b.length; j++) {
6572
+ const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
6573
+ cur[j] = Math.min(
6574
+ prev[j] + 1,
6575
+ cur[j - 1] + 1,
6576
+ prev[j - 1] + cost
6577
+ );
6578
+ }
6579
+ [prev, cur] = [cur, prev];
6580
+ }
6581
+ return prev[b.length];
6582
+ }
6583
+ function firstLineIndent(text) {
6584
+ for (const line of text.split("\n")) {
6585
+ if (line.trim() === "") continue;
6586
+ return /^[ \t]*/.exec(line)[0];
6587
+ }
6588
+ return "";
6589
+ }
6590
+ function adjustIndent(newLf, fromIndent, toIndent) {
6591
+ if (fromIndent === toIndent) return { text: newLf, adjusted: false };
6592
+ const lines = newLf.split("\n");
6593
+ if (toIndent.startsWith(fromIndent)) {
6594
+ const extra = toIndent.slice(fromIndent.length);
6595
+ return {
6596
+ text: lines.map((l) => l.trim() === "" ? l : extra + l).join("\n"),
6597
+ adjusted: true
6598
+ };
6599
+ }
6600
+ if (fromIndent.startsWith(toIndent)) {
6601
+ const remove = fromIndent.slice(toIndent.length);
6602
+ return {
6603
+ text: lines.map((l) => l.startsWith(remove) ? l.slice(remove.length) : l).join("\n"),
6604
+ adjusted: true
6605
+ };
6606
+ }
6607
+ return { text: newLf, adjusted: false };
6608
+ }
6609
+ function nearestMatchHint(fileLf, oldLf) {
6610
+ const fileLines = fileLf.split("\n");
6611
+ const needleLines = oldLf.split("\n");
6612
+ if (needleLines.length > fileLines.length) return void 0;
6613
+ const needleTrimmed = needleLines.map((l) => l.trim());
6614
+ let bestScore = 0;
6615
+ let bestStart = -1;
6616
+ for (let i = 0; i + needleLines.length <= fileLines.length; i++) {
6617
+ let sum = 0;
6618
+ for (let j = 0; j < needleLines.length; j++) {
6619
+ sum += prefixSimilarity(needleTrimmed[j], fileLines[i + j].trim());
6620
+ }
6621
+ const score = sum / needleLines.length;
6622
+ if (score > bestScore) {
6623
+ bestScore = score;
6624
+ bestStart = i;
6625
+ }
6626
+ }
6627
+ if (bestStart === -1 || bestScore < 0.5) return void 0;
6628
+ const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, needleLines.length)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
6629
+ return { line: bestStart + 1, snippet };
6630
+ }
6631
+ function prefixSimilarity(a, b) {
6632
+ if (a === b) return 1;
6633
+ const max = Math.max(a.length, b.length);
6634
+ if (max === 0) return 1;
6635
+ let p = 0;
6636
+ const lim = Math.min(a.length, b.length);
6637
+ while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;
6638
+ return p / max;
6639
+ }
6640
+ var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
6641
+ var MAX_CHECK_CHARS = 15e5;
6642
+ var MAX_ERRORS = 5;
6643
+ var tsLoad = null;
6644
+ function loadTypescript() {
6645
+ tsLoad ??= import('typescript').then(
6646
+ (m) => m.default ?? m,
6647
+ () => null
6648
+ );
6649
+ return tsLoad;
6650
+ }
6651
+ async function checkSyntax(filePath, content, previousContent) {
6652
+ const errors = await parseErrors(filePath, content);
6653
+ if (errors === void 0) return void 0;
6654
+ if (errors.length === 0) return { errors, preExisting: false };
6655
+ let preExisting = false;
6656
+ if (previousContent !== void 0) {
6657
+ const prevErrors = await parseErrors(filePath, previousContent);
6658
+ preExisting = prevErrors !== void 0 && prevErrors.length > 0;
6659
+ }
6660
+ return { errors, preExisting };
6661
+ }
6662
+ function isJsoncFile(filePath) {
6663
+ const base = path12.basename(filePath).toLowerCase();
6664
+ if (base.endsWith(".jsonc")) return true;
6665
+ if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
6666
+ const dir = path12.basename(path12.dirname(filePath)).toLowerCase();
6667
+ return dir === ".vscode";
6668
+ }
6669
+ async function parseErrors(filePath, content) {
6670
+ if (content.length > MAX_CHECK_CHARS) return void 0;
6671
+ const ext = path12.extname(filePath).toLowerCase();
6672
+ if (ext === ".json" || ext === ".jsonc") {
6673
+ try {
6674
+ JSON.parse(content);
6675
+ return [];
6676
+ } catch (err) {
6677
+ if (isJsoncFile(filePath)) {
6678
+ const ts3 = await loadTypescript();
6679
+ if (ts3) {
6680
+ const jsonc = ts3.parseConfigFileTextToJson(filePath, content);
6681
+ if (!jsonc.error) return [];
6682
+ return [formatDiag(ts3, jsonc.error, content)];
6683
+ }
6684
+ }
6685
+ return [`JSON parse error: ${err.message}`];
6686
+ }
6687
+ }
6688
+ if (!TS_LIKE.has(ext)) return void 0;
6689
+ const ts2 = await loadTypescript();
6690
+ if (!ts2) return void 0;
6691
+ const scriptKind = ext === ".tsx" ? ts2.ScriptKind.TSX : ext === ".ts" || ext === ".mts" || ext === ".cts" ? ts2.ScriptKind.TS : (
6692
+ // Plain JS may legitimately contain JSX; the JSX grammar is a
6693
+ // superset for untyped code, so parsing .js/.jsx as JSX avoids
6694
+ // false positives on React files.
6695
+ ts2.ScriptKind.JSX
6696
+ );
6697
+ const sourceFile = ts2.createSourceFile(
6698
+ path12.basename(filePath),
6699
+ content,
6700
+ ts2.ScriptTarget.Latest,
6701
+ /* setParentNodes */
6702
+ false,
6703
+ scriptKind
6704
+ );
6705
+ const diags = sourceFile.parseDiagnostics ?? [];
6706
+ return diags.slice(0, MAX_ERRORS).map((d) => formatDiag(ts2, d, content, sourceFile));
6707
+ }
6708
+ function formatDiag(ts2, diag, content, sourceFile) {
6709
+ const message = ts2.flattenDiagnosticMessageText(diag.messageText, " ");
6710
+ if (diag.start === void 0) return message;
6711
+ let line;
6712
+ if (sourceFile) {
6713
+ line = sourceFile.getLineAndCharacterOfPosition(diag.start).line + 1;
6714
+ } else {
6715
+ line = 1;
6716
+ for (let i = 0; i < diag.start && i < content.length; i++) {
6717
+ if (content.charCodeAt(i) === 10) line++;
6718
+ }
6719
+ }
6720
+ return `line ${line}: ${message}`;
6721
+ }
6722
+
6723
+ // src/edit.ts
6393
6724
  var editTool = {
6394
6725
  name: "edit",
6395
6726
  category: "Filesystem",
6396
6727
  description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
6397
- usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.",
6728
+ usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.\nIf `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` \u2014 always verify the diff when this happens.\nAfter the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.",
6729
+ selection: {
6730
+ doNotUseWhen: "creating a new file, replacing the whole file, or applying an existing unified diff.",
6731
+ useInstead: ["write", "patch"]
6732
+ },
6398
6733
  permission: "confirm",
6399
6734
  mutating: true,
6400
6735
  capabilities: ["fs.write"],
@@ -6410,7 +6745,7 @@ var editTool = {
6410
6745
  },
6411
6746
  required: ["path", "old_string", "new_string"]
6412
6747
  },
6413
- async execute(input, ctx) {
6748
+ async execute(input, ctx, opts) {
6414
6749
  if (!input?.path) {
6415
6750
  throw new ToolValidationError({ message: "edit: path is required", field: "path" });
6416
6751
  }
@@ -6433,7 +6768,7 @@ var editTool = {
6433
6768
  });
6434
6769
  }
6435
6770
  const absPath = await safeResolveReal(input.path, ctx);
6436
- const stat12 = await fs2.stat(absPath).catch((err) => {
6771
+ const stat13 = await fs2.stat(absPath).catch((err) => {
6437
6772
  if (err.code === "ENOENT") {
6438
6773
  throw new ToolValidationError({
6439
6774
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
@@ -6443,7 +6778,7 @@ var editTool = {
6443
6778
  }
6444
6779
  throw err;
6445
6780
  });
6446
- if (!stat12.isFile()) {
6781
+ if (!stat13.isFile()) {
6447
6782
  throw new ToolValidationError({
6448
6783
  message: `edit: "${input.path}" is not a regular file`,
6449
6784
  field: "path"
@@ -6453,15 +6788,27 @@ var editTool = {
6453
6788
  const original = await fs2.readFile(absPath, "utf8");
6454
6789
  const updated = await fs2.stat(absPath);
6455
6790
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
6456
- const lastReadMtime = ctx.lastReadMtime(absPath);
6457
- if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
6458
- throw new ToolValidationError({
6459
- message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
6460
- field: "path",
6461
- context: { reason: "external_modification" }
6462
- });
6791
+ const originalHash = sha256hex(original);
6792
+ const lastReadHash = ctx.lastReadHash?.(absPath);
6793
+ if (lastReadHash !== void 0) {
6794
+ if (lastReadHash !== originalHash) {
6795
+ throw new ToolValidationError({
6796
+ message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
6797
+ field: "path",
6798
+ context: { reason: "external_modification" }
6799
+ });
6800
+ }
6801
+ } else {
6802
+ const lastReadMtime = ctx.lastReadMtime(absPath);
6803
+ if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
6804
+ throw new ToolValidationError({
6805
+ message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
6806
+ field: "path",
6807
+ context: { reason: "external_modification" }
6808
+ });
6809
+ }
6463
6810
  }
6464
- if (autoRead && updated.mtimeMs > stat12.mtimeMs + mtimeTolerance) {
6811
+ if (autoRead && updated.mtimeMs > stat13.mtimeMs + mtimeTolerance) {
6465
6812
  throw new ToolValidationError({
6466
6813
  message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
6467
6814
  field: "path",
@@ -6474,42 +6821,78 @@ var editTool = {
6474
6821
  const oldLf = normalizeToLf(input.old_string);
6475
6822
  const newLf = normalizeToLf(input.new_string);
6476
6823
  if (oldLf === newLf) {
6477
- if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
6824
+ if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
6478
6825
  return {
6479
6826
  path: absPath,
6480
6827
  replacements: 0,
6481
6828
  diff: "(no-op: old and new are identical)",
6829
+ matched_by: "exact",
6482
6830
  note: autoReadNote
6483
6831
  };
6484
6832
  }
6485
- let count = 0;
6486
- let idx = fileLf.indexOf(oldLf);
6487
- const matches = [];
6488
- while (idx !== -1) {
6489
- matches.push(idx);
6490
- count++;
6491
- idx = fileLf.indexOf(oldLf, idx + 1);
6492
- }
6493
- if (count === 0) {
6494
- const hint = findSimilarity(fileLf, oldLf);
6833
+ const ladder = findLadderMatches(fileLf, oldLf);
6834
+ if (!ladder) {
6835
+ const hint = nearestMatchHint(fileLf, oldLf);
6495
6836
  throw new ToolValidationError({
6496
- message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`,
6837
+ message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
6838
+ ${hint.snippet}
6839
+ Compare this against your old_string and retry with the file's actual text.` : ""}`,
6497
6840
  field: "old_string"
6498
6841
  });
6499
6842
  }
6843
+ const { tier, matches } = ladder;
6844
+ const count = matches.length;
6845
+ if (ladder.ambiguous) {
6846
+ const lines = matches.map((m) => m.startLine);
6847
+ throw new ToolValidationError({
6848
+ message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
6849
+ field: "old_string",
6850
+ context: { occurrences: count, matchTier: tier }
6851
+ });
6852
+ }
6853
+ if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
6854
+ throw new ToolValidationError({
6855
+ message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
6856
+ field: "old_string",
6857
+ context: { matchTier: tier }
6858
+ });
6859
+ }
6500
6860
  if (count > 1 && !input.replace_all) {
6501
- const lines = lineNumbersFor(fileLf, matches);
6861
+ const lines = matches.map((m) => m.startLine);
6502
6862
  throw new ToolValidationError({
6503
- message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`,
6863
+ message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
6504
6864
  field: "old_string",
6505
- context: { occurrences: count }
6865
+ context: { occurrences: count, matchTier: tier }
6506
6866
  });
6507
6867
  }
6508
- const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
6868
+ let newFileLf;
6869
+ let tierNote;
6870
+ if (tier === "exact") {
6871
+ newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
6872
+ } else {
6873
+ let replacement = newLf;
6874
+ let indentSuffix = "";
6875
+ if (tier === "whitespace-normalized" || tier === "fuzzy") {
6876
+ const first = matches[0];
6877
+ const matchedText = fileLf.slice(first.start, first.end);
6878
+ const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));
6879
+ replacement = adjusted.text;
6880
+ if (adjusted.adjusted) indentSuffix = "; replacement re-indented to match the file";
6881
+ }
6882
+ newFileLf = fileLf;
6883
+ const applied = input.replace_all ? matches : matches.slice(0, 1);
6884
+ for (let i = applied.length - 1; i >= 0; i--) {
6885
+ const m = applied[i];
6886
+ newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);
6887
+ }
6888
+ const scoreSuffix = ladder.score !== void 0 ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : "";
6889
+ tierNote = `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ${matches[0].startLine} (confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). Review the diff to confirm the intended target was edited.`;
6890
+ }
6509
6891
  const newFile = toStyle(newFileLf, style);
6892
+ opts?.signal?.throwIfAborted();
6510
6893
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
6511
6894
  const written = await fs2.stat(absPath);
6512
- ctx.recordRead(absPath, written.mtimeMs, "write");
6895
+ ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
6513
6896
  ctx.session.recordFileChange({
6514
6897
  path: absPath,
6515
6898
  action: "modified",
@@ -6520,38 +6903,22 @@ var editTool = {
6520
6903
  fromFile: input.path,
6521
6904
  toFile: input.path
6522
6905
  });
6906
+ const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
6907
+ let syntaxNote;
6908
+ if (syntax && syntax.errors.length > 0) {
6909
+ syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
6910
+ }
6911
+ const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
6523
6912
  return {
6524
6913
  path: absPath,
6525
6914
  replacements: input.replace_all ? count : 1,
6526
6915
  diff,
6527
- note: autoReadNote
6916
+ matched_by: tier,
6917
+ syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : void 0,
6918
+ note: notes.length > 0 ? notes.join("\n") : void 0
6528
6919
  };
6529
6920
  }
6530
6921
  };
6531
- function lineNumbersFor(text, indices) {
6532
- const out = [];
6533
- let pos = 0;
6534
- let line = 1;
6535
- for (const target of indices) {
6536
- while (pos < target) {
6537
- if (text.charCodeAt(pos) === 10) line++;
6538
- pos++;
6539
- }
6540
- out.push(line);
6541
- }
6542
- return out;
6543
- }
6544
- function findSimilarity(haystack, needle) {
6545
- if (needle.length < 20) return void 0;
6546
- const probe = needle.slice(0, Math.min(40, needle.length));
6547
- const idx = haystack.indexOf(probe);
6548
- if (idx === -1) return void 0;
6549
- let line = 1;
6550
- for (let i = 0; i < idx; i++) {
6551
- if (haystack.charCodeAt(i) === 10) line++;
6552
- }
6553
- return line;
6554
- }
6555
6922
  var isWin3 = process.platform === "win32";
6556
6923
  var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
6557
6924
  // JS / TS toolchain
@@ -7283,6 +7650,10 @@ var execTool = {
7283
7650
  category: "Shell",
7284
7651
  description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
7285
7652
  usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
7653
+ selection: {
7654
+ doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
7655
+ useInstead: ["bash"]
7656
+ },
7286
7657
  permission: "confirm",
7287
7658
  mutating: true,
7288
7659
  riskTier: "standard",
@@ -7946,13 +8317,13 @@ var formatTool = {
7946
8317
  }
7947
8318
  };
7948
8319
  async function detectFixer(cwd) {
7949
- const { stat: stat12 } = await import('node:fs/promises');
8320
+ const { stat: stat13 } = await import('node:fs/promises');
7950
8321
  try {
7951
- await stat12(`${cwd}/biome.json`);
8322
+ await stat13(`${cwd}/biome.json`);
7952
8323
  return "biome";
7953
8324
  } catch {
7954
8325
  try {
7955
- await stat12(`${cwd}/.prettierrc`);
8326
+ await stat13(`${cwd}/.prettierrc`);
7956
8327
  return "prettier";
7957
8328
  } catch {
7958
8329
  return "biome";
@@ -8113,8 +8484,8 @@ function findGitDir2(cwd, projectRoot) {
8113
8484
  let dir = cwd;
8114
8485
  for (let i = 0; i < 20; i++) {
8115
8486
  try {
8116
- const stat12 = statSync(`${dir}/.git`);
8117
- if (stat12.isDirectory() || stat12.isFile()) return dir;
8487
+ const stat13 = statSync(`${dir}/.git`);
8488
+ if (stat13.isDirectory() || stat13.isFile()) return dir;
8118
8489
  } catch {
8119
8490
  }
8120
8491
  if (dir === root) break;
@@ -8261,6 +8632,10 @@ var globTool = {
8261
8632
  category: "Filesystem",
8262
8633
  description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
8263
8634
  usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
8635
+ selection: {
8636
+ doNotUseWhen: "you need to search inside file contents.",
8637
+ useInstead: ["grep"]
8638
+ },
8264
8639
  permission: "auto",
8265
8640
  mutating: false,
8266
8641
  capabilities: ["fs.read"],
@@ -8285,8 +8660,9 @@ var globTool = {
8285
8660
  },
8286
8661
  required: ["pattern"]
8287
8662
  },
8288
- async execute(input, ctx) {
8663
+ async execute(input, ctx, opts) {
8289
8664
  if (!input?.pattern) throw new Error("glob: pattern is required");
8665
+ const signal = opts?.signal;
8290
8666
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
8291
8667
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
8292
8668
  const ignored = await readGitignore(base);
@@ -8310,6 +8686,10 @@ var globTool = {
8310
8686
  }
8311
8687
  };
8312
8688
  const walk2 = async (dir, relPrefix) => {
8689
+ if (signal?.aborted) {
8690
+ truncated = true;
8691
+ return;
8692
+ }
8313
8693
  if (results.length >= limit) {
8314
8694
  truncated = true;
8315
8695
  return;
@@ -8327,7 +8707,7 @@ var globTool = {
8327
8707
  if (DEFAULT_IGNORE2.includes(name)) continue;
8328
8708
  if (ignored.includes(name)) continue;
8329
8709
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
8330
- const full = path3.join(dir, name);
8710
+ const full = path12.join(dir, name);
8331
8711
  if (e.isDirectory()) {
8332
8712
  subdirs.push({ full, rel });
8333
8713
  } else if (e.isFile()) {
@@ -8371,7 +8751,7 @@ var globTool = {
8371
8751
  };
8372
8752
  async function readGitignore(dir) {
8373
8753
  try {
8374
- const raw = await fs2.readFile(path3.join(dir, ".gitignore"), "utf8");
8754
+ const raw = await fs2.readFile(path12.join(dir, ".gitignore"), "utf8");
8375
8755
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
8376
8756
  } catch {
8377
8757
  return [];
@@ -8433,6 +8813,10 @@ var grepTool = {
8433
8813
  category: "Search",
8434
8814
  description: "Search across files using a regular expression. This is one of the primary code search tools. Prefers ripgrep for speed and features when available.",
8435
8815
  usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
8816
+ selection: {
8817
+ doNotUseWhen: "you only need to locate files by name or path pattern.",
8818
+ useInstead: ["glob"]
8819
+ },
8436
8820
  permission: "auto",
8437
8821
  mutating: false,
8438
8822
  capabilities: ["fs.read"],
@@ -8672,8 +9056,8 @@ async function runNative(input, base, mode, limit, signal) {
8672
9056
  if (globRe && !globRe.test(name) && !globRe.test(full)) return;
8673
9057
  if (globRe) globRe.lastIndex = 0;
8674
9058
  try {
8675
- const stat12 = await fs2.stat(full);
8676
- if (!stat12.isFile() || stat12.size > maxBytes || stopped || signal.aborted) return;
9059
+ const stat13 = await fs2.stat(full);
9060
+ if (!stat13.isFile() || stat13.size > maxBytes || stopped || signal.aborted) return;
8677
9061
  const file = await fs2.open(full, "r");
8678
9062
  try {
8679
9063
  let bytesReadTotal = 0;
@@ -8763,7 +9147,7 @@ async function runNative(input, base, mode, limit, signal) {
8763
9147
  if (stopped) return;
8764
9148
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
8765
9149
  if (e.isSymbolicLink()) continue;
8766
- const full = path3.join(dir, e.name);
9150
+ const full = path12.join(dir, e.name);
8767
9151
  if (e.isDirectory()) {
8768
9152
  subdirs.push(full);
8769
9153
  } else if (e.isFile()) {
@@ -9310,56 +9694,56 @@ function jmespathSearch(data, query) {
9310
9694
  }
9311
9695
  function validateJsonSchema(data, schema) {
9312
9696
  const errors = [];
9313
- function check(value, s, path23) {
9697
+ function check(value, s, path24) {
9314
9698
  if (s["type"]) {
9315
9699
  const expectedType = s["type"];
9316
9700
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
9317
9701
  if (expectedType === "integer") {
9318
- if (!Number.isInteger(value)) errors.push(`${path23}: expected integer, got ${actualType}`);
9702
+ if (!Number.isInteger(value)) errors.push(`${path24}: expected integer, got ${actualType}`);
9319
9703
  } else if (expectedType !== actualType) {
9320
- errors.push(`${path23}: expected ${expectedType}, got ${actualType}`);
9704
+ errors.push(`${path24}: expected ${expectedType}, got ${actualType}`);
9321
9705
  }
9322
9706
  }
9323
9707
  if (typeof value === "string" && s["format"] === "uri" && value) {
9324
9708
  try {
9325
9709
  new URL(value);
9326
9710
  } catch {
9327
- errors.push(`${path23}: not a valid URI`);
9711
+ errors.push(`${path24}: not a valid URI`);
9328
9712
  }
9329
9713
  }
9330
9714
  if (typeof value === "string" && s["pattern"]) {
9331
9715
  const re = new RegExp(s["pattern"]);
9332
- if (!re.test(value)) errors.push(`${path23}: does not match pattern ${s["pattern"]}`);
9716
+ if (!re.test(value)) errors.push(`${path24}: does not match pattern ${s["pattern"]}`);
9333
9717
  }
9334
9718
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
9335
- errors.push(`${path23}: string too short (min ${s["minLength"]})`);
9719
+ errors.push(`${path24}: string too short (min ${s["minLength"]})`);
9336
9720
  }
9337
9721
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
9338
- errors.push(`${path23}: string too long (max ${s["maxLength"]})`);
9722
+ errors.push(`${path24}: string too long (max ${s["maxLength"]})`);
9339
9723
  }
9340
9724
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
9341
- errors.push(`${path23}: below minimum ${s["minimum"]}`);
9725
+ errors.push(`${path24}: below minimum ${s["minimum"]}`);
9342
9726
  }
9343
9727
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
9344
- errors.push(`${path23}: above maximum ${s["maximum"]}`);
9728
+ errors.push(`${path24}: above maximum ${s["maximum"]}`);
9345
9729
  }
9346
9730
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
9347
9731
  for (let i = 0; i < value.length; i++) {
9348
- check(value[i], s["items"], `${path23}[${i}]`);
9732
+ check(value[i], s["items"], `${path24}[${i}]`);
9349
9733
  }
9350
9734
  }
9351
9735
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
9352
9736
  const props = s["properties"];
9353
9737
  for (const [k, propSchema] of Object.entries(props)) {
9354
- check(value[k], propSchema, `${path23}.${k}`);
9738
+ check(value[k], propSchema, `${path24}.${k}`);
9355
9739
  }
9356
9740
  }
9357
9741
  }
9358
9742
  check(data, schema, "$");
9359
9743
  return { valid: errors.length === 0, errors };
9360
9744
  }
9361
- function simpleQuery(data, path23) {
9362
- const parts = path23.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
9745
+ function simpleQuery(data, path24) {
9746
+ const parts = path24.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
9363
9747
  let current = data;
9364
9748
  for (const part of parts) {
9365
9749
  if (current === null || current === void 0) return void 0;
@@ -10284,11 +10668,11 @@ var lintTool = {
10284
10668
  }
10285
10669
  };
10286
10670
  async function detectLinter(cwd) {
10287
- const { stat: stat12 } = await import('node:fs/promises');
10671
+ const { stat: stat13 } = await import('node:fs/promises');
10288
10672
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
10289
10673
  for (const f of checks) {
10290
10674
  try {
10291
- await stat12(`${cwd}/${f}`);
10675
+ await stat13(`${cwd}/${f}`);
10292
10676
  if (f.includes("biome")) return "biome";
10293
10677
  if (f.includes("eslint")) return "eslint";
10294
10678
  if (f.includes("tslint")) return "tslint";
@@ -10430,7 +10814,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
10430
10814
  }
10431
10815
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
10432
10816
  var MAX_TAIL_LINES = 1e5;
10433
- async function fileLogs(path23, lines, filterRe, stream) {
10817
+ async function fileLogs(path24, lines, filterRe, stream) {
10434
10818
  const { createInterface } = await import('node:readline');
10435
10819
  const { createReadStream } = await import('node:fs');
10436
10820
  const entries = [];
@@ -10439,7 +10823,7 @@ async function fileLogs(path23, lines, filterRe, stream) {
10439
10823
  let writeIdx = 0;
10440
10824
  let totalLines = 0;
10441
10825
  const rl = createInterface({
10442
- input: createReadStream(path23),
10826
+ input: createReadStream(path24),
10443
10827
  crlfDelay: Number.POSITIVE_INFINITY
10444
10828
  });
10445
10829
  for await (const line of rl) {
@@ -10460,7 +10844,7 @@ async function fileLogs(path23, lines, filterRe, stream) {
10460
10844
  if (parsed) entries.push(parsed);
10461
10845
  }
10462
10846
  return {
10463
- source: path23,
10847
+ source: path24,
10464
10848
  entries,
10465
10849
  total: entries.length,
10466
10850
  truncated: totalLines > effLines,
@@ -10627,6 +11011,10 @@ var patchTool = {
10627
11011
  category: "Filesystem",
10628
11012
  description: "Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.",
10629
11013
  usageHint: "Best used when you already have a diff (from generation, external source, or previous step).\n- Use `dry_run: true` to see what would happen without modifying files.\n- On failure it creates .rej and .orig files for manual review.\nOften cleaner than many small `edit` operations for larger changes.",
11014
+ selection: {
11015
+ doNotUseWhen: "you do not already have a unified diff or only need one precise replacement.",
11016
+ useInstead: ["edit"]
11017
+ },
10630
11018
  permission: "confirm",
10631
11019
  mutating: true,
10632
11020
  capabilities: ["fs.write"],
@@ -10648,12 +11036,13 @@ var patchTool = {
10648
11036
  const strip = Math.max(1, input.strip ?? 1);
10649
11037
  const dryRun = input.dry_run ?? false;
10650
11038
  const targets = extractDiffTargets(input.patch);
11039
+ const resolvedTargets = [];
10651
11040
  for (const t of targets) {
10652
11041
  const stripped = stripPathComponents(t, strip);
10653
11042
  if (!stripped) continue;
10654
- const candidate = path3.resolve(dir, stripped);
10655
- const rel = path3.relative(ctx.projectRoot, candidate);
10656
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
11043
+ const candidate = path12.resolve(dir, stripped);
11044
+ const rel = path12.relative(ctx.projectRoot, candidate);
11045
+ if (rel.startsWith("..") || path12.isAbsolute(rel)) {
10657
11046
  return {
10658
11047
  applied: 0,
10659
11048
  rejected: 1,
@@ -10662,12 +11051,19 @@ var patchTool = {
10662
11051
  message: `patch refused: target "${t}" resolves outside project root`
10663
11052
  };
10664
11053
  }
11054
+ resolvedTargets.push(candidate);
10665
11055
  }
10666
- const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), ".wstack_patch_"));
11056
+ const beforeContents = /* @__PURE__ */ new Map();
11057
+ if (!dryRun) {
11058
+ for (const target of resolvedTargets) {
11059
+ beforeContents.set(target, await readTextForTracking(target));
11060
+ }
11061
+ }
11062
+ const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), ".wstack_patch_"));
10667
11063
  try {
10668
11064
  await fs2.chmod(tmpDir, 448).catch(() => {
10669
11065
  });
10670
- const patchFile = path3.join(tmpDir, "in.diff");
11066
+ const patchFile = path12.join(tmpDir, "in.diff");
10671
11067
  await fs2.writeFile(patchFile, input.patch, { mode: 384 });
10672
11068
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
10673
11069
  const result = await runPatch(args, dir, opts.signal);
@@ -10681,6 +11077,21 @@ var patchTool = {
10681
11077
  };
10682
11078
  }
10683
11079
  const patched = extractPatchedFiles(result.stdout);
11080
+ if (!dryRun) {
11081
+ for (const target of resolvedTargets) {
11082
+ const before = beforeContents.get(target) ?? null;
11083
+ const after = await readTextForTracking(target);
11084
+ if (after === null || after === before) continue;
11085
+ const stat13 = await fs2.stat(target).catch(() => null);
11086
+ if (stat13) ctx.recordRead?.(target, stat13.mtimeMs, "write", sha256hex(after));
11087
+ ctx.session?.recordFileChange?.({
11088
+ path: target,
11089
+ action: before === null ? "created" : "modified",
11090
+ before,
11091
+ after
11092
+ });
11093
+ }
11094
+ }
10684
11095
  return {
10685
11096
  applied: patched.length,
10686
11097
  rejected: 0,
@@ -10694,6 +11105,18 @@ var patchTool = {
10694
11105
  }
10695
11106
  }
10696
11107
  };
11108
+ var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
11109
+ async function readTextForTracking(absPath) {
11110
+ try {
11111
+ const stat13 = await fs2.stat(absPath);
11112
+ if (!stat13.isFile() || stat13.size > MAX_TRACKING_BYTES) return null;
11113
+ const buf = await fs2.readFile(absPath);
11114
+ if (buf.includes(0)) return null;
11115
+ return buf.toString("utf8");
11116
+ } catch {
11117
+ return null;
11118
+ }
11119
+ }
10697
11120
  function extractDiffTargets(patch) {
10698
11121
  const out = [];
10699
11122
  const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
@@ -11001,6 +11424,10 @@ var readTool = {
11001
11424
  category: "Filesystem",
11002
11425
  description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.",
11003
11426
  usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.",
11427
+ selection: {
11428
+ doNotUseWhen: "you need to search many files for matching content.",
11429
+ useInstead: ["grep"]
11430
+ },
11004
11431
  permission: "auto",
11005
11432
  mutating: false,
11006
11433
  capabilities: ["fs.read"],
@@ -11038,9 +11465,9 @@ var readTool = {
11038
11465
  });
11039
11466
  }
11040
11467
  const absPath = await safeResolveReal(input.path, ctx);
11041
- let stat12;
11468
+ let stat13;
11042
11469
  try {
11043
- stat12 = await fs2.stat(absPath);
11470
+ stat13 = await fs2.stat(absPath);
11044
11471
  } catch (err) {
11045
11472
  const code = err.code;
11046
11473
  if (code === "ENOENT") {
@@ -11059,7 +11486,7 @@ var readTool = {
11059
11486
  cause: err
11060
11487
  });
11061
11488
  }
11062
- if (!stat12.isFile()) {
11489
+ if (!stat13.isFile()) {
11063
11490
  throw new FsError({
11064
11491
  message: `read: "${input.path}" is not a regular file`,
11065
11492
  code: "FS_READ_FAILED",
@@ -11067,22 +11494,22 @@ var readTool = {
11067
11494
  context: { reason: "not-a-regular-file" }
11068
11495
  });
11069
11496
  }
11070
- if (stat12.size > MAX_BYTES2) {
11497
+ if (stat13.size > MAX_BYTES2) {
11071
11498
  throw new FsError({
11072
- message: `read: file too large (${stat12.size} bytes, limit ${MAX_BYTES2})`,
11499
+ message: `read: file too large (${stat13.size} bytes, limit ${MAX_BYTES2})`,
11073
11500
  code: "FS_READ_FAILED",
11074
11501
  path: absPath,
11075
- context: { size: stat12.size, limit: MAX_BYTES2, reason: "too-large" }
11502
+ context: { size: stat13.size, limit: MAX_BYTES2, reason: "too-large" }
11076
11503
  });
11077
11504
  }
11078
11505
  const offset = Math.max(1, input.offset ?? 1);
11079
11506
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
11080
11507
  const prior = getReadRangeRecord(ctx, absPath);
11081
11508
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
11082
- if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat12.mtimeMs, offset, requestedEnd)) {
11083
- ctx.recordRead(absPath, stat12.mtimeMs);
11509
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat13.mtimeMs, offset, requestedEnd)) {
11510
+ ctx.recordRead(absPath, stat13.mtimeMs);
11084
11511
  return {
11085
- text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat12.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
11512
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat13.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
11086
11513
  total_lines: prior.totalLines,
11087
11514
  encoding: "utf8",
11088
11515
  truncated: requestedEnd < prior.totalLines,
@@ -11095,13 +11522,14 @@ var readTool = {
11095
11522
  throw new Error(`read: "${input.path}" appears to be binary`);
11096
11523
  }
11097
11524
  const text = buf.toString("utf8");
11525
+ const contentHash = sha256hex(text);
11098
11526
  const allLines = text.split(/\r\n|\r|\n/);
11099
11527
  const total = allLines.length;
11100
11528
  if (input.mode === "summary") {
11101
- ctx.recordRead(absPath, stat12.mtimeMs);
11102
- rememberReadRange(ctx, absPath, stat12.mtimeMs, total, 1, Math.min(total, 200));
11529
+ ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
11530
+ rememberReadRange(ctx, absPath, stat13.mtimeMs, total, 1, Math.min(total, 200));
11103
11531
  return {
11104
- text: summarizeFile(input.path, stat12.size, allLines),
11532
+ text: summarizeFile(input.path, stat13.size, allLines),
11105
11533
  total_lines: total,
11106
11534
  encoding: "utf8",
11107
11535
  truncated: total > 200,
@@ -11109,13 +11537,13 @@ var readTool = {
11109
11537
  };
11110
11538
  }
11111
11539
  if (limit === 0) {
11112
- ctx.recordRead(absPath, stat12.mtimeMs);
11113
- rememberReadRange(ctx, absPath, stat12.mtimeMs, total, 1, 0);
11540
+ ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
11541
+ rememberReadRange(ctx, absPath, stat13.mtimeMs, total, 1, 0);
11114
11542
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
11115
11543
  }
11116
11544
  if (offset > total) {
11117
- ctx.recordRead(absPath, stat12.mtimeMs);
11118
- rememberReadRange(ctx, absPath, stat12.mtimeMs, total, total + 1, total + 1);
11545
+ ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
11546
+ rememberReadRange(ctx, absPath, stat13.mtimeMs, total, total + 1, total + 1);
11119
11547
  return {
11120
11548
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
11121
11549
  total_lines: total,
@@ -11127,8 +11555,8 @@ var readTool = {
11127
11555
  const truncated = offset - 1 + slice.length < total;
11128
11556
  const width = String(offset + slice.length - 1).length;
11129
11557
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
11130
- ctx.recordRead(absPath, stat12.mtimeMs);
11131
- rememberReadRange(ctx, absPath, stat12.mtimeMs, total, offset, offset + slice.length - 1);
11558
+ ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
11559
+ rememberReadRange(ctx, absPath, stat13.mtimeMs, total, offset, offset + slice.length - 1);
11132
11560
  return {
11133
11561
  text: numbered,
11134
11562
  total_lines: total,
@@ -11270,10 +11698,10 @@ var replaceTool = {
11270
11698
  } catch {
11271
11699
  continue;
11272
11700
  }
11273
- const rel = path3.relative(realRoot, realPath);
11274
- if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
11275
- const stat12 = await fs2.stat(realPath).catch(() => null);
11276
- if (!stat12?.isFile()) continue;
11701
+ const rel = path12.relative(realRoot, realPath);
11702
+ if (rel.startsWith("..") || path12.isAbsolute(rel)) continue;
11703
+ const stat13 = await fs2.stat(realPath).catch(() => null);
11704
+ if (!stat13?.isFile()) continue;
11277
11705
  let content;
11278
11706
  try {
11279
11707
  const buf = await fs2.readFile(realPath);
@@ -11298,7 +11726,17 @@ var replaceTool = {
11298
11726
  totalReplacements += count;
11299
11727
  if (!dryRun) {
11300
11728
  const newContent = toStyle(newContentLf, style);
11301
- await atomicWrite(realPath, newContent, { mode: stat12.mode & 511 });
11729
+ await atomicWrite(realPath, newContent, { mode: stat13.mode & 511 });
11730
+ const written = await fs2.stat(realPath).catch(() => null);
11731
+ if (written) {
11732
+ ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
11733
+ }
11734
+ ctx.session?.recordFileChange?.({
11735
+ path: realPath,
11736
+ action: "modified",
11737
+ before: content,
11738
+ after: newContent
11739
+ });
11302
11740
  }
11303
11741
  const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
11304
11742
  fromFile: absPath,
@@ -11328,8 +11766,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
11328
11766
  const resolved = [];
11329
11767
  for (const p of parts) {
11330
11768
  const absPath = safeResolve(p, ctx);
11331
- const stat12 = await fs2.stat(absPath).catch(() => null);
11332
- if (stat12?.isFile()) {
11769
+ const stat13 = await fs2.stat(absPath).catch(() => null);
11770
+ if (stat13?.isFile()) {
11333
11771
  resolved.push(absPath);
11334
11772
  }
11335
11773
  }
@@ -11390,10 +11828,10 @@ async function globNative(pattern, base, extraGlob) {
11390
11828
  }
11391
11829
  for (const e of entries) {
11392
11830
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
11393
- const full = path3.join(dir, e.name);
11831
+ const full = path12.join(dir, e.name);
11394
11832
  try {
11395
- const stat12 = await fs2.lstat(full);
11396
- if (stat12.isSymbolicLink()) continue;
11833
+ const stat13 = await fs2.lstat(full);
11834
+ if (stat13.isSymbolicLink()) continue;
11397
11835
  } catch {
11398
11836
  continue;
11399
11837
  }
@@ -11561,16 +11999,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
11561
11999
  let filesCreated = 0;
11562
12000
  for (const [filePath, content] of Object.entries(templateFiles)) {
11563
12001
  const resolvedPath = substituteVars(filePath, name, vars);
11564
- const joinedPath = path3.join(cwd, resolvedPath);
11565
- const root = path3.resolve(ctx.projectRoot);
11566
- const target = path3.resolve(joinedPath);
11567
- const rel = path3.relative(root, target);
11568
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
12002
+ const joinedPath = path12.join(cwd, resolvedPath);
12003
+ const root = path12.resolve(ctx.projectRoot);
12004
+ const target = path12.resolve(joinedPath);
12005
+ const rel = path12.relative(root, target);
12006
+ if (rel.startsWith("..") || path12.isAbsolute(rel)) {
11569
12007
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
11570
12008
  }
11571
12009
  const fullPath = target;
11572
12010
  if (!dryRun) {
11573
- await fs2.mkdir(path3.dirname(fullPath), { recursive: true });
12011
+ await fs2.mkdir(path12.dirname(fullPath), { recursive: true });
11574
12012
  await atomicWrite(fullPath, substituteVars(content, name, vars));
11575
12013
  }
11576
12014
  files.push(resolvedPath);
@@ -12470,11 +12908,11 @@ var testTool = {
12470
12908
  }
12471
12909
  };
12472
12910
  async function detectRunner(cwd) {
12473
- const { stat: stat12 } = await import('node:fs/promises');
12911
+ const { stat: stat13 } = await import('node:fs/promises');
12474
12912
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
12475
12913
  for (const f of candidates) {
12476
12914
  try {
12477
- await stat12(path3.join(cwd, f));
12915
+ await stat13(path12.join(cwd, f));
12478
12916
  if (f.includes("vitest")) return "vitest";
12479
12917
  if (f.includes("jest")) return "jest";
12480
12918
  if (f.includes("mocha")) return "mocha";
@@ -13102,7 +13540,7 @@ async function walkDir(dir, depth, opts) {
13102
13540
  opts.lines.push(opts.prefix + branch + displayName);
13103
13541
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
13104
13542
  const childPrefix = opts.prefix + connector;
13105
- await walkDir(path3.join(dir, entry.name), depth + 1, {
13543
+ await walkDir(path12.join(dir, entry.name), depth + 1, {
13106
13544
  ...opts,
13107
13545
  prefix: childPrefix,
13108
13546
  isLast
@@ -13188,12 +13626,12 @@ var typecheckTool = {
13188
13626
  }
13189
13627
  };
13190
13628
  async function findTsConfig(cwd) {
13191
- const { stat: stat12 } = await import('node:fs/promises');
13629
+ const { stat: stat13 } = await import('node:fs/promises');
13192
13630
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
13193
13631
  for (const f of candidates) {
13194
13632
  try {
13195
- const s = await stat12(path3.join(cwd, f));
13196
- if (s.isFile()) return path3.join(cwd, f);
13633
+ const s = await stat13(path12.join(cwd, f));
13634
+ if (s.isFile()) return path12.join(cwd, f);
13197
13635
  } catch {
13198
13636
  }
13199
13637
  }
@@ -13204,6 +13642,10 @@ var writeTool = {
13204
13642
  category: "Filesystem",
13205
13643
  description: "Write or completely overwrite a file on disk. This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, because `edit` is safer and works on the last-read version of the file.",
13206
13644
  usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- You MUST have called `read` on the file earlier in the conversation before using `write` on an existing path (the system enforces this for safety).\n- The path is resolved relative to the project root and protected against escaping the workspace.",
13645
+ selection: {
13646
+ doNotUseWhen: "making a precise change to part of an existing file.",
13647
+ useInstead: ["edit"]
13648
+ },
13207
13649
  permission: "confirm",
13208
13650
  mutating: true,
13209
13651
  timeoutMs: 5e3,
@@ -13223,10 +13665,10 @@ var writeTool = {
13223
13665
  },
13224
13666
  required: ["path", "content"]
13225
13667
  },
13226
- async execute(input, ctx) {
13227
- return writeFile7(input, ctx);
13668
+ async execute(input, ctx, opts) {
13669
+ return writeFile7(input, ctx, opts?.signal);
13228
13670
  },
13229
- async *executeStream(input, ctx) {
13671
+ async *executeStream(input, ctx, opts) {
13230
13672
  const prepared = await prepareWrite(input, ctx);
13231
13673
  if (!prepared.existed) {
13232
13674
  for (const line of input.content.split("\n")) {
@@ -13234,11 +13676,11 @@ var writeTool = {
13234
13676
  `, data: { livePreview: true } };
13235
13677
  }
13236
13678
  }
13237
- yield { type: "final", output: await finishWrite(input, ctx, prepared) };
13679
+ yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
13238
13680
  }
13239
13681
  };
13240
- async function writeFile7(input, ctx) {
13241
- return finishWrite(input, ctx, await prepareWrite(input, ctx));
13682
+ async function writeFile7(input, ctx, signal) {
13683
+ return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
13242
13684
  }
13243
13685
  async function prepareWrite(input, ctx) {
13244
13686
  if (!input?.path) {
@@ -13257,12 +13699,12 @@ async function prepareWrite(input, ctx) {
13257
13699
  let existed = false;
13258
13700
  let prev = "";
13259
13701
  try {
13260
- const stat12 = await fs2.stat(absPath);
13261
- existed = stat12.isFile();
13702
+ const stat13 = await fs2.stat(absPath);
13703
+ existed = stat13.isFile();
13262
13704
  if (existed) {
13263
13705
  if (!ctx.hasRead(absPath)) {
13264
13706
  prev = await fs2.readFile(absPath, "utf8");
13265
- ctx.recordRead(absPath, stat12.mtimeMs, "write");
13707
+ ctx.recordRead(absPath, stat13.mtimeMs, "write", sha256hex(prev));
13266
13708
  } else {
13267
13709
  prev = await fs2.readFile(absPath, "utf8");
13268
13710
  }
@@ -13274,23 +13716,32 @@ async function prepareWrite(input, ctx) {
13274
13716
  }
13275
13717
  return { absPath, existed, prev };
13276
13718
  }
13277
- async function finishWrite(input, ctx, prepared) {
13719
+ async function finishWrite(input, ctx, prepared, signal) {
13720
+ signal?.throwIfAborted();
13278
13721
  await atomicWrite(prepared.absPath, input.content);
13279
13722
  const diff = prepared.existed ? unifiedDiff(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
13280
13723
  + (new file, ${input.content.split("\n").length} lines)`;
13281
- const stat12 = await fs2.stat(prepared.absPath);
13282
- ctx.recordRead(prepared.absPath, stat12.mtimeMs, "write");
13724
+ const stat13 = await fs2.stat(prepared.absPath);
13725
+ ctx.recordRead(prepared.absPath, stat13.mtimeMs, "write", sha256hex(input.content));
13283
13726
  ctx.session.recordFileChange({
13284
13727
  path: prepared.absPath,
13285
13728
  action: prepared.existed ? "modified" : "created",
13286
13729
  before: prepared.existed ? prepared.prev : null,
13287
13730
  after: input.content
13288
13731
  });
13732
+ const syntax = await checkSyntax(
13733
+ prepared.absPath,
13734
+ input.content,
13735
+ prepared.existed ? prepared.prev : void 0
13736
+ ).catch(() => void 0);
13737
+ const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
13289
13738
  return {
13290
13739
  path: prepared.absPath,
13291
13740
  bytes_written: Buffer.byteLength(input.content, "utf8"),
13292
13741
  created: !prepared.existed,
13293
- diff
13742
+ diff,
13743
+ syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
13744
+ note: hasSyntaxErrors ? syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.` : void 0
13294
13745
  };
13295
13746
  }
13296
13747
 
@@ -13699,6 +14150,7 @@ var ProcessGuardian = class {
13699
14150
  heartbeatTimer = null;
13700
14151
  isRunning = false;
13701
14152
  instanceId;
14153
+ sigtermCount = 0;
13702
14154
  constructor(config = {}) {
13703
14155
  this.registry = getPersistentProcessRegistry();
13704
14156
  this.config = {
@@ -13706,7 +14158,8 @@ var ProcessGuardian = class {
13706
14158
  autoResurrect: config.autoResurrect ?? false,
13707
14159
  // Disabled by default
13708
14160
  maxResurrectionAttempts: config.maxResurrectionAttempts ?? 3,
13709
- protectedPatterns: config.protectedPatterns ?? ["node", "wrongstack"]
14161
+ protectedPatterns: config.protectedPatterns ?? ["node", "wrongstack"],
14162
+ sigtermThreshold: config.sigtermThreshold ?? 3
13710
14163
  };
13711
14164
  this.instanceId = this.registry.getInstanceId();
13712
14165
  }
@@ -13849,15 +14302,43 @@ var ProcessGuardian = class {
13849
14302
  this.stop();
13850
14303
  process.exit(1);
13851
14304
  });
13852
- process.on("SIGTERM", (origin) => {
14305
+ process.on("SIGTERM", () => {
14306
+ this.sigtermCount++;
14307
+ const threshold = this.config.sigtermThreshold;
14308
+ if (threshold === Infinity) {
14309
+ console.log(JSON.stringify({
14310
+ level: "warn",
14311
+ event: "process_guardian.sigterm_ignored",
14312
+ pid: process.pid,
14313
+ instanceId: this.instanceId,
14314
+ sigtermCount: this.sigtermCount,
14315
+ message: "SIGTERM received but sigtermThreshold is Infinity \u2014 ignoring (use graceful shutdown instead)"
14316
+ }));
14317
+ return;
14318
+ }
14319
+ if (this.sigtermCount < threshold) {
14320
+ console.log(JSON.stringify({
14321
+ level: "warn",
14322
+ event: "process_guardian.sigterm_deferred",
14323
+ pid: process.pid,
14324
+ instanceId: this.instanceId,
14325
+ sigtermCount: this.sigtermCount,
14326
+ threshold,
14327
+ message: `SIGTERM received (${this.sigtermCount}/${threshold}) \u2014 ignoring until threshold is reached`
14328
+ }));
14329
+ return;
14330
+ }
13853
14331
  console.log(JSON.stringify({
13854
14332
  level: "warn",
13855
- event: "process_guardian.sigterm_received",
13856
- origin,
14333
+ event: "process_guardian.sigterm_exiting",
13857
14334
  pid: process.pid,
13858
14335
  instanceId: this.instanceId,
13859
- message: "SIGTERM received but ignored - use graceful shutdown instead"
14336
+ sigtermCount: this.sigtermCount,
14337
+ threshold,
14338
+ message: `SIGTERM threshold (${threshold}) reached \u2014 shutting down gracefully`
13860
14339
  }));
14340
+ this.stop();
14341
+ process.exit(0);
13861
14342
  });
13862
14343
  process.on("SIGHUP", () => {
13863
14344
  console.log(JSON.stringify({
@@ -14265,7 +14746,7 @@ function makeSkillTool(skillLoader) {
14265
14746
  field: "name"
14266
14747
  });
14267
14748
  }
14268
- const dir = path3.dirname(manifest.path);
14749
+ const dir = path12.dirname(manifest.path);
14269
14750
  let loadedResource;
14270
14751
  if (input.resource?.trim()) {
14271
14752
  loadedResource = await loadResource(dir, input.resource.trim());
@@ -14315,15 +14796,15 @@ ${listing}`;
14315
14796
  }
14316
14797
  async function loadResource(skillDir, rel) {
14317
14798
  const norm = rel.replace(/\\/g, "/");
14318
- if (path3.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
14799
+ if (path12.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
14319
14800
  throw new ToolValidationError({
14320
14801
  message: `skill: invalid resource path "${rel}"`,
14321
14802
  field: "resource"
14322
14803
  });
14323
14804
  }
14324
- const absPath = path3.resolve(skillDir, rel);
14325
- const root = path3.resolve(skillDir);
14326
- if (absPath !== root && !absPath.startsWith(root + path3.sep)) {
14805
+ const absPath = path12.resolve(skillDir, rel);
14806
+ const root = path12.resolve(skillDir);
14807
+ if (absPath !== root && !absPath.startsWith(root + path12.sep)) {
14327
14808
  throw new ToolValidationError({
14328
14809
  message: `skill: resource "${rel}" escapes the skill directory`,
14329
14810
  field: "resource"
@@ -14364,7 +14845,7 @@ async function walk(root, dir, out) {
14364
14845
  }
14365
14846
  for (const e of entries) {
14366
14847
  if (out.length >= MAX_LISTED_RESOURCES) return;
14367
- const fullPath = path3.join(dir, e.name);
14848
+ const fullPath = path12.join(dir, e.name);
14368
14849
  let isDir = e.isDirectory();
14369
14850
  if (e.isSymbolicLink()) {
14370
14851
  try {
@@ -14379,9 +14860,9 @@ async function walk(root, dir, out) {
14379
14860
  } else if (e.isFile()) {
14380
14861
  if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
14381
14862
  try {
14382
- const stat12 = await fs2.stat(fullPath);
14383
- const rel = path3.relative(root, fullPath).split(path3.sep).join("/");
14384
- out.push({ path: rel, bytes: stat12.size });
14863
+ const stat13 = await fs2.stat(fullPath);
14864
+ const rel = path12.relative(root, fullPath).split(path12.sep).join("/");
14865
+ out.push({ path: rel, bytes: stat13.size });
14385
14866
  } catch {
14386
14867
  }
14387
14868
  }