@theokit/sdk-tools 0.20.1 → 0.22.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.cjs CHANGED
@@ -4,105 +4,17 @@ var promises = require('fs/promises');
4
4
  var path = require('path');
5
5
  var sdk = require('@theokit/sdk');
6
6
  var zod = require('zod');
7
- var fs = require('fs');
8
7
  var pathSafety = require('@theokit/sdk/path-safety');
9
8
  var persistence = require('@theokit/sdk/persistence');
10
9
  var filesystem = require('@theokit/sdk/filesystem');
11
- var child_process = require('child_process');
10
+ var fs = require('fs');
12
11
  var sandbox = require('@theokit/sdk/sandbox');
12
+ var child_process = require('child_process');
13
13
  var interactive = require('@theokit/sdk/interactive');
14
14
  var promises$1 = require('dns/promises');
15
15
  var net = require('net');
16
16
 
17
17
  // src/apply-patch.ts
18
- var PathTraversalError = class extends sdk.ConfigurationError {
19
- name = "PathTraversalError";
20
- constructor(input, resolvedPath) {
21
- super(`Path traversal attempt: ${input} \u2192 ${resolvedPath}`, {
22
- code: "path_traversal"
23
- });
24
- }
25
- };
26
- var ForbiddenPathError = class extends sdk.ConfigurationError {
27
- name = "ForbiddenPathError";
28
- constructor(path) {
29
- super(
30
- `Path '${path}' is in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files)`,
31
- {
32
- code: "forbidden_path"
33
- }
34
- );
35
- }
36
- };
37
- function safePathJoin(base, ...parts) {
38
- if (base === "") {
39
- throw new Error("safePathJoin: base must be non-empty");
40
- }
41
- const baseResolved = path.resolve(base);
42
- const target = path.resolve(base, ...parts);
43
- if (target !== baseResolved && !target.startsWith(baseResolved + path.sep)) {
44
- throw new PathTraversalError(parts.join("/"), target);
45
- }
46
- return target;
47
- }
48
- function assertNoSymlinkEscape(path$1, base) {
49
- let baseResolved;
50
- try {
51
- baseResolved = fs.realpathSync(base);
52
- } catch {
53
- baseResolved = path.resolve(base);
54
- }
55
- const resolved = realpathOfDeepestExisting(path$1);
56
- if (resolved === void 0) return;
57
- if (resolved !== baseResolved && !resolved.startsWith(baseResolved + path.sep)) {
58
- throw new PathTraversalError(`symlink ${path$1}`, resolved);
59
- }
60
- }
61
- function realpathOfDeepestExisting(path$1) {
62
- try {
63
- return fs.realpathSync(path$1);
64
- } catch {
65
- }
66
- try {
67
- const stat3 = fs.lstatSync(path$1);
68
- if (stat3.isSymbolicLink()) {
69
- const target = fs.readlinkSync(path$1);
70
- const parentReal = realpathOfDeepestExisting(path.dirname(path$1));
71
- const parentBase = parentReal ?? path.dirname(path$1);
72
- return path.resolve(parentBase, target);
73
- }
74
- } catch {
75
- }
76
- let cursor = path.dirname(path$1);
77
- let suffix = path$1.slice(cursor.length);
78
- while (cursor !== path.dirname(cursor)) {
79
- try {
80
- const real = fs.realpathSync(cursor);
81
- return path.resolve(real, `.${suffix}`);
82
- } catch {
83
- suffix = path$1.slice(path.dirname(cursor).length);
84
- cursor = path.dirname(cursor);
85
- }
86
- }
87
- return void 0;
88
- }
89
- var LOCK_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
90
- function isForbiddenPath(input) {
91
- const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
92
- if (normalized.length === 0) return false;
93
- const segments = normalized.split("/").filter((s) => s.length > 0);
94
- if (segments.length === 0) return false;
95
- const first = segments[0];
96
- if (first === ".env.example") return false;
97
- if (first === ".env") return true;
98
- if (/^\.env\./.test(first)) return true;
99
- if (first === ".git") return true;
100
- if (first === "node_modules") return true;
101
- if (first === ".theo") return true;
102
- const basename = segments[segments.length - 1];
103
- if (LOCK_FILES.has(basename)) return true;
104
- return false;
105
- }
106
18
 
107
19
  // src/internal/v4a-patch.ts
108
20
  var BEGIN = "*** Begin Patch";
@@ -446,15 +358,15 @@ function isForbiddenRel(rel) {
446
358
  function v4aScope(projectRoot, file) {
447
359
  let abs;
448
360
  try {
449
- abs = safePathJoin(projectRoot, file);
450
- assertNoSymlinkEscape(abs, projectRoot);
361
+ abs = pathSafety.safePathJoin(projectRoot, file);
362
+ pathSafety.assertNoSymlinkEscape(abs, projectRoot);
451
363
  } catch (err) {
452
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
364
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
453
365
  return { error: JSON.stringify({ ok: false, error: "path_traversal", path: file }) };
454
366
  }
455
367
  throw err;
456
368
  }
457
- if (isForbiddenPath(file) || isForbiddenRel(path.relative(projectRoot, abs))) {
369
+ if (pathSafety.isForbiddenPath(file) || isForbiddenRel(path.relative(projectRoot, abs))) {
458
370
  return { error: JSON.stringify({ ok: false, error: "forbidden_path", path: file }) };
459
371
  }
460
372
  return { abs };
@@ -665,7 +577,7 @@ async function editViaBackend(filesystem$1, ctx, path, old_string, new_string) {
665
577
  return JSON.stringify({ ok: true, replacements: 1 });
666
578
  }
667
579
  async function editViaLocal(projectRoot, path, old_string, new_string) {
668
- const absolutePath = safePathJoin(projectRoot, path);
580
+ const absolutePath = pathSafety.safePathJoin(projectRoot, path);
669
581
  let content;
670
582
  try {
671
583
  content = await promises.readFile(absolutePath, "utf-8");
@@ -683,14 +595,14 @@ async function editViaLocal(projectRoot, path, old_string, new_string) {
683
595
  return JSON.stringify({ ok: true, replacements: 1 });
684
596
  }
685
597
  function editScopeError(path, projectRoot) {
686
- if (isForbiddenPath(path)) {
598
+ if (pathSafety.isForbiddenPath(path)) {
687
599
  return JSON.stringify({ ok: false, error: "forbidden_path", path });
688
600
  }
689
601
  try {
690
- assertNoSymlinkEscape(safePathJoin(projectRoot, path), projectRoot);
602
+ pathSafety.assertNoSymlinkEscape(pathSafety.safePathJoin(projectRoot, path), projectRoot);
691
603
  return null;
692
604
  } catch (err) {
693
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
605
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
694
606
  return JSON.stringify({ ok: false, error: "path_traversal", path });
695
607
  }
696
608
  throw err;
@@ -699,8 +611,8 @@ function editScopeError(path, projectRoot) {
699
611
  function createEditFileTool(opts) {
700
612
  const { projectRoot, filesystem } = opts;
701
613
  return sdk.Tool.create({
702
- name: "edit_file",
703
- description: "Make an exact string replacement in a project-relative file. Replaces the FIRST occurrence of old_string with new_string (a whitespace-normalized fallback is attempted if the exact match fails) and writes a .bak backup first. Read the file first so old_string matches the on-disk text exactly; include enough surrounding context to make it unique \u2014 only the first match is replaced, so a too-short old_string can edit the wrong location. old_string must be non-empty and differ from new_string; to change every occurrence, call edit_file repeatedly. Returns { ok, replacements } or { ok: false, error }.",
614
+ name: opts.name ?? "edit_file",
615
+ description: opts.description ?? "Make an exact string replacement in a project-relative file. Replaces the FIRST occurrence of old_string with new_string (a whitespace-normalized fallback is attempted if the exact match fails) and writes a .bak backup first. Read the file first so old_string matches the on-disk text exactly; include enough surrounding context to make it unique \u2014 only the first match is replaced, so a too-short old_string can edit the wrong location. old_string must be non-empty and differ from new_string; to change every occurrence, call edit_file repeatedly. Returns { ok, replacements } or { ok: false, error }.",
704
616
  inputSchema: zod.z.object({
705
617
  path: zod.z.string().min(1).describe("Project-relative file path."),
706
618
  old_string: zod.z.string().min(1).describe("String to find in the file."),
@@ -772,21 +684,6 @@ function formatError(message, code) {
772
684
  return `> **Error:** ${prefix}${message}`;
773
685
  }
774
686
 
775
- // src/path-scope.ts
776
- function checkPathScope(path, projectRoot) {
777
- if (path === void 0 || path === "") return null;
778
- try {
779
- const abs = safePathJoin(projectRoot, path);
780
- assertNoSymlinkEscape(abs, projectRoot);
781
- return null;
782
- } catch (err) {
783
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
784
- return JSON.stringify({ ok: false, error: "path_traversal", path });
785
- }
786
- throw err;
787
- }
788
- }
789
-
790
687
  // src/subprocess.ts
791
688
  function createSettleGate(timer) {
792
689
  let done = false;
@@ -800,25 +697,104 @@ function createSettleGate(timer) {
800
697
  }
801
698
  };
802
699
  }
803
- function armTimeoutKill(child, timeoutMs, onTimeout, resolve2) {
700
+ function armTimeoutKill(child, timeoutMs, onTimeout, resolve) {
804
701
  const timer = setTimeout(() => {
805
702
  gate.fire(() => {
806
703
  try {
807
704
  process.kill(-(child.pid ?? 0), "SIGKILL");
808
705
  } catch {
809
706
  }
810
- resolve2(onTimeout());
707
+ resolve(onTimeout());
811
708
  });
812
709
  }, timeoutMs);
813
710
  const gate = createSettleGate(timer);
814
711
  return gate;
815
712
  }
816
- function attachChildSettlers(child, gate, onClose, onError, resolve2) {
713
+ function attachChildSettlers(child, gate, onClose, onError, resolve) {
817
714
  child.on("close", (code) => {
818
- gate.fire(() => resolve2(onClose(code)));
715
+ gate.fire(() => resolve(onClose(code)));
819
716
  });
820
717
  child.on("error", (err) => {
821
- gate.fire(() => resolve2(onError(err)));
718
+ gate.fire(() => resolve(onError(err)));
719
+ });
720
+ }
721
+
722
+ // src/internal/git-exec.ts
723
+ function formatGitResult(result, timeoutMs) {
724
+ if (result.kind === "timeout") {
725
+ return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
726
+ }
727
+ if (result.kind === "error") {
728
+ return JSON.stringify({ ok: false, error: "git_failed", stderr: result.stderr });
729
+ }
730
+ return JSON.stringify({ ok: true, diff: result.stdout, truncated: result.truncated });
731
+ }
732
+ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
733
+ return new Promise((resolve) => {
734
+ const child = child_process.spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
735
+ const stdoutChunks = [];
736
+ const stderrChunks = [];
737
+ let stdoutBytes = 0;
738
+ let truncated = false;
739
+ const gate = armTimeoutKill(
740
+ child,
741
+ timeoutMs,
742
+ () => ({ kind: "timeout" }),
743
+ resolve
744
+ );
745
+ child.stdout.on("data", (chunk) => {
746
+ if (gate.settled()) return;
747
+ if (stdoutBytes >= maxStdoutBytes) {
748
+ truncated = true;
749
+ return;
750
+ }
751
+ const remaining = maxStdoutBytes - stdoutBytes;
752
+ if (chunk.length > remaining) {
753
+ stdoutChunks.push(chunk.subarray(0, remaining));
754
+ stdoutBytes = maxStdoutBytes;
755
+ truncated = true;
756
+ } else {
757
+ stdoutChunks.push(chunk);
758
+ stdoutBytes += chunk.length;
759
+ }
760
+ });
761
+ child.stderr.on("data", (chunk) => {
762
+ stderrChunks.push(chunk);
763
+ });
764
+ attachChildSettlers(
765
+ child,
766
+ gate,
767
+ (code) => {
768
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
769
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
770
+ return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
771
+ },
772
+ (err) => ({ kind: "error", stderr: err.message }),
773
+ resolve
774
+ );
775
+ });
776
+ }
777
+
778
+ // src/path-scope.ts
779
+ function checkPathScope(path, projectRoot) {
780
+ if (path === void 0 || path === "") return null;
781
+ try {
782
+ const abs = pathSafety.safePathJoin(projectRoot, path);
783
+ pathSafety.assertNoSymlinkEscape(abs, projectRoot);
784
+ return null;
785
+ } catch (err) {
786
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
787
+ return JSON.stringify({ ok: false, error: "path_traversal", path });
788
+ }
789
+ throw err;
790
+ }
791
+ }
792
+ var SEGMENTOS_SENSIVEIS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
793
+ function ehProibidoEmQualquerProfundidade(path) {
794
+ const segs = path.replace(/\\/g, "/").split("/").filter(Boolean);
795
+ return segs.some((s) => {
796
+ if (s === ".env.example") return false;
797
+ return SEGMENTOS_SENSIVEIS.has(s) || /^\.env\./.test(s);
822
798
  });
823
799
  }
824
800
 
@@ -875,58 +851,25 @@ function buildDiffArgs(cached, path) {
875
851
  if (path !== void 0 && path !== "") args.push("--", path);
876
852
  return args;
877
853
  }
878
- function formatGitResult(result, timeoutMs) {
879
- if (result.kind === "timeout") {
880
- return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
881
- }
882
- if (result.kind === "error") {
883
- return JSON.stringify({ ok: false, error: "git_failed", stderr: result.stderr });
884
- }
885
- return JSON.stringify({ ok: true, diff: result.stdout, truncated: result.truncated });
886
- }
887
- function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
888
- return new Promise((resolve2) => {
889
- const child = child_process.spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
890
- const stdoutChunks = [];
891
- const stderrChunks = [];
892
- let stdoutBytes = 0;
893
- let truncated = false;
894
- const gate = armTimeoutKill(
895
- child,
896
- timeoutMs,
897
- () => ({ kind: "timeout" }),
898
- resolve2
899
- );
900
- child.stdout.on("data", (chunk) => {
901
- if (gate.settled()) return;
902
- if (stdoutBytes >= maxStdoutBytes) {
903
- truncated = true;
904
- return;
905
- }
906
- const remaining = maxStdoutBytes - stdoutBytes;
907
- if (chunk.length > remaining) {
908
- stdoutChunks.push(chunk.subarray(0, remaining));
909
- stdoutBytes = maxStdoutBytes;
910
- truncated = true;
911
- } else {
912
- stdoutChunks.push(chunk);
913
- stdoutBytes += chunk.length;
854
+ function createGitStatusTool(opts) {
855
+ const { projectRoot, timeoutMs = 3e4, maxStdoutBytes = 5 * 1024 * 1024 } = opts;
856
+ return sdk.Tool.create({
857
+ name: opts.name ?? "git_status",
858
+ description: opts.description ?? "Show the working-tree status in porcelain format: staged, unstaged and untracked paths, one per line with a two-character status code. Use before committing, or to see what changed without reading the full diff. Optional 'path' scopes the report to a subdirectory.",
859
+ inputSchema: zod.z.object({
860
+ path: zod.z.string().optional().describe("Optional project-relative path to scope the status report.")
861
+ }),
862
+ handler: async ({ path: path$1 }) => {
863
+ if (!fs.existsSync(path.join(projectRoot, ".git"))) {
864
+ return JSON.stringify({ ok: false, error: "not_a_repo" });
914
865
  }
915
- });
916
- child.stderr.on("data", (chunk) => {
917
- stderrChunks.push(chunk);
918
- });
919
- attachChildSettlers(
920
- child,
921
- gate,
922
- (code) => {
923
- const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
924
- const stderr = Buffer.concat(stderrChunks).toString("utf-8");
925
- return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
926
- },
927
- (err) => ({ kind: "error", stderr: err.message }),
928
- resolve2
929
- );
866
+ const scopeCheck = checkPathScope(path$1, projectRoot);
867
+ if (scopeCheck !== null) return scopeCheck;
868
+ const args = ["status", "--porcelain"];
869
+ if (path$1 !== void 0 && path$1 !== "") args.push("--", path$1);
870
+ const result = await runGitProcess(projectRoot, args, timeoutMs, maxStdoutBytes);
871
+ return formatGitResult(result, timeoutMs);
872
+ }
930
873
  });
931
874
  }
932
875
  var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
@@ -951,7 +894,7 @@ function createGlobTool(opts) {
951
894
  await walkDirBackend(backend, searchRel, searchRel, regex, found, 0);
952
895
  return JSON.stringify({ ok: true, files: found.sort(), count: found.length });
953
896
  }
954
- const searchRoot = cwd ? safePathJoin(projectRoot, cwd) : projectRoot;
897
+ const searchRoot = cwd ? pathSafety.safePathJoin(projectRoot, cwd) : projectRoot;
955
898
  const files = [];
956
899
  await walkDir(searchRoot, searchRoot, regex, files);
957
900
  const relativePaths = files.map((f) => path.relative(projectRoot, f)).sort();
@@ -962,10 +905,10 @@ function createGlobTool(opts) {
962
905
  function globScopeError(cwd, projectRoot) {
963
906
  if (!cwd) return null;
964
907
  try {
965
- assertNoSymlinkEscape(safePathJoin(projectRoot, cwd), projectRoot);
908
+ pathSafety.assertNoSymlinkEscape(pathSafety.safePathJoin(projectRoot, cwd), projectRoot);
966
909
  return null;
967
910
  } catch (err) {
968
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
911
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
969
912
  return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
970
913
  }
971
914
  throw err;
@@ -1598,15 +1541,17 @@ var DEFAULT_MAX_ENTRIES = 500;
1598
1541
  function createListDirTool(opts) {
1599
1542
  const { projectRoot, max = DEFAULT_MAX_ENTRIES, filesystem: filesystem$1 } = opts;
1600
1543
  return sdk.Tool.create({
1601
- name: "list_dir",
1602
- description: `Return the direct entries of a project-relative directory. Refuses paths outside the project root or in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files). Caps at ${String(max)} entries by default; result carries truncated + totalCount.`,
1544
+ name: opts.name ?? "list_dir",
1545
+ description: opts.description ?? `Return the direct entries of a project-relative directory. Refuses paths outside the project root or in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files). Caps at ${String(max)} entries by default; result carries truncated + totalCount.`,
1603
1546
  inputSchema: zod.z.object({
1604
1547
  path: zod.z.string().min(1).describe("Project-relative directory path. Use '.' for root.")
1605
1548
  }),
1606
1549
  handler: async ({ path }, ctx) => {
1607
1550
  const relative3 = path === "" || path === "." ? "." : path;
1608
- if (relative3 !== "." && isForbiddenPath(relative3)) {
1609
- return JSON.stringify({ ok: false, error: "forbidden_path", path });
1551
+ const veredito = decidirEscopo(relative3, path, opts.allowAbsolute === true);
1552
+ if (veredito.erro !== void 0) return veredito.erro;
1553
+ if (veredito.raizAbsoluta !== void 0) {
1554
+ return listViaLocalFs(veredito.raizAbsoluta, ".", path, max);
1610
1555
  }
1611
1556
  if (filesystem$1) {
1612
1557
  const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
@@ -1616,6 +1561,16 @@ function createListDirTool(opts) {
1616
1561
  }
1617
1562
  });
1618
1563
  }
1564
+ function decidirEscopo(relative3, original, allowAbsolute) {
1565
+ const recusa = (error) => ({
1566
+ erro: JSON.stringify({ ok: false, error, path: original })
1567
+ });
1568
+ if (relative3 !== "." && pathSafety.isForbiddenPath(relative3)) return recusa("forbidden_path");
1569
+ if (!path.isAbsolute(relative3)) return {};
1570
+ if (!allowAbsolute) return recusa("path_traversal");
1571
+ if (ehProibidoEmQualquerProfundidade(relative3)) return recusa("forbidden_path");
1572
+ return { raizAbsoluta: relative3 };
1573
+ }
1619
1574
  async function listViaLocalFs(projectRoot, relative3, originalPath, max) {
1620
1575
  const boundary = resolveDirBoundary(relative3, projectRoot, originalPath);
1621
1576
  if ("error" in boundary) return boundary.error;
@@ -1653,11 +1608,11 @@ async function listViaBackend(backend, relative3, originalPath, max) {
1653
1608
  }
1654
1609
  function resolveDirBoundary(relative3, projectRoot, originalPath) {
1655
1610
  try {
1656
- const absolutePath = relative3 === "." ? projectRoot : safePathJoin(projectRoot, relative3);
1657
- assertNoSymlinkEscape(absolutePath, projectRoot);
1611
+ const absolutePath = relative3 === "." ? projectRoot : pathSafety.safePathJoin(projectRoot, relative3);
1612
+ pathSafety.assertNoSymlinkEscape(absolutePath, projectRoot);
1658
1613
  return { absolutePath };
1659
1614
  } catch (err) {
1660
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
1615
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
1661
1616
  return { error: JSON.stringify({ ok: false, error: "path_traversal", path: originalPath }) };
1662
1617
  }
1663
1618
  throw err;
@@ -1776,6 +1731,11 @@ function createPlanModeTool(options) {
1776
1731
  }
1777
1732
 
1778
1733
  // src/question.ts
1734
+ function askerDoContexto(context) {
1735
+ if (typeof context !== "object" || context === null) return void 0;
1736
+ const candidato = context.askUser;
1737
+ return typeof candidato === "function" ? candidato : void 0;
1738
+ }
1779
1739
  function createQuestionTool(opts) {
1780
1740
  const timeoutMs = opts.timeoutMs ?? 3e5;
1781
1741
  return {
@@ -1788,12 +1748,20 @@ function createQuestionTool(opts) {
1788
1748
  },
1789
1749
  required: ["question"]
1790
1750
  },
1791
- handler: async (input) => {
1751
+ handler: async (input, ctx) => {
1752
+ const askUser = askerDoContexto(ctx?.context) ?? opts.askUser;
1753
+ if (askUser === void 0) {
1754
+ return JSON.stringify({
1755
+ ok: false,
1756
+ error: "no_asker",
1757
+ message: "No asker available: pass `askUser` to createQuestionTool, or provide `context.askUser` via SendOptions.context."
1758
+ });
1759
+ }
1792
1760
  const timeout = new Promise((_, reject) => {
1793
1761
  setTimeout(() => reject(new Error("timeout")), timeoutMs);
1794
1762
  });
1795
1763
  try {
1796
- const answer = await Promise.race([opts.askUser(input.question), timeout]);
1764
+ const answer = await Promise.race([askUser(input.question), timeout]);
1797
1765
  return JSON.stringify({ ok: true, answer });
1798
1766
  } catch (err) {
1799
1767
  if (err instanceof Error && err.message === "timeout") {
@@ -1819,7 +1787,7 @@ function isForbiddenAtAnyDepth(path) {
1819
1787
  });
1820
1788
  }
1821
1789
  function forbiddenReadError(path$1, allowAbsolute) {
1822
- if (isForbiddenPath(path$1)) {
1790
+ if (pathSafety.isForbiddenPath(path$1)) {
1823
1791
  return JSON.stringify({ ok: false, error: "forbidden_path", path: path$1 });
1824
1792
  }
1825
1793
  if (allowAbsolute && path.isAbsolute(path$1) && isForbiddenAtAnyDepth(path$1)) {
@@ -1906,11 +1874,11 @@ function resolveBoundary(path$1, projectRoot, allowAbsolute) {
1906
1874
  return { absolutePath: path$1 };
1907
1875
  }
1908
1876
  try {
1909
- const absolutePath = safePathJoin(projectRoot, path$1);
1910
- assertNoSymlinkEscape(absolutePath, projectRoot);
1877
+ const absolutePath = pathSafety.safePathJoin(projectRoot, path$1);
1878
+ pathSafety.assertNoSymlinkEscape(absolutePath, projectRoot);
1911
1879
  return { absolutePath };
1912
1880
  } catch (err) {
1913
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
1881
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
1914
1882
  return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
1915
1883
  }
1916
1884
  throw err;
@@ -2032,7 +2000,7 @@ function createRunVitestTool(opts) {
2032
2000
  });
2033
2001
  }
2034
2002
  function validateVitestScope(path, projectRoot) {
2035
- if (path !== void 0 && path !== "" && isForbiddenPath(path)) {
2003
+ if (path !== void 0 && path !== "" && pathSafety.isForbiddenPath(path)) {
2036
2004
  return JSON.stringify({ ok: false, error: "forbidden_path", path });
2037
2005
  }
2038
2006
  return checkPathScope(path, projectRoot);
@@ -2078,7 +2046,7 @@ function appendCapped(chunks, chunk, current, cap) {
2078
2046
  return current + chunk.length;
2079
2047
  }
2080
2048
  function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
2081
- return new Promise((resolve2) => {
2049
+ return new Promise((resolve) => {
2082
2050
  const child = child_process.spawn(command, args, {
2083
2051
  cwd,
2084
2052
  detached: true,
@@ -2091,7 +2059,7 @@ function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
2091
2059
  child,
2092
2060
  timeoutMs,
2093
2061
  () => ({ kind: "timeout" }),
2094
- resolve2
2062
+ resolve
2095
2063
  );
2096
2064
  child.stdout.on("data", (chunk) => {
2097
2065
  if (gate.settled()) return;
@@ -2110,7 +2078,7 @@ function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
2110
2078
  exitCode: code ?? 0
2111
2079
  }),
2112
2080
  (err) => ({ kind: "spawn_error", message: err.message }),
2113
- resolve2
2081
+ resolve
2114
2082
  );
2115
2083
  });
2116
2084
  }
@@ -2131,8 +2099,8 @@ function createSearchTextTool(opts) {
2131
2099
  const queryKind = regex ? "a JavaScript REGULAR EXPRESSION" : "LITERAL, CASE-SENSITIVE text";
2132
2100
  const queryMatch = regex ? "matched as a regex" : "matched as a substring, not a regex";
2133
2101
  return sdk.Tool.create({
2134
- name: "search_text",
2135
- description: `Search file CONTENTS for ${queryKind} across the project tree (the query is ${queryMatch}). Use search_text when you know the content; use glob_files when you know the filename shape; use read_file when you know the exact path. Skips sensitive dirs (.env/.git/node_modules/.theo), binary files, and files over 1 MB; 'path' scopes the search to a subdirectory. Returns up to ${String(maxMatches)} matches as { file, line, preview } \u2014 cite locations to the user as file:line. Returns { ok, matches } or { ok: false, error }.`,
2102
+ name: opts.name ?? "search_text",
2103
+ description: opts.description ?? `Search file CONTENTS for ${queryKind} across the project tree (the query is ${queryMatch}). Use search_text when you know the content; use glob_files when you know the filename shape; use read_file when you know the exact path. Skips sensitive dirs (.env/.git/node_modules/.theo), binary files, and files over 1 MB; 'path' scopes the search to a subdirectory. Returns up to ${String(maxMatches)} matches as { file, line, preview } \u2014 cite locations to the user as file:line. Returns { ok, matches } or { ok: false, error }.`,
2136
2104
  inputSchema: zod.z.object({
2137
2105
  query: regex ? zod.z.string().min(1).describe("A JavaScript regular expression, e.g. 'function\\\\s+main'.") : zod.z.string().min(1).describe("Literal text to search for. Case-sensitive."),
2138
2106
  path: zod.z.string().optional().describe(
@@ -2191,11 +2159,11 @@ function resolveSearchScope(path$1, projectRoot, allowAbsolute) {
2191
2159
  return { scopeAbs: scopeRel };
2192
2160
  }
2193
2161
  try {
2194
- const scopeAbs = scopeRel === "." ? projectRoot : safePathJoin(projectRoot, scopeRel);
2195
- assertNoSymlinkEscape(scopeAbs, projectRoot);
2162
+ const scopeAbs = scopeRel === "." ? projectRoot : pathSafety.safePathJoin(projectRoot, scopeRel);
2163
+ pathSafety.assertNoSymlinkEscape(scopeAbs, projectRoot);
2196
2164
  return { scopeAbs };
2197
2165
  } catch (err) {
2198
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
2166
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
2199
2167
  return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
2200
2168
  }
2201
2169
  throw err;
@@ -2204,7 +2172,7 @@ function resolveSearchScope(path$1, projectRoot, allowAbsolute) {
2204
2172
  async function handleEntry(entry, absDir, state) {
2205
2173
  const entryAbs = path.join(absDir, entry.name);
2206
2174
  const entryRel = path.relative(state.projectRoot, entryAbs);
2207
- if (isForbiddenPath(entryRel)) return;
2175
+ if (pathSafety.isForbiddenPath(entryRel)) return;
2208
2176
  if (entry.isDirectory()) {
2209
2177
  await walk(entryAbs, state);
2210
2178
  return;
@@ -2270,10 +2238,10 @@ function resolveScopeRel(path$1, projectRoot, allowAbsolute) {
2270
2238
  if (scopeRel === "") return { rel: "" };
2271
2239
  if (allowAbsolute && path.isAbsolute(scopeRel)) return { rel: scopeRel };
2272
2240
  try {
2273
- assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
2241
+ pathSafety.assertNoSymlinkEscape(pathSafety.safePathJoin(projectRoot, scopeRel), projectRoot);
2274
2242
  return { rel: scopeRel };
2275
2243
  } catch (err) {
2276
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
2244
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
2277
2245
  return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
2278
2246
  }
2279
2247
  throw err;
@@ -2295,7 +2263,7 @@ async function walkBackend(backend, dirRel, state, depth) {
2295
2263
  }
2296
2264
  async function handleBackendEntry(backend, dirRel, name, state, depth) {
2297
2265
  const entryRel = dirRel === "" ? name : `${dirRel}/${name}`;
2298
- if (isForbiddenPath(entryRel)) return;
2266
+ if (pathSafety.isForbiddenPath(entryRel)) return;
2299
2267
  let st;
2300
2268
  try {
2301
2269
  st = await backend.stat(entryRel);
@@ -2340,8 +2308,8 @@ function createShellTool(opts) {
2340
2308
  sandbox
2341
2309
  } = opts;
2342
2310
  return sdk.Tool.create({
2343
- name: "shell_exec",
2344
- description: "Execute a shell command in the project directory. Use this for terminal operations \u2014 running tests, git, package managers, build tools. Do NOT use it for file operations (reading, writing, editing, finding files): prefer the specialized read_file/write_file/edit_file/glob_files/search_text tools, which are path-checked and safer. Only commit, push, or change git state when the user explicitly asks. timeout_ms defaults to 30000 (max 300000); stdout/stderr are capped (~5 MB). Returns { ok, stdout, stderr, exit_code } or { ok: false, error }.",
2311
+ name: opts.name ?? "shell_exec",
2312
+ description: opts.description ?? "Execute a shell command in the project directory. Use this for terminal operations \u2014 running tests, git, package managers, build tools. Do NOT use it for file operations (reading, writing, editing, finding files): prefer the specialized read_file/write_file/edit_file/glob_files/search_text tools, which are path-checked and safer. Only commit, push, or change git state when the user explicitly asks. timeout_ms defaults to 30000 (max 300000); stdout/stderr are capped (~5 MB). Returns { ok, stdout, stderr, exit_code } or { ok: false, error }.",
2345
2313
  inputSchema: zod.z.object({
2346
2314
  command: zod.z.string().min(1).describe("Shell command to execute."),
2347
2315
  timeout_ms: zod.z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
@@ -2361,7 +2329,7 @@ function createShellTool(opts) {
2361
2329
  });
2362
2330
  }
2363
2331
  function runShell(cwd, command, timeoutMs) {
2364
- return new Promise((resolve2) => {
2332
+ return new Promise((resolve) => {
2365
2333
  const child = child_process.spawn("/bin/sh", ["-c", command], {
2366
2334
  cwd,
2367
2335
  detached: true,
@@ -2375,7 +2343,7 @@ function runShell(cwd, command, timeoutMs) {
2375
2343
  child,
2376
2344
  timeoutMs,
2377
2345
  () => ({ kind: "timeout" }),
2378
- (result) => resolve2(formatResult(result, timeoutMs))
2346
+ (result) => resolve(formatResult(result, timeoutMs))
2379
2347
  );
2380
2348
  child.stdout.on("data", (chunk) => {
2381
2349
  if (gate.settled()) return;
@@ -2413,7 +2381,7 @@ function runShell(cwd, command, timeoutMs) {
2413
2381
  exitCode: code
2414
2382
  }),
2415
2383
  (err) => ({ kind: "error", message: err.message }),
2416
- (result) => resolve2(formatResult(result, timeoutMs))
2384
+ (result) => resolve(formatResult(result, timeoutMs))
2417
2385
  );
2418
2386
  });
2419
2387
  }
@@ -2795,7 +2763,7 @@ function createWriteFileTool(opts) {
2795
2763
  content: zod.z.string().describe("UTF-8 content to write.")
2796
2764
  }),
2797
2765
  handler: async ({ path, content }, ctx) => {
2798
- if (isForbiddenPath(path)) {
2766
+ if (pathSafety.isForbiddenPath(path)) {
2799
2767
  return JSON.stringify({ ok: false, error: "forbidden_path", path });
2800
2768
  }
2801
2769
  if (filesystem$1) {
@@ -2817,10 +2785,10 @@ function readBeforeWriteError(guard, path, currentMtimeMs) {
2817
2785
  async function writeViaLocalFs(projectRoot, path$1, content, guard) {
2818
2786
  let absolutePath;
2819
2787
  try {
2820
- absolutePath = safePathJoin(projectRoot, path$1);
2821
- assertNoSymlinkEscape(absolutePath, projectRoot);
2788
+ absolutePath = pathSafety.safePathJoin(projectRoot, path$1);
2789
+ pathSafety.assertNoSymlinkEscape(absolutePath, projectRoot);
2822
2790
  } catch (err) {
2823
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
2791
+ if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
2824
2792
  return JSON.stringify({ ok: false, error: "path_traversal", path: path$1 });
2825
2793
  }
2826
2794
  throw err;
@@ -2924,6 +2892,7 @@ exports.createCurrentTimeTool = createCurrentTimeTool;
2924
2892
  exports.createEditFileTool = createEditFileTool;
2925
2893
  exports.createGenericHttpSearchAdapter = createGenericHttpSearchAdapter;
2926
2894
  exports.createGitDiffTool = createGitDiffTool;
2895
+ exports.createGitStatusTool = createGitStatusTool;
2927
2896
  exports.createGlobTool = createGlobTool;
2928
2897
  exports.createInteractiveShellTool = createInteractiveShellTool;
2929
2898
  exports.createListDirTool = createListDirTool;