@theokit/sdk-tools 0.21.0 → 0.22.1

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.js CHANGED
@@ -5,9 +5,9 @@ import { z } from 'zod';
5
5
  import { safePathJoin, assertNoSymlinkEscape, PathTraversalError, ForbiddenPathError, isForbiddenPath, safeFilenameForId } from '@theokit/sdk/path-safety';
6
6
  import { replaceFileAtomic } from '@theokit/sdk/persistence';
7
7
  import { resolveFilesystem, FileNotFoundError, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError } from '@theokit/sdk/filesystem';
8
- import { spawn } from 'child_process';
9
8
  import { existsSync, statSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
10
9
  import { resolveSandbox } from '@theokit/sdk/sandbox';
10
+ import { spawn } from 'child_process';
11
11
  import { resolveInteractive, InteractiveUnavailableError, NoSuchSessionError } from '@theokit/sdk/interactive';
12
12
  import { lookup } from 'dns/promises';
13
13
  import { isIP } from 'net';
@@ -609,8 +609,8 @@ function editScopeError(path, projectRoot) {
609
609
  function createEditFileTool(opts) {
610
610
  const { projectRoot, filesystem } = opts;
611
611
  return Tool.create({
612
- name: "edit_file",
613
- 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 }.",
612
+ name: opts.name ?? "edit_file",
613
+ 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 }.",
614
614
  inputSchema: z.object({
615
615
  path: z.string().min(1).describe("Project-relative file path."),
616
616
  old_string: z.string().min(1).describe("String to find in the file."),
@@ -682,21 +682,6 @@ function formatError(message, code) {
682
682
  return `> **Error:** ${prefix}${message}`;
683
683
  }
684
684
 
685
- // src/path-scope.ts
686
- function checkPathScope(path, projectRoot) {
687
- if (path === void 0 || path === "") return null;
688
- try {
689
- const abs = safePathJoin(projectRoot, path);
690
- assertNoSymlinkEscape(abs, projectRoot);
691
- return null;
692
- } catch (err) {
693
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
694
- return JSON.stringify({ ok: false, error: "path_traversal", path });
695
- }
696
- throw err;
697
- }
698
- }
699
-
700
685
  // src/subprocess.ts
701
686
  function createSettleGate(timer) {
702
687
  let done = false;
@@ -732,6 +717,85 @@ function attachChildSettlers(child, gate, onClose, onError, resolve) {
732
717
  });
733
718
  }
734
719
 
720
+ // src/internal/git-exec.ts
721
+ function formatGitResult(result, timeoutMs) {
722
+ if (result.kind === "timeout") {
723
+ return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
724
+ }
725
+ if (result.kind === "error") {
726
+ return JSON.stringify({ ok: false, error: "git_failed", stderr: result.stderr });
727
+ }
728
+ return JSON.stringify({ ok: true, diff: result.stdout, truncated: result.truncated });
729
+ }
730
+ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
731
+ return new Promise((resolve) => {
732
+ const child = spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
733
+ const stdoutChunks = [];
734
+ const stderrChunks = [];
735
+ let stdoutBytes = 0;
736
+ let truncated = false;
737
+ const gate = armTimeoutKill(
738
+ child,
739
+ timeoutMs,
740
+ () => ({ kind: "timeout" }),
741
+ resolve
742
+ );
743
+ child.stdout.on("data", (chunk) => {
744
+ if (gate.settled()) return;
745
+ if (stdoutBytes >= maxStdoutBytes) {
746
+ truncated = true;
747
+ return;
748
+ }
749
+ const remaining = maxStdoutBytes - stdoutBytes;
750
+ if (chunk.length > remaining) {
751
+ stdoutChunks.push(chunk.subarray(0, remaining));
752
+ stdoutBytes = maxStdoutBytes;
753
+ truncated = true;
754
+ } else {
755
+ stdoutChunks.push(chunk);
756
+ stdoutBytes += chunk.length;
757
+ }
758
+ });
759
+ child.stderr.on("data", (chunk) => {
760
+ stderrChunks.push(chunk);
761
+ });
762
+ attachChildSettlers(
763
+ child,
764
+ gate,
765
+ (code) => {
766
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
767
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
768
+ return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
769
+ },
770
+ (err) => ({ kind: "error", stderr: err.message }),
771
+ resolve
772
+ );
773
+ });
774
+ }
775
+
776
+ // src/path-scope.ts
777
+ function checkPathScope(path, projectRoot) {
778
+ if (path === void 0 || path === "") return null;
779
+ try {
780
+ const abs = safePathJoin(projectRoot, path);
781
+ assertNoSymlinkEscape(abs, projectRoot);
782
+ return null;
783
+ } catch (err) {
784
+ if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
785
+ return JSON.stringify({ ok: false, error: "path_traversal", path });
786
+ }
787
+ throw err;
788
+ }
789
+ }
790
+ var SEGMENTOS_SENSIVEIS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
791
+ function ehProibidoEmQualquerProfundidade(path) {
792
+ const segs = path.replace(/\\/g, "/").split("/").filter(Boolean);
793
+ return segs.some((s) => {
794
+ if (s === ".env.example") return false;
795
+ return SEGMENTOS_SENSIVEIS.has(s) || /^\.env\./.test(s);
796
+ });
797
+ }
798
+
735
799
  // src/git-diff.ts
736
800
  var DEFAULT_TIMEOUT_MS = 3e4;
737
801
  var DEFAULT_MAX_STDOUT_BYTES = 5 * 1024 * 1024;
@@ -785,58 +849,25 @@ function buildDiffArgs(cached, path) {
785
849
  if (path !== void 0 && path !== "") args.push("--", path);
786
850
  return args;
787
851
  }
788
- function formatGitResult(result, timeoutMs) {
789
- if (result.kind === "timeout") {
790
- return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
791
- }
792
- if (result.kind === "error") {
793
- return JSON.stringify({ ok: false, error: "git_failed", stderr: result.stderr });
794
- }
795
- return JSON.stringify({ ok: true, diff: result.stdout, truncated: result.truncated });
796
- }
797
- function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
798
- return new Promise((resolve) => {
799
- const child = spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
800
- const stdoutChunks = [];
801
- const stderrChunks = [];
802
- let stdoutBytes = 0;
803
- let truncated = false;
804
- const gate = armTimeoutKill(
805
- child,
806
- timeoutMs,
807
- () => ({ kind: "timeout" }),
808
- resolve
809
- );
810
- child.stdout.on("data", (chunk) => {
811
- if (gate.settled()) return;
812
- if (stdoutBytes >= maxStdoutBytes) {
813
- truncated = true;
814
- return;
815
- }
816
- const remaining = maxStdoutBytes - stdoutBytes;
817
- if (chunk.length > remaining) {
818
- stdoutChunks.push(chunk.subarray(0, remaining));
819
- stdoutBytes = maxStdoutBytes;
820
- truncated = true;
821
- } else {
822
- stdoutChunks.push(chunk);
823
- stdoutBytes += chunk.length;
852
+ function createGitStatusTool(opts) {
853
+ const { projectRoot, timeoutMs = 3e4, maxStdoutBytes = 5 * 1024 * 1024 } = opts;
854
+ return Tool.create({
855
+ name: opts.name ?? "git_status",
856
+ 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.",
857
+ inputSchema: z.object({
858
+ path: z.string().optional().describe("Optional project-relative path to scope the status report.")
859
+ }),
860
+ handler: async ({ path }) => {
861
+ if (!existsSync(join(projectRoot, ".git"))) {
862
+ return JSON.stringify({ ok: false, error: "not_a_repo" });
824
863
  }
825
- });
826
- child.stderr.on("data", (chunk) => {
827
- stderrChunks.push(chunk);
828
- });
829
- attachChildSettlers(
830
- child,
831
- gate,
832
- (code) => {
833
- const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
834
- const stderr = Buffer.concat(stderrChunks).toString("utf-8");
835
- return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
836
- },
837
- (err) => ({ kind: "error", stderr: err.message }),
838
- resolve
839
- );
864
+ const scopeCheck = checkPathScope(path, projectRoot);
865
+ if (scopeCheck !== null) return scopeCheck;
866
+ const args = ["status", "--porcelain"];
867
+ if (path !== void 0 && path !== "") args.push("--", path);
868
+ const result = await runGitProcess(projectRoot, args, timeoutMs, maxStdoutBytes);
869
+ return formatGitResult(result, timeoutMs);
870
+ }
840
871
  });
841
872
  }
842
873
  var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
@@ -1508,15 +1539,17 @@ var DEFAULT_MAX_ENTRIES = 500;
1508
1539
  function createListDirTool(opts) {
1509
1540
  const { projectRoot, max = DEFAULT_MAX_ENTRIES, filesystem } = opts;
1510
1541
  return Tool.create({
1511
- name: "list_dir",
1512
- 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.`,
1542
+ name: opts.name ?? "list_dir",
1543
+ 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.`,
1513
1544
  inputSchema: z.object({
1514
1545
  path: z.string().min(1).describe("Project-relative directory path. Use '.' for root.")
1515
1546
  }),
1516
1547
  handler: async ({ path }, ctx) => {
1517
1548
  const relative3 = path === "" || path === "." ? "." : path;
1518
- if (relative3 !== "." && isForbiddenPath(relative3)) {
1519
- return JSON.stringify({ ok: false, error: "forbidden_path", path });
1549
+ const veredito = decidirEscopo(relative3, path, opts.allowAbsolute === true);
1550
+ if (veredito.erro !== void 0) return veredito.erro;
1551
+ if (veredito.raizAbsoluta !== void 0) {
1552
+ return listViaLocalFs(veredito.raizAbsoluta, ".", path, max);
1520
1553
  }
1521
1554
  if (filesystem) {
1522
1555
  const backend = await resolveFilesystem(filesystem, ctx ?? {});
@@ -1526,6 +1559,16 @@ function createListDirTool(opts) {
1526
1559
  }
1527
1560
  });
1528
1561
  }
1562
+ function decidirEscopo(relative3, original, allowAbsolute) {
1563
+ const recusa = (error) => ({
1564
+ erro: JSON.stringify({ ok: false, error, path: original })
1565
+ });
1566
+ if (relative3 !== "." && isForbiddenPath(relative3)) return recusa("forbidden_path");
1567
+ if (!isAbsolute(relative3)) return {};
1568
+ if (!allowAbsolute) return recusa("path_traversal");
1569
+ if (ehProibidoEmQualquerProfundidade(relative3)) return recusa("forbidden_path");
1570
+ return { raizAbsoluta: relative3 };
1571
+ }
1529
1572
  async function listViaLocalFs(projectRoot, relative3, originalPath, max) {
1530
1573
  const boundary = resolveDirBoundary(relative3, projectRoot, originalPath);
1531
1574
  if ("error" in boundary) return boundary.error;
@@ -1686,6 +1729,11 @@ function createPlanModeTool(options) {
1686
1729
  }
1687
1730
 
1688
1731
  // src/question.ts
1732
+ function askerDoContexto(context) {
1733
+ if (typeof context !== "object" || context === null) return void 0;
1734
+ const candidato = context.askUser;
1735
+ return typeof candidato === "function" ? candidato : void 0;
1736
+ }
1689
1737
  function createQuestionTool(opts) {
1690
1738
  const timeoutMs = opts.timeoutMs ?? 3e5;
1691
1739
  return {
@@ -1698,12 +1746,20 @@ function createQuestionTool(opts) {
1698
1746
  },
1699
1747
  required: ["question"]
1700
1748
  },
1701
- handler: async (input) => {
1749
+ handler: async (input, ctx) => {
1750
+ const askUser = askerDoContexto(ctx?.context) ?? opts.askUser;
1751
+ if (askUser === void 0) {
1752
+ return JSON.stringify({
1753
+ ok: false,
1754
+ error: "no_asker",
1755
+ message: "No asker available: pass `askUser` to createQuestionTool, or provide `context.askUser` via SendOptions.context."
1756
+ });
1757
+ }
1702
1758
  const timeout = new Promise((_, reject) => {
1703
1759
  setTimeout(() => reject(new Error("timeout")), timeoutMs);
1704
1760
  });
1705
1761
  try {
1706
- const answer = await Promise.race([opts.askUser(input.question), timeout]);
1762
+ const answer = await Promise.race([askUser(String(input.question ?? "")), timeout]);
1707
1763
  return JSON.stringify({ ok: true, answer });
1708
1764
  } catch (err) {
1709
1765
  if (err instanceof Error && err.message === "timeout") {
@@ -2041,8 +2097,8 @@ function createSearchTextTool(opts) {
2041
2097
  const queryKind = regex ? "a JavaScript REGULAR EXPRESSION" : "LITERAL, CASE-SENSITIVE text";
2042
2098
  const queryMatch = regex ? "matched as a regex" : "matched as a substring, not a regex";
2043
2099
  return Tool.create({
2044
- name: "search_text",
2045
- 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 }.`,
2100
+ name: opts.name ?? "search_text",
2101
+ 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 }.`,
2046
2102
  inputSchema: z.object({
2047
2103
  query: regex ? z.string().min(1).describe("A JavaScript regular expression, e.g. 'function\\\\s+main'.") : z.string().min(1).describe("Literal text to search for. Case-sensitive."),
2048
2104
  path: z.string().optional().describe(
@@ -2250,8 +2306,8 @@ function createShellTool(opts) {
2250
2306
  sandbox
2251
2307
  } = opts;
2252
2308
  return Tool.create({
2253
- name: "shell_exec",
2254
- 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 }.",
2309
+ name: opts.name ?? "shell_exec",
2310
+ 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 }.",
2255
2311
  inputSchema: z.object({
2256
2312
  command: z.string().min(1).describe("Shell command to execute."),
2257
2313
  timeout_ms: z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
@@ -2817,6 +2873,6 @@ async function isBinaryFile(absolutePath) {
2817
2873
  }
2818
2874
  }
2819
2875
 
2820
- export { CatastrophicCommandError, ContextMatchError, DEFAULT_TOOL_GUIDANCE, ReadTracker, ReasoningTools, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createCurrentTimeTool, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createInteractiveShellTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createUpdatePlanTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, createWriteStdinTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, replaceUnique, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withName, withShellExitGuidance, withToolResultGuidance };
2876
+ export { CatastrophicCommandError, ContextMatchError, DEFAULT_TOOL_GUIDANCE, ReadTracker, ReasoningTools, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createCurrentTimeTool, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGitStatusTool, createGlobTool, createInteractiveShellTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createUpdatePlanTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, createWriteStdinTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, replaceUnique, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withName, withShellExitGuidance, withToolResultGuidance };
2821
2877
  //# sourceMappingURL=index.js.map
2822
2878
  //# sourceMappingURL=index.js.map