@theokit/sdk-tools 0.20.0 → 0.21.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.
package/dist/index.d.cts CHANGED
@@ -14,9 +14,15 @@ import { InteractiveProvider } from '@theokit/sdk/interactive';
14
14
  * security-checked) before ANY write. A parse error, context mismatch, or path violation anywhere ⇒ typed
15
15
  * error and ZERO writes (stronger than Codex, which writes file-by-file and can leave partial writes).
16
16
  *
17
- * Return shape (always a JSON string):
17
+ * Return shape (always a JSON string — never throws on a bad patch):
18
18
  * - `{ ok: true, files_patched: string[] }`
19
- * - `{ ok: false, error: 'parse_error' | 'path_traversal' | 'forbidden_path' | 'not_found' | 'patch_failed' }`
19
+ * - `{ ok: false, error: 'parse_error' | 'path_traversal' | 'forbidden_path' | 'not_found' |
20
+ * 'patch_failed' | 'duplicate_target' | 'file_exists' | 'io_error' }`
21
+ *
22
+ * Security/robustness (M18 review): forbidden secrets (`.env`/`.git`/`node_modules`/`.theo`) are blocked
23
+ * at ANY path depth and against absolute-path spelling; a file touched by two hunks is rejected
24
+ * (`duplicate_target`); Add over an existing file is rejected (`file_exists`); Delete of a missing file
25
+ * is `not_found`; any unexpected fs error maps to `io_error` (the handler never throws).
20
26
  */
21
27
 
22
28
  interface CreateApplyPatchToolOptions {
package/dist/index.d.ts CHANGED
@@ -14,9 +14,15 @@ import { InteractiveProvider } from '@theokit/sdk/interactive';
14
14
  * security-checked) before ANY write. A parse error, context mismatch, or path violation anywhere ⇒ typed
15
15
  * error and ZERO writes (stronger than Codex, which writes file-by-file and can leave partial writes).
16
16
  *
17
- * Return shape (always a JSON string):
17
+ * Return shape (always a JSON string — never throws on a bad patch):
18
18
  * - `{ ok: true, files_patched: string[] }`
19
- * - `{ ok: false, error: 'parse_error' | 'path_traversal' | 'forbidden_path' | 'not_found' | 'patch_failed' }`
19
+ * - `{ ok: false, error: 'parse_error' | 'path_traversal' | 'forbidden_path' | 'not_found' |
20
+ * 'patch_failed' | 'duplicate_target' | 'file_exists' | 'io_error' }`
21
+ *
22
+ * Security/robustness (M18 review): forbidden secrets (`.env`/`.git`/`node_modules`/`.theo`) are blocked
23
+ * at ANY path depth and against absolute-path spelling; a file touched by two hunks is rejected
24
+ * (`duplicate_target`); Add over an existing file is rejected (`file_exists`); Delete of a missing file
25
+ * is `not_found`; any unexpected fs error maps to `io_error` (the handler never throws).
20
26
  */
21
27
 
22
28
  interface CreateApplyPatchToolOptions {
package/dist/index.js CHANGED
@@ -1,106 +1,18 @@
1
1
  import { rm, mkdir, writeFile, readFile, readdir, open, stat, copyFile } from 'fs/promises';
2
- import { dirname, join, relative, isAbsolute, resolve, sep } from 'path';
2
+ import { dirname, relative, join, isAbsolute } from 'path';
3
3
  import { Tool, ConfigurationError } from '@theokit/sdk';
4
4
  import { z } from 'zod';
5
- import { existsSync, statSync, mkdirSync, writeFileSync, realpathSync, readFileSync, lstatSync, readlinkSync, readdirSync } from 'fs';
6
- import { safeFilenameForId, safePathJoin as safePathJoin$1 } from '@theokit/sdk/path-safety';
5
+ import { safePathJoin, assertNoSymlinkEscape, PathTraversalError, ForbiddenPathError, isForbiddenPath, safeFilenameForId } from '@theokit/sdk/path-safety';
7
6
  import { replaceFileAtomic } from '@theokit/sdk/persistence';
8
7
  import { resolveFilesystem, FileNotFoundError, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError } from '@theokit/sdk/filesystem';
9
8
  import { spawn } from 'child_process';
9
+ import { existsSync, statSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
10
10
  import { resolveSandbox } from '@theokit/sdk/sandbox';
11
11
  import { resolveInteractive, InteractiveUnavailableError, NoSuchSessionError } from '@theokit/sdk/interactive';
12
12
  import { lookup } from 'dns/promises';
13
13
  import { isIP } from 'net';
14
14
 
15
15
  // src/apply-patch.ts
16
- var PathTraversalError = class extends ConfigurationError {
17
- name = "PathTraversalError";
18
- constructor(input, resolvedPath) {
19
- super(`Path traversal attempt: ${input} \u2192 ${resolvedPath}`, {
20
- code: "path_traversal"
21
- });
22
- }
23
- };
24
- var ForbiddenPathError = class extends ConfigurationError {
25
- name = "ForbiddenPathError";
26
- constructor(path) {
27
- super(
28
- `Path '${path}' is in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files)`,
29
- {
30
- code: "forbidden_path"
31
- }
32
- );
33
- }
34
- };
35
- function safePathJoin(base, ...parts) {
36
- if (base === "") {
37
- throw new Error("safePathJoin: base must be non-empty");
38
- }
39
- const baseResolved = resolve(base);
40
- const target = resolve(base, ...parts);
41
- if (target !== baseResolved && !target.startsWith(baseResolved + sep)) {
42
- throw new PathTraversalError(parts.join("/"), target);
43
- }
44
- return target;
45
- }
46
- function assertNoSymlinkEscape(path, base) {
47
- let baseResolved;
48
- try {
49
- baseResolved = realpathSync(base);
50
- } catch {
51
- baseResolved = resolve(base);
52
- }
53
- const resolved = realpathOfDeepestExisting(path);
54
- if (resolved === void 0) return;
55
- if (resolved !== baseResolved && !resolved.startsWith(baseResolved + sep)) {
56
- throw new PathTraversalError(`symlink ${path}`, resolved);
57
- }
58
- }
59
- function realpathOfDeepestExisting(path) {
60
- try {
61
- return realpathSync(path);
62
- } catch {
63
- }
64
- try {
65
- const stat2 = lstatSync(path);
66
- if (stat2.isSymbolicLink()) {
67
- const target = readlinkSync(path);
68
- const parentReal = realpathOfDeepestExisting(dirname(path));
69
- const parentBase = parentReal ?? dirname(path);
70
- return resolve(parentBase, target);
71
- }
72
- } catch {
73
- }
74
- let cursor = dirname(path);
75
- let suffix = path.slice(cursor.length);
76
- while (cursor !== dirname(cursor)) {
77
- try {
78
- const real = realpathSync(cursor);
79
- return resolve(real, `.${suffix}`);
80
- } catch {
81
- suffix = path.slice(dirname(cursor).length);
82
- cursor = dirname(cursor);
83
- }
84
- }
85
- return void 0;
86
- }
87
- var LOCK_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
88
- function isForbiddenPath(input) {
89
- const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
90
- if (normalized.length === 0) return false;
91
- const segments = normalized.split("/").filter((s) => s.length > 0);
92
- if (segments.length === 0) return false;
93
- const first = segments[0];
94
- if (first === ".env.example") return false;
95
- if (first === ".env") return true;
96
- if (/^\.env\./.test(first)) return true;
97
- if (first === ".git") return true;
98
- if (first === "node_modules") return true;
99
- if (first === ".theo") return true;
100
- const basename = segments[segments.length - 1];
101
- if (LOCK_FILES.has(basename)) return true;
102
- return false;
103
- }
104
16
 
105
17
  // src/internal/v4a-patch.ts
106
18
  var BEGIN = "*** Begin Patch";
@@ -265,7 +177,14 @@ function matchesAt(lines, pattern, at, eq) {
265
177
  function seekSequence(lines, pattern, searchStart, eof) {
266
178
  if (pattern.length === 0) return searchStart;
267
179
  if (pattern.length > lines.length) return null;
268
- const start = eof ? Math.max(searchStart, lines.length - pattern.length) : searchStart;
180
+ const starts = eof ? [Math.max(searchStart, lines.length - pattern.length), searchStart] : [searchStart];
181
+ for (const start of starts) {
182
+ const hit = searchFrom(lines, pattern, start);
183
+ if (hit !== null) return hit;
184
+ }
185
+ return null;
186
+ }
187
+ function searchFrom(lines, pattern, start) {
269
188
  for (const eq of LADDER) {
270
189
  for (let i = start; i <= lines.length - pattern.length; i++) {
271
190
  if (matchesAt(lines, pattern, i, eq)) return i;
@@ -322,12 +241,28 @@ async function applyV4APatch(projectRoot, patch) {
322
241
  const detail = err instanceof V4APatchError ? err.message : String(err);
323
242
  return JSON.stringify({ ok: false, error: "parse_error", detail });
324
243
  }
325
- const plan = await buildPlan(projectRoot, hunks);
326
- if ("error" in plan) return plan.error;
327
- await executePlan(plan.ops);
328
- return JSON.stringify({ ok: true, files_patched: plan.patched });
244
+ try {
245
+ const plan = await buildPlan(projectRoot, hunks);
246
+ if ("error" in plan) return plan.error;
247
+ await executePlan(plan.ops);
248
+ return JSON.stringify({ ok: true, files_patched: plan.patched });
249
+ } catch (err) {
250
+ return JSON.stringify({
251
+ ok: false,
252
+ error: "io_error",
253
+ detail: err instanceof Error ? err.message : String(err)
254
+ });
255
+ }
256
+ }
257
+ function hunkTargets(hunk) {
258
+ if (hunk.kind === "update" && hunk.movePath) return [hunk.path, hunk.movePath];
259
+ return [hunk.path];
329
260
  }
330
261
  async function buildPlan(projectRoot, hunks) {
262
+ const dup = firstDuplicateTarget(hunks);
263
+ if (dup !== null) {
264
+ return { error: JSON.stringify({ ok: false, error: "duplicate_target", path: dup }) };
265
+ }
331
266
  const ops = [];
332
267
  const patched = [];
333
268
  for (const hunk of hunks) {
@@ -338,6 +273,16 @@ async function buildPlan(projectRoot, hunks) {
338
273
  }
339
274
  return { ops, patched };
340
275
  }
276
+ function firstDuplicateTarget(hunks) {
277
+ const seen = /* @__PURE__ */ new Set();
278
+ for (const hunk of hunks) {
279
+ for (const t of hunkTargets(hunk)) {
280
+ if (seen.has(t)) return t;
281
+ seen.add(t);
282
+ }
283
+ }
284
+ return null;
285
+ }
341
286
  async function executePlan(ops) {
342
287
  for (const op of ops) {
343
288
  if ("rm" in op) {
@@ -348,11 +293,29 @@ async function executePlan(ops) {
348
293
  }
349
294
  }
350
295
  }
296
+ async function pathExists(abs) {
297
+ try {
298
+ await stat(abs);
299
+ return true;
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
351
304
  async function planHunk(projectRoot, hunk) {
352
305
  const scope = v4aScope(projectRoot, hunk.path);
353
306
  if ("error" in scope) return scope;
354
- if (hunk.kind === "add") return { ops: [{ write: { abs: scope.abs, content: hunk.content } }] };
355
- if (hunk.kind === "delete") return { ops: [{ rm: scope.abs }] };
307
+ if (hunk.kind === "add") {
308
+ if (await pathExists(scope.abs)) {
309
+ return { error: JSON.stringify({ ok: false, error: "file_exists", path: hunk.path }) };
310
+ }
311
+ return { ops: [{ write: { abs: scope.abs, content: hunk.content } }] };
312
+ }
313
+ if (hunk.kind === "delete") {
314
+ if (!await pathExists(scope.abs)) {
315
+ return { error: JSON.stringify({ ok: false, error: "not_found", path: hunk.path }) };
316
+ }
317
+ return { ops: [{ rm: scope.abs }] };
318
+ }
356
319
  return planUpdate(projectRoot, hunk, scope.abs);
357
320
  }
358
321
  async function planUpdate(projectRoot, hunk, abs) {
@@ -386,27 +349,32 @@ async function planUpdate(projectRoot, hunk, abs) {
386
349
  if ("error" in dest) return dest;
387
350
  return { ops: [{ write: { abs: dest.abs, content: updated } }, { rm: abs }] };
388
351
  }
352
+ var FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
353
+ function isForbiddenRel(rel) {
354
+ return rel.split(/[/\\]/).filter(Boolean).some((s) => s !== ".env.example" && (FORBIDDEN_SEGMENTS.has(s) || /^\.env\./.test(s)));
355
+ }
389
356
  function v4aScope(projectRoot, file) {
390
- if (isForbiddenPath(file)) {
391
- return { error: JSON.stringify({ ok: false, error: "forbidden_path", path: file }) };
392
- }
357
+ let abs;
393
358
  try {
394
- const abs = safePathJoin(projectRoot, file);
359
+ abs = safePathJoin(projectRoot, file);
395
360
  assertNoSymlinkEscape(abs, projectRoot);
396
- return { abs };
397
361
  } catch (err) {
398
362
  if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
399
363
  return { error: JSON.stringify({ ok: false, error: "path_traversal", path: file }) };
400
364
  }
401
365
  throw err;
402
366
  }
367
+ if (isForbiddenPath(file) || isForbiddenRel(relative(projectRoot, abs))) {
368
+ return { error: JSON.stringify({ ok: false, error: "forbidden_path", path: file }) };
369
+ }
370
+ return { abs };
403
371
  }
404
372
  function createSessionArtifactStore(options) {
405
373
  const { dir } = options;
406
374
  const idStrategy = options.idStrategy ?? ((id) => safeFilenameForId(id));
407
375
  const extension = options.extension ?? ".md";
408
376
  function path(id) {
409
- return safePathJoin$1(dir, `${idStrategy(id)}${extension}`);
377
+ return safePathJoin(dir, `${idStrategy(id)}${extension}`);
410
378
  }
411
379
  async function write(id, content) {
412
380
  const target = path(id);
@@ -742,25 +710,25 @@ function createSettleGate(timer) {
742
710
  }
743
711
  };
744
712
  }
745
- function armTimeoutKill(child, timeoutMs, onTimeout, resolve2) {
713
+ function armTimeoutKill(child, timeoutMs, onTimeout, resolve) {
746
714
  const timer = setTimeout(() => {
747
715
  gate.fire(() => {
748
716
  try {
749
717
  process.kill(-(child.pid ?? 0), "SIGKILL");
750
718
  } catch {
751
719
  }
752
- resolve2(onTimeout());
720
+ resolve(onTimeout());
753
721
  });
754
722
  }, timeoutMs);
755
723
  const gate = createSettleGate(timer);
756
724
  return gate;
757
725
  }
758
- function attachChildSettlers(child, gate, onClose, onError, resolve2) {
726
+ function attachChildSettlers(child, gate, onClose, onError, resolve) {
759
727
  child.on("close", (code) => {
760
- gate.fire(() => resolve2(onClose(code)));
728
+ gate.fire(() => resolve(onClose(code)));
761
729
  });
762
730
  child.on("error", (err) => {
763
- gate.fire(() => resolve2(onError(err)));
731
+ gate.fire(() => resolve(onError(err)));
764
732
  });
765
733
  }
766
734
 
@@ -827,7 +795,7 @@ function formatGitResult(result, timeoutMs) {
827
795
  return JSON.stringify({ ok: true, diff: result.stdout, truncated: result.truncated });
828
796
  }
829
797
  function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
830
- return new Promise((resolve2) => {
798
+ return new Promise((resolve) => {
831
799
  const child = spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
832
800
  const stdoutChunks = [];
833
801
  const stderrChunks = [];
@@ -837,7 +805,7 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
837
805
  child,
838
806
  timeoutMs,
839
807
  () => ({ kind: "timeout" }),
840
- resolve2
808
+ resolve
841
809
  );
842
810
  child.stdout.on("data", (chunk) => {
843
811
  if (gate.settled()) return;
@@ -867,7 +835,7 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
867
835
  return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
868
836
  },
869
837
  (err) => ({ kind: "error", stderr: err.message }),
870
- resolve2
838
+ resolve
871
839
  );
872
840
  });
873
841
  }
@@ -1546,29 +1514,29 @@ function createListDirTool(opts) {
1546
1514
  path: z.string().min(1).describe("Project-relative directory path. Use '.' for root.")
1547
1515
  }),
1548
1516
  handler: async ({ path }, ctx) => {
1549
- const relative2 = path === "" || path === "." ? "." : path;
1550
- if (relative2 !== "." && isForbiddenPath(relative2)) {
1517
+ const relative3 = path === "" || path === "." ? "." : path;
1518
+ if (relative3 !== "." && isForbiddenPath(relative3)) {
1551
1519
  return JSON.stringify({ ok: false, error: "forbidden_path", path });
1552
1520
  }
1553
1521
  if (filesystem) {
1554
1522
  const backend = await resolveFilesystem(filesystem, ctx ?? {});
1555
- return listViaBackend(backend, relative2, path, max);
1523
+ return listViaBackend(backend, relative3, path, max);
1556
1524
  }
1557
- return listViaLocalFs(projectRoot, relative2, path, max);
1525
+ return listViaLocalFs(projectRoot, relative3, path, max);
1558
1526
  }
1559
1527
  });
1560
1528
  }
1561
- async function listViaLocalFs(projectRoot, relative2, originalPath, max) {
1562
- const boundary = resolveDirBoundary(relative2, projectRoot, originalPath);
1529
+ async function listViaLocalFs(projectRoot, relative3, originalPath, max) {
1530
+ const boundary = resolveDirBoundary(relative3, projectRoot, originalPath);
1563
1531
  if ("error" in boundary) return boundary.error;
1564
1532
  const readResult = await readDirSafe(boundary.absolutePath, originalPath);
1565
1533
  if ("error" in readResult) return readResult.error;
1566
1534
  return formatListing(readResult.dirents, max);
1567
1535
  }
1568
- async function listViaBackend(backend, relative2, originalPath, max) {
1536
+ async function listViaBackend(backend, relative3, originalPath, max) {
1569
1537
  let names;
1570
1538
  try {
1571
- names = await backend.list(relative2);
1539
+ names = await backend.list(relative3);
1572
1540
  } catch (err) {
1573
1541
  if (err instanceof FileNotFoundError) {
1574
1542
  return JSON.stringify({ ok: false, error: "not_found", path: originalPath });
@@ -1582,7 +1550,7 @@ async function listViaBackend(backend, relative2, originalPath, max) {
1582
1550
  const windowed = names.slice(0, max);
1583
1551
  const entries = await Promise.all(
1584
1552
  windowed.map(async (name) => {
1585
- const child = relative2 === "." ? name : `${relative2}/${name}`;
1553
+ const child = relative3 === "." ? name : `${relative3}/${name}`;
1586
1554
  let type = "file";
1587
1555
  try {
1588
1556
  type = (await backend.stat(child)).isDirectory ? "directory" : "file";
@@ -1593,9 +1561,9 @@ async function listViaBackend(backend, relative2, originalPath, max) {
1593
1561
  );
1594
1562
  return JSON.stringify({ ok: true, entries, truncated: totalCount > max, totalCount });
1595
1563
  }
1596
- function resolveDirBoundary(relative2, projectRoot, originalPath) {
1564
+ function resolveDirBoundary(relative3, projectRoot, originalPath) {
1597
1565
  try {
1598
- const absolutePath = relative2 === "." ? projectRoot : safePathJoin(projectRoot, relative2);
1566
+ const absolutePath = relative3 === "." ? projectRoot : safePathJoin(projectRoot, relative3);
1599
1567
  assertNoSymlinkEscape(absolutePath, projectRoot);
1600
1568
  return { absolutePath };
1601
1569
  } catch (err) {
@@ -1817,22 +1785,22 @@ function createReadFileTool(opts) {
1817
1785
  }
1818
1786
  async function readViaBackend(backend, path, view, onRead) {
1819
1787
  try {
1820
- const stat2 = await backend.stat(path);
1821
- if (stat2.size > MAX_FILE_SIZE) {
1788
+ const stat3 = await backend.stat(path);
1789
+ if (stat3.size > MAX_FILE_SIZE) {
1822
1790
  return JSON.stringify({
1823
1791
  ok: false,
1824
1792
  error: "too_large",
1825
1793
  path,
1826
- size: stat2.size,
1794
+ size: stat3.size,
1827
1795
  limit: MAX_FILE_SIZE
1828
1796
  });
1829
1797
  }
1830
1798
  const raw = await backend.readFile(path);
1831
1799
  if (raw.includes("\0")) {
1832
- return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
1800
+ return JSON.stringify({ ok: false, error: "binary_file", path, size: stat3.size });
1833
1801
  }
1834
- onRead?.(stat2.mtimeMs);
1835
- return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat2.size });
1802
+ onRead?.(stat3.mtimeMs);
1803
+ return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat3.size });
1836
1804
  } catch (err) {
1837
1805
  if (err instanceof FileNotFoundError) {
1838
1806
  return JSON.stringify({ ok: false, error: "not_found", path });
@@ -1871,22 +1839,22 @@ async function openHandleSafe(absolutePath, path) {
1871
1839
  }
1872
1840
  }
1873
1841
  async function readContent(handle, path, view, onRead) {
1874
- const stat2 = await handle.stat();
1875
- if (stat2.size > MAX_FILE_SIZE) {
1842
+ const stat3 = await handle.stat();
1843
+ if (stat3.size > MAX_FILE_SIZE) {
1876
1844
  return JSON.stringify({
1877
1845
  ok: false,
1878
1846
  error: "too_large",
1879
1847
  path,
1880
- size: stat2.size,
1848
+ size: stat3.size,
1881
1849
  limit: MAX_FILE_SIZE
1882
1850
  });
1883
1851
  }
1884
- if (await isBinaryProbe(handle, Number(stat2.size))) {
1885
- return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
1852
+ if (await isBinaryProbe(handle, Number(stat3.size))) {
1853
+ return JSON.stringify({ ok: false, error: "binary_file", path, size: stat3.size });
1886
1854
  }
1887
1855
  const raw = await handle.readFile({ encoding: "utf-8" });
1888
- onRead?.(stat2.mtimeMs);
1889
- return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat2.size });
1856
+ onRead?.(stat3.mtimeMs);
1857
+ return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat3.size });
1890
1858
  }
1891
1859
  async function isBinaryProbe(handle, size) {
1892
1860
  const probeLen = Math.min(BINARY_PROBE_BYTES, size);
@@ -2020,7 +1988,7 @@ function appendCapped(chunks, chunk, current, cap) {
2020
1988
  return current + chunk.length;
2021
1989
  }
2022
1990
  function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
2023
- return new Promise((resolve2) => {
1991
+ return new Promise((resolve) => {
2024
1992
  const child = spawn(command, args, {
2025
1993
  cwd,
2026
1994
  detached: true,
@@ -2033,7 +2001,7 @@ function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
2033
2001
  child,
2034
2002
  timeoutMs,
2035
2003
  () => ({ kind: "timeout" }),
2036
- resolve2
2004
+ resolve
2037
2005
  );
2038
2006
  child.stdout.on("data", (chunk) => {
2039
2007
  if (gate.settled()) return;
@@ -2052,7 +2020,7 @@ function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
2052
2020
  exitCode: code ?? 0
2053
2021
  }),
2054
2022
  (err) => ({ kind: "spawn_error", message: err.message }),
2055
- resolve2
2023
+ resolve
2056
2024
  );
2057
2025
  });
2058
2026
  }
@@ -2303,7 +2271,7 @@ function createShellTool(opts) {
2303
2271
  });
2304
2272
  }
2305
2273
  function runShell(cwd, command, timeoutMs) {
2306
- return new Promise((resolve2) => {
2274
+ return new Promise((resolve) => {
2307
2275
  const child = spawn("/bin/sh", ["-c", command], {
2308
2276
  cwd,
2309
2277
  detached: true,
@@ -2317,7 +2285,7 @@ function runShell(cwd, command, timeoutMs) {
2317
2285
  child,
2318
2286
  timeoutMs,
2319
2287
  () => ({ kind: "timeout" }),
2320
- (result) => resolve2(formatResult(result, timeoutMs))
2288
+ (result) => resolve(formatResult(result, timeoutMs))
2321
2289
  );
2322
2290
  child.stdout.on("data", (chunk) => {
2323
2291
  if (gate.settled()) return;
@@ -2355,7 +2323,7 @@ function runShell(cwd, command, timeoutMs) {
2355
2323
  exitCode: code
2356
2324
  }),
2357
2325
  (err) => ({ kind: "error", message: err.message }),
2358
- (result) => resolve2(formatResult(result, timeoutMs))
2326
+ (result) => resolve(formatResult(result, timeoutMs))
2359
2327
  );
2360
2328
  });
2361
2329
  }
@@ -2802,12 +2770,12 @@ async function writeViaBackend(backend, path, content, guard) {
2802
2770
  if (rbw) return rbw;
2803
2771
  expectedMtime = current ?? void 0;
2804
2772
  }
2805
- const stat2 = await backend.writeFile(
2773
+ const stat3 = await backend.writeFile(
2806
2774
  path,
2807
2775
  content,
2808
2776
  expectedMtime !== void 0 ? { expectedMtime } : void 0
2809
2777
  );
2810
- return JSON.stringify({ ok: true, path, bytes: stat2.size });
2778
+ return JSON.stringify({ ok: true, path, bytes: stat3.size });
2811
2779
  } catch (err) {
2812
2780
  return backendErrorToJson(err, path);
2813
2781
  }
@@ -2835,8 +2803,8 @@ async function isBinaryFile(absolutePath) {
2835
2803
  return false;
2836
2804
  }
2837
2805
  try {
2838
- const stat2 = await handle.stat();
2839
- const probeLen = Math.min(BINARY_PROBE_BYTES3, Number(stat2.size));
2806
+ const stat3 = await handle.stat();
2807
+ const probeLen = Math.min(BINARY_PROBE_BYTES3, Number(stat3.size));
2840
2808
  if (probeLen <= 0) return false;
2841
2809
  const probe = Buffer.alloc(probeLen);
2842
2810
  const { bytesRead } = await handle.read(probe, 0, probeLen, 0);