@theokit/sdk-tools 0.23.0 → 0.24.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.cjs CHANGED
@@ -227,8 +227,8 @@ ${chunk.oldLines.join("\n")}`
227
227
  function createApplyPatchTool(opts) {
228
228
  const { projectRoot } = opts;
229
229
  return sdk.Tool.create({
230
- name: "apply_patch",
231
- description: "Apply a Codex-style V4A patch. The patch is `*** Begin Patch` \u2026 `*** End Patch` wrapping one or more hunks: `*** Add File: <path>` (then `+`lines), `*** Delete File: <path>`, or `*** Update File: <path>` (optional `*** Move to: <path>`) with `@@`-anchored `+` (add) / `-` (remove) / ` ` (context) lines. Read a file first so your context/removed lines match. Applied atomically \u2014 a mismatch anywhere aborts the whole patch with zero writes; each path is security-checked. Returns { ok, files_patched } or { ok: false, error }.",
230
+ name: opts.name ?? "apply_patch",
231
+ description: opts.description ?? "Apply a Codex-style V4A patch. The patch is `*** Begin Patch` \u2026 `*** End Patch` wrapping one or more hunks: `*** Add File: <path>` (then `+`lines), `*** Delete File: <path>`, or `*** Update File: <path>` (optional `*** Move to: <path>`) with `@@`-anchored `+` (add) / `-` (remove) / ` ` (context) lines. Read a file first so your context/removed lines match. Applied atomically \u2014 a mismatch anywhere aborts the whole patch with zero writes; each path is security-checked. Returns { ok, files_patched } or { ok: false, error }.",
232
232
  inputSchema: zod.z.object({
233
233
  patch: zod.z.string().min(1).describe("V4A patch: *** Begin Patch \u2026 *** End Patch.")
234
234
  }),
@@ -429,8 +429,8 @@ function formatInTimezone(now, tz) {
429
429
  function createCurrentTimeTool(opts = {}) {
430
430
  const clock = opts.clock ?? (() => /* @__PURE__ */ new Date());
431
431
  return sdk.Tool.create({
432
- name: "current_time",
433
- description: "Get the current date and time. Returns { current_time, iso, timezone } as a JSON string, where current_time is 'YYYY-MM-DD HH:MM:SS <timezone>' and iso is the ISO-8601 instant. Pass an optional IANA timezone (e.g. 'America/Sao_Paulo', 'Europe/Lisbon'); defaults to UTC. Never state the date or time from memory \u2014 always call this. Returns { ok: false, error: 'invalid_timezone' } for an unknown timezone.",
432
+ name: opts.name ?? "current_time",
433
+ description: opts.description ?? "Get the current date and time. Returns { current_time, iso, timezone } as a JSON string, where current_time is 'YYYY-MM-DD HH:MM:SS <timezone>' and iso is the ISO-8601 instant. Pass an optional IANA timezone (e.g. 'America/Sao_Paulo', 'Europe/Lisbon'); defaults to UTC. Never state the date or time from memory \u2014 always call this. Returns { ok: false, error: 'invalid_timezone' } for an unknown timezone.",
434
434
  inputSchema: zod.z.object({
435
435
  timezone: zod.z.string().optional().describe("IANA timezone, e.g. 'America/Sao_Paulo' or 'Europe/Lisbon'. Defaults to UTC.")
436
436
  }),
@@ -720,6 +720,9 @@ function attachChildSettlers(child, gate, onClose, onError, resolve) {
720
720
  }
721
721
 
722
722
  // src/internal/git-exec.ts
723
+ function shq(arg) {
724
+ return `'${arg.replaceAll("'", `'\\''`)}'`;
725
+ }
723
726
  function formatGitResult(result, timeoutMs) {
724
727
  if (result.kind === "timeout") {
725
728
  return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
@@ -801,9 +804,6 @@ function ehProibidoEmQualquerProfundidade(path) {
801
804
  // src/git-diff.ts
802
805
  var DEFAULT_TIMEOUT_MS = 3e4;
803
806
  var DEFAULT_MAX_STDOUT_BYTES = 5 * 1024 * 1024;
804
- function shq(arg) {
805
- return `'${arg.replace(/'/g, `'\\''`)}'`;
806
- }
807
807
  async function diffViaSandbox(sandbox$1, ctx, cached, path, projectRoot, timeoutMs) {
808
808
  const scopeCheck = checkPathScope(path, projectRoot);
809
809
  if (scopeCheck !== null) return scopeCheck;
@@ -824,8 +824,8 @@ function createGitDiffTool(opts) {
824
824
  sandbox
825
825
  } = opts;
826
826
  return sdk.Tool.create({
827
- name: "git_diff",
828
- description: "Return the unified diff of the project's working tree (or staged changes when cached=true). Scoped to a single file when 'path' is provided. Requires the project to be a git repository. Returns { ok, diff, truncated? } or { ok: false, error }.",
827
+ name: opts.name ?? "git_diff",
828
+ description: opts.description ?? "Return the unified diff of the project's working tree (or staged changes when cached=true). Scoped to a single file when 'path' is provided. Requires the project to be a git repository. Returns { ok, diff, truncated? } or { ok: false, error }.",
829
829
  inputSchema: zod.z.object({
830
830
  path: zod.z.string().optional().describe("Optional project-relative file or dir scope."),
831
831
  cached: zod.z.boolean().optional().describe("If true, show staged changes (git diff --cached). Default false.")
@@ -859,27 +859,44 @@ function createGitStatusTool(opts) {
859
859
  inputSchema: zod.z.object({
860
860
  path: zod.z.string().optional().describe("Optional project-relative path to scope the status report.")
861
861
  }),
862
- handler: async ({ path: path$1 }) => {
862
+ handler: async ({ path: path$1 }, ctx) => {
863
863
  if (!fs.existsSync(path.join(projectRoot, ".git"))) {
864
864
  return JSON.stringify({ ok: false, error: "not_a_repo" });
865
865
  }
866
866
  const scopeCheck = checkPathScope(path$1, projectRoot);
867
867
  if (scopeCheck !== null) return scopeCheck;
868
- const args = ["status", "--porcelain=v1"];
869
- if (opts.includeBranch !== false) args.push("-b");
870
- if (path$1 !== void 0 && path$1 !== "") args.push("--", path$1);
868
+ const args = montarArgs(path$1, opts.includeBranch !== false);
869
+ if (opts.sandbox !== void 0) {
870
+ return statusViaSandbox(opts.sandbox, ctx, args, timeoutMs);
871
+ }
871
872
  const result = await runGitProcess(projectRoot, args, timeoutMs, maxStdoutBytes);
872
873
  return formatGitResult(result, timeoutMs);
873
874
  }
874
875
  });
875
876
  }
877
+ function montarArgs(path, comBranch) {
878
+ const args = ["status", "--porcelain=v1"];
879
+ if (comBranch) args.push("-b");
880
+ if (path !== void 0 && path !== "") args.push("--", path);
881
+ return args;
882
+ }
883
+ async function statusViaSandbox(sandbox$1, ctx, args, timeoutMs) {
884
+ const command = ["git", ...args].map(shq).join(" ");
885
+ const backend = await sandbox.resolveSandbox(sandbox$1, ctx ?? {});
886
+ const r = await backend.execute(command, { timeoutMs });
887
+ if (r.timedOut) return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
888
+ if (r.exitCode !== 0) {
889
+ return /not a git repository/i.test(r.stderr) ? JSON.stringify({ ok: false, error: "not_a_repo" }) : JSON.stringify({ ok: false, error: "git_failed", stderr: r.stderr });
890
+ }
891
+ return JSON.stringify({ ok: true, diff: r.stdout, truncated: false });
892
+ }
876
893
  var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
877
894
  var MAX_BACKEND_WALK_DEPTH = 64;
878
895
  function createGlobTool(opts) {
879
896
  const { projectRoot, filesystem: filesystem$1 } = opts;
880
897
  return sdk.Tool.create({
881
- name: "glob_files",
882
- description: "Find files by glob pattern across the project \u2014 fast at any repo size. Use glob_files when you know the filename SHAPE; use search_text when you know the file CONTENT; use read_file when you know the exact path. The pattern supports * and ** wildcards (e.g. '**/*.ts', 'src/**/*.json'); node_modules/.git/dist/.theo are excluded and results are relative paths. Returns { ok, files } or { ok: false, error }.",
898
+ name: opts.name ?? "glob_files",
899
+ description: opts.description ?? "Find files by glob pattern across the project \u2014 fast at any repo size. Use glob_files when you know the filename SHAPE; use search_text when you know the file CONTENT; use read_file when you know the exact path. The pattern supports * and ** wildcards (e.g. '**/*.ts', 'src/**/*.json'); node_modules/.git/dist/.theo are excluded and results are relative paths. Returns { ok, files } or { ok: false, error }.",
883
900
  inputSchema: zod.z.object({
884
901
  pattern: zod.z.string().min(1).describe("Glob pattern (e.g. '**/*.ts', 'src/**/*.json')."),
885
902
  cwd: zod.z.string().optional().describe("Project-relative subdirectory to search from.")
@@ -998,8 +1015,8 @@ function toErrorJson(err) {
998
1015
  function createInteractiveShellTool(opts) {
999
1016
  const { interactive: interactive$1 } = opts;
1000
1017
  return sdk.Tool.create({
1001
- name: "interactive_shell",
1002
- description: "Start an interactive shell session for a command that PROMPTS for input or is a REPL (python, node, `git rebase -i`, a `read` prompt) \u2014 NOT for one-shot commands (use shell_exec). Returns a session_id; drive it with write_stdin, reading the incremental output each step. Returns { ok, session_id, output } or { ok: false, error }.",
1018
+ name: opts.name ?? "interactive_shell",
1019
+ description: opts.description ?? "Start an interactive shell session for a command that PROMPTS for input or is a REPL (python, node, `git rebase -i`, a `read` prompt) \u2014 NOT for one-shot commands (use shell_exec). Returns a session_id; drive it with write_stdin, reading the incremental output each step. Returns { ok, session_id, output } or { ok: false, error }.",
1003
1020
  inputSchema: zod.z.object({
1004
1021
  command: zod.z.string().min(1).describe("Command to run interactively, e.g. 'python3' or 'bash -i'."),
1005
1022
  yield_time_ms: zod.z.number().int().positive().optional().describe("How long to wait for startup output before returning (clamped by the backend).")
@@ -1703,8 +1720,8 @@ function createPlanModeTool(options) {
1703
1720
  }
1704
1721
  const { artifactStore, artifactId = "plan" } = options;
1705
1722
  return {
1706
- name: "plan_mode",
1707
- description: DESCRIPTION,
1723
+ name: options.name ?? "plan_mode",
1724
+ description: options.description ?? DESCRIPTION,
1708
1725
  inputSchema: planModeSchema(true),
1709
1726
  handler: async (input) => {
1710
1727
  if (input.action === "enter") {
@@ -1762,10 +1779,14 @@ function createQuestionTool(opts) {
1762
1779
  setTimeout(() => reject(new Error("timeout")), timeoutMs);
1763
1780
  });
1764
1781
  try {
1765
- const answer = await Promise.race([askUser(String(input.question ?? "")), timeout]);
1782
+ const answer = await Promise.race([
1783
+ askUser(String(input.question ?? ""), ctx?.threadId),
1784
+ timeout
1785
+ ]);
1766
1786
  return JSON.stringify({ ok: true, answer });
1767
1787
  } catch (err) {
1768
1788
  if (err instanceof Error && err.message === "timeout") {
1789
+ opts.onAbandon?.(ctx?.threadId);
1769
1790
  return JSON.stringify({
1770
1791
  ok: false,
1771
1792
  error: "timeout",
@@ -1810,8 +1831,8 @@ function createReadFileTool(opts) {
1810
1831
  const numbered = lineNumbers === true ? " Returns a cat -n numbered view (`<n>\\t<line>`)." : "";
1811
1832
  const abs = allowAbsolute === true ? " Absolute paths outside the project are honored." : "";
1812
1833
  return sdk.Tool.create({
1813
- name: "read_file",
1814
- description: "Read a text file as UTF-8. ALWAYS read a file before you edit it (edit_file) or overwrite it (write_file), so your old_string / new content matches the real bytes exactly." + numbered + abs + " By default returns the whole file; use the optional offset (1-based first line) + limit to page through a large file, or search_text to locate a symbol. Refuses sensitive files (.env, .git/, node_modules/, .theo/, lock files) and binary files (null byte in the first 8 KB); caps at 5 MB. Returns { ok, content, size } or { ok: false, error }.",
1834
+ name: opts.name ?? "read_file",
1835
+ description: opts.description ?? "Read a text file as UTF-8. ALWAYS read a file before you edit it (edit_file) or overwrite it (write_file), so your old_string / new content matches the real bytes exactly." + numbered + abs + " By default returns the whole file; use the optional offset (1-based first line) + limit to page through a large file, or search_text to locate a symbol. Refuses sensitive files (.env, .git/, node_modules/, .theo/, lock files) and binary files (null byte in the first 8 KB); caps at 5 MB. Returns { ok, content, size } or { ok: false, error }.",
1815
1836
  inputSchema: zod.z.object({
1816
1837
  path: zod.z.string().min(1).describe("File path (project-relative; absolute when allowed)."),
1817
1838
  offset: zod.z.number().int().min(1).optional().describe("1-based first line to read (default 1)."),
@@ -1985,8 +2006,8 @@ function createRunVitestTool(opts) {
1985
2006
  maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES2
1986
2007
  } = opts;
1987
2008
  return sdk.Tool.create({
1988
- name: "run_vitest",
1989
- description: "Run the project's vitest suite, optionally scoped to a file or pattern via 'path'. Returns parsed { ok, summary } or { ok: false, error }. Vitest stdout warnings are stripped \u2014 the parser extracts the trailing JSON report.",
2009
+ name: opts.name ?? "run_vitest",
2010
+ description: opts.description ?? "Run the project's vitest suite, optionally scoped to a file or pattern via 'path'. Returns parsed { ok, summary } or { ok: false, error }. Vitest stdout warnings are stripped \u2014 the parser extracts the trailing JSON report.",
1990
2011
  inputSchema: zod.z.object({
1991
2012
  path: zod.z.string().optional().describe("Optional vitest pattern or file path (project-relative).")
1992
2013
  }),
@@ -2583,8 +2604,8 @@ function createWebFetchTool(opts) {
2583
2604
  const fetchImpl = opts?.fetchImpl;
2584
2605
  const lookup = opts?.lookup;
2585
2606
  return sdk.Tool.create({
2586
- name: "web_fetch",
2587
- description: "Fetch the contents of a URL via HTTP/HTTPS. Use only for URLs the user provided or that you are confident help with the task; never invent or guess URLs. Rejects non-http(s) URLs and is SSRF-guarded by default (private/loopback/link-local/cloud-metadata hosts are refused with an ssrf_blocked error). The response body is capped at 1 MB. Returns { ok, content, status_code, content_type } or { ok: false, error }.",
2607
+ name: opts?.name ?? "web_fetch",
2608
+ description: opts?.description ?? "Fetch the contents of a URL via HTTP/HTTPS. Use only for URLs the user provided or that you are confident help with the task; never invent or guess URLs. Rejects non-http(s) URLs and is SSRF-guarded by default (private/loopback/link-local/cloud-metadata hosts are refused with an ssrf_blocked error). The response body is capped at 1 MB. Returns { ok, content, status_code, content_type } or { ok: false, error }.",
2588
2609
  inputSchema: zod.z.object({
2589
2610
  url: zod.z.string().min(1).describe("URL to fetch (http or https only)."),
2590
2611
  timeout_ms: zod.z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000).")
@@ -2670,8 +2691,8 @@ function createWebFetchTool(opts) {
2670
2691
  function createWebSearchTool(opts) {
2671
2692
  const { search, defaultMaxResults = 5 } = opts;
2672
2693
  return sdk.Tool.create({
2673
- name: "web_search",
2674
- description: "Search the web for a query \u2014 use when you need current information beyond the repo or your training cutoff (library docs, an error message, an API). Returns a list of results with title, URL, and snippet; follow up with web_fetch on a promising result to read it in full. The search provider is injected by the consumer. Returns { ok, results } or { ok: false, error }.",
2694
+ name: opts.name ?? "web_search",
2695
+ description: opts.description ?? "Search the web for a query \u2014 use when you need current information beyond the repo or your training cutoff (library docs, an error message, an API). Returns a list of results with title, URL, and snippet; follow up with web_fetch on a promising result to read it in full. The search provider is injected by the consumer. Returns { ok, results } or { ok: false, error }.",
2675
2696
  inputSchema: zod.z.object({
2676
2697
  query: zod.z.string().min(1).describe("Search query."),
2677
2698
  max_results: zod.z.number().int().positive().max(20).optional().describe("Maximum results to return (default 5, max 20).")
@@ -2757,8 +2778,8 @@ function createWriteFileTool(opts) {
2757
2778
  }
2758
2779
  const guard = opts.requireReadBeforeWrite ? opts.readTracker : void 0;
2759
2780
  return sdk.Tool.create({
2760
- name: "write_file",
2761
- description: "Write UTF-8 content to a project-relative file, creating parent directories as needed. OVERWRITES any existing file at the path. Prefer editing an existing file with edit_file over rewriting it; use write_file to create a NEW file or fully replace a small one. If the file already exists, read_file it first so you do not discard content you have not seen. Refuses paths that escape the write root and sensitive files (.env, .git/, node_modules/, .theo/, lock files); the default local root also refuses binary-file overwrites. Returns { ok, path, bytes } or { ok: false, error }.",
2781
+ name: opts.name ?? "write_file",
2782
+ description: opts.description ?? "Write UTF-8 content to a project-relative file, creating parent directories as needed. OVERWRITES any existing file at the path. Prefer editing an existing file with edit_file over rewriting it; use write_file to create a NEW file or fully replace a small one. If the file already exists, read_file it first so you do not discard content you have not seen. Refuses paths that escape the write root and sensitive files (.env, .git/, node_modules/, .theo/, lock files); the default local root also refuses binary-file overwrites. Returns { ok, path, bytes } or { ok: false, error }.",
2762
2783
  inputSchema: zod.z.object({
2763
2784
  path: zod.z.string().min(1).describe("Project-relative file path."),
2764
2785
  content: zod.z.string().describe("UTF-8 content to write.")