@theokit/sdk-tools 0.27.0 → 0.27.2

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.ts CHANGED
@@ -34,6 +34,21 @@ interface CreateApplyPatchToolOptions {
34
34
  /** Absolute path to the project root. Every hunk path is gated against this boundary. */
35
35
  projectRoot: string;
36
36
  }
37
+ /**
38
+ * Build the `apply_patch` tool over Codex's V4A patch grammar — one call that adds, updates, deletes
39
+ * and moves several files at once.
40
+ *
41
+ * Prefer it to a run of {@link createEditFileTool} calls when a change spans files, because the whole
42
+ * patch is planned before anything is written: every file read, every new content computed and every
43
+ * path security-checked first, and any failure aborts with zero writes. A sequence of `edit_file`
44
+ * calls has no such property — the third can fail with the first two already on disk. The price is
45
+ * that it leaves no backup, where `edit_file` writes a `.bak`.
46
+ *
47
+ * Refusals: `parse_error`, `path_traversal`, `forbidden_path` (checked at every path segment here,
48
+ * not just the first), `not_found`, `patch_failed` (context did not match), `duplicate_target` (two
49
+ * hunks touching one file, which would silently lose the earlier edit), `file_exists` (Add over an
50
+ * existing file) and `io_error` for anything the filesystem raises during the write phase.
51
+ */
37
52
  declare function createApplyPatchTool(opts: CreateApplyPatchToolOptions): CustomTool;
38
53
 
39
54
  /**
@@ -114,6 +129,17 @@ interface CreateCurrentTimeToolOptions {
114
129
  /** Injectable clock — defaults to `() => new Date()`. Pass a fixed clock for deterministic tests. */
115
130
  clock?: () => Date;
116
131
  }
132
+ /**
133
+ * Build the `current_time` tool, so the model reads a clock instead of stating a date from training
134
+ * data.
135
+ *
136
+ * The model may pass an IANA `timezone`; omitted, the answer is UTC. An unknown zone comes back as
137
+ * `{ ok: false, error: "invalid_timezone" }` — the `RangeError` `Intl` raises is caught, so the
138
+ * handler never throws at the model.
139
+ *
140
+ * Pass `clock` in tests. The default reads the real wall clock, and a test asserting on a formatted
141
+ * date without injecting one is asserting on the day it was written.
142
+ */
117
143
  declare function createCurrentTimeTool(opts?: CreateCurrentTimeToolOptions): CustomTool;
118
144
 
119
145
  /**
@@ -124,7 +150,7 @@ declare function createCurrentTimeTool(opts?: CreateCurrentTimeToolOptions): Cus
124
150
  *
125
151
  * Return shape (always a JSON string):
126
152
  * - `{ ok: true, replacements: 1 }` on success
127
- * - `{ ok: false, error: 'no_match' | 'not_found' | 'path_traversal' |
153
+ * - `{ ok: false, error: 'no_change' | 'no_match' | 'not_found' | 'path_traversal' |
128
154
  * 'forbidden_path' }` on refusal
129
155
  */
130
156
 
@@ -146,6 +172,21 @@ interface CreateEditFileToolOptions {
146
172
  * write go through the backend (surface-agnostic); omitted ⇒ the local `fs` path (byte-identical). */
147
173
  filesystem?: FilesystemProvider;
148
174
  }
175
+ /**
176
+ * Build the `edit_file` tool: replace one occurrence of `old_string` with `new_string` in place,
177
+ * after copying the previous content to `<path>.bak`.
178
+ *
179
+ * Three matching attempts run in order and the first hit wins — the exact substring, then the same
180
+ * text with runs of whitespace collapsed, then a line-based ladder tolerating trailing whitespace and
181
+ * typographic punctuation. The FIRST exact occurrence is taken with no ambiguity check, so a short
182
+ * `old_string` appearing twice edits the earlier one silently: include enough surrounding context to
183
+ * make it unique. Only the ladder refuses an ambiguous target, and it is reached only when the exact
184
+ * and whitespace-normalised passes have both failed.
185
+ *
186
+ * `old_string === new_string` is refused up front as `no_change`; the other refusals are `no_match`,
187
+ * `not_found`, `forbidden_path` and `path_traversal`. Exactly one occurrence changes per call, so
188
+ * replacing every occurrence means calling until `no_match`.
189
+ */
149
190
  declare function createEditFileTool(opts: CreateEditFileToolOptions): CustomTool;
150
191
 
151
192
  /**
@@ -192,13 +233,31 @@ interface CreateGitDiffToolOptions {
192
233
  name?: string;
193
234
  /** M76 — description exposed to the model. Omitted => today's literal (additive). */
194
235
  description?: string;
236
+ /** Absolute path to the project root. `git` runs here and every `path` scope is gated against it. */
195
237
  projectRoot: string;
238
+ /** Wall-clock cap on the local `git` child; the process group is killed on expiry. Default 30_000. */
196
239
  timeoutMs?: number;
240
+ /** Cap on captured stdout; excess sets `truncated: true`. Default 5 MB. Local path only. */
197
241
  maxStdoutBytes?: number;
198
242
  /** Optional injected execution backend (`@theokit/sdk/sandbox`) — when provided, `git diff` runs via
199
243
  * `SandboxBackend.execute` (surface-agnostic); omitted ⇒ the local `git` child process (unchanged). */
200
244
  sandbox?: SandboxProvider;
201
245
  }
246
+ /**
247
+ * Build the `git_diff` tool: `git diff --no-color` over the working tree, or the staged changes when
248
+ * the model passes `cached`.
249
+ *
250
+ * Reach for it before a commit or after a run of edits, when the question is what CHANGED rather than
251
+ * what a file now contains — `read_file` answers the latter and spends a whole file doing it.
252
+ *
253
+ * Refusals are `not_a_repo`, `path_traversal`, `timeout` and `git_failed`. The local path kills the
254
+ * process group at `timeoutMs` and caps captured stdout at `maxStdoutBytes`, flagging
255
+ * `truncated: true`; neither limit applies on the `sandbox` path, where the backend's own `timeoutMs`
256
+ * is the only bound and `truncated` always comes back false.
257
+ *
258
+ * With `sandbox` set the local `.git` probe is skipped deliberately — the repository lives in the
259
+ * backend, and a missing one surfaces as git's own "not a git repository".
260
+ */
202
261
  declare function createGitDiffTool(opts: CreateGitDiffToolOptions): CustomTool;
203
262
 
204
263
  /**
@@ -229,8 +288,12 @@ interface CreateGitStatusToolOptions {
229
288
  * `SandboxBackend.execute`; omitted ⇒ the local `git` (unchanged).
230
289
  *
231
290
  * Symmetry with `createGitDiffTool`, flagged by the M76 review: without it `git_diff` would run
232
- * confined and `git_status` not, in the same session — and the asymmetry would be invisible until someone
233
- * notice that one of the two escapes the sandbox.
291
+ * confined and `git_status` not, in the same session — and the asymmetry would be invisible until
292
+ * someone noticed that one of the two escapes the sandbox.
293
+ *
294
+ * When set, the repository question is answered by the BACKEND (from git's own stderr), not by a
295
+ * probe of the host's filesystem. #346 — the host probe used to run first, so a session whose
296
+ * checkout lives inside the backend got `not_a_repo` for a repository that was there.
234
297
  */
235
298
  sandbox?: SandboxProvider;
236
299
  /**
@@ -250,6 +313,23 @@ interface CreateGitStatusToolOptions {
250
313
  */
251
314
  includeBranch?: boolean;
252
315
  }
316
+ /**
317
+ * Build the `git_status` tool. Returns `git status --porcelain=v1` output, which is the reason to use
318
+ * this rather than a `shell_exec` of plain `git status`: the human format is not stable across git
319
+ * versions and the porcelain one is.
320
+ *
321
+ * The output arrives in a field named `diff`, shared with {@link createGitDiffTool} rather than named
322
+ * after what it holds.
323
+ *
324
+ * A missing `.git` is `{ ok: false, error: "not_a_repo" }` rather than an empty string, so the model
325
+ * cannot read "not a repository" as "nothing changed". The other refusals are `path_traversal`,
326
+ * `timeout` and `git_failed`.
327
+ *
328
+ * With `sandbox` the command itself runs through the injected backend — but the local `.git` probe
329
+ * runs BEFORE that branch, so a sandboxed status still requires `<projectRoot>/.git` to exist on the
330
+ * host. {@link createGitDiffTool} skips its equivalent probe when sandboxed. Configure both tools the
331
+ * same way in one session: one confined and one not is an asymmetry nothing surfaces.
332
+ */
253
333
  declare function createGitStatusTool(opts: CreateGitStatusToolOptions): CustomTool;
254
334
 
255
335
  /**
@@ -275,6 +355,22 @@ interface CreateGlobToolOptions {
275
355
  * backend (Local/remote), so the tool is surface-agnostic; omitted ⇒ the local `readdir` (unchanged). */
276
356
  filesystem?: FilesystemProvider;
277
357
  }
358
+ /**
359
+ * Build the `glob_files` tool: find files by the shape of their name. Use it when you know what the
360
+ * file is called, `search_text` when you know what is inside it, `read_file` when you know the path.
361
+ *
362
+ * The pattern is matched against paths relative to `cwd`, while the paths returned are relative to
363
+ * `projectRoot` — `{ pattern: "*.ts", cwd: "src" }` matches `index.ts` and returns `src/index.ts`.
364
+ * Only `*`, `**` and `?` are wildcards; braces and character classes are escaped to literals, so
365
+ * `*.{ts,js}` matches a file actually named that. Matching is anchored at both ends, which is why a
366
+ * bare `*.ts` sees only the top level of the search root and a leading `**` is usually what is meant.
367
+ *
368
+ * `node_modules`, `.git`, `dist` and `.theo` are skipped by name at any depth and cannot be
369
+ * re-enabled. Nothing else is filtered: this returns names, so a `.env` file is listed even though
370
+ * `read_file` would refuse to open it. An unreadable directory is skipped without a word rather than
371
+ * failing the call, so a permission problem looks like an empty directory. The only refusal is
372
+ * `path_traversal` on `cwd`; a pattern matching nothing is `{ ok: true, files: [] }`.
373
+ */
278
374
  declare function createGlobTool(opts: CreateGlobToolOptions): CustomTool;
279
375
 
280
376
  /**
@@ -356,6 +452,15 @@ declare function isCommandAllowed(command: string, policies: CommandPolicy[]): b
356
452
  * its own `{ ok: false, error }` shape.
357
453
  */
358
454
  type ContextMatchReason = "empty" | "ambiguous" | "not_found";
455
+ /**
456
+ * Thrown by {@link replaceUnique} when the replacement cannot be made safely. `reason` names the
457
+ * case: `empty` (no target given), `ambiguous` (the target matched in more than one place, so nothing
458
+ * was replaced), `not_found` (no rung of the ladder matched).
459
+ *
460
+ * `ambiguous` is the one worth handling apart from the others. It says the caller's target text is
461
+ * too short, not that the file lacks it, so the remedy is more surrounding context rather than a
462
+ * different search.
463
+ */
359
464
  declare class ContextMatchError extends Error {
360
465
  readonly reason: ContextMatchReason;
361
466
  constructor(reason: ContextMatchReason, message: string);
@@ -457,6 +562,11 @@ interface RepoMapOptions {
457
562
  /** Max directory depth to descend. Default 4. */
458
563
  maxDepth?: number;
459
564
  }
565
+ /**
566
+ * Options for {@link buildEnvContext}. Both fields exist to make the block deterministic under test:
567
+ * `now` fixes the date line, `gitHeadPath` points the branch lookup at a fixture instead of
568
+ * `<cwd>/.git/HEAD`. Neither changes what the block contains.
569
+ */
460
570
  interface EnvContextOptions {
461
571
  /** Injectable clock for the date line (deterministic tests). Default `new Date()`. */
462
572
  now?: Date;
@@ -647,6 +757,22 @@ interface CreateListDirToolOptions {
647
757
  */
648
758
  filesystem?: FilesystemProvider;
649
759
  }
760
+ /**
761
+ * Build the `list_dir` tool: the direct entries of one directory as `{ name, type }`. Never
762
+ * recursive — use `glob_files` to search a tree.
763
+ *
764
+ * The result always carries `truncated` and `totalCount`, so an agent that hits the `max` cap
765
+ * (default 500) can tell a large directory from a complete listing and switch to `glob_files` or
766
+ * `search_text`. There is no offset input, so paging past the cap is not possible; `totalCount`
767
+ * counts everything the directory holds, including what was cut.
768
+ *
769
+ * `path` is project-relative and `"."` means the root. Refusals are `forbidden_path`,
770
+ * `path_traversal` and `not_found`, the last of which also covers a path that exists but is a file.
771
+ *
772
+ * On a `filesystem` backend each entry costs a `stat` to learn its type, and an entry whose `stat`
773
+ * fails is reported as a file rather than dropped — so `type` is best-effort there and exact on the
774
+ * local path.
775
+ */
650
776
  declare function createListDirTool(opts: CreateListDirToolOptions): CustomTool;
651
777
 
652
778
  /**
@@ -663,6 +789,13 @@ declare function createListDirTool(opts: CreateListDirToolOptions): CustomTool;
663
789
  */
664
790
 
665
791
  type Mode = "normal" | "plan";
792
+ /**
793
+ * The `plan_mode` tool object returned by the no-argument {@link createPlanModeTool}. Its `handler`
794
+ * is synchronous, where {@link PlanModeToolWithStore}'s returns a promise — so the two are not
795
+ * interchangeable at a call site that does not await.
796
+ *
797
+ * `currentMode` is a test seam; the mode also comes back in every result.
798
+ */
666
799
  interface PlanModeTool {
667
800
  name: string;
668
801
  description: string;
@@ -706,6 +839,20 @@ interface PlanModeToolOptions {
706
839
  /** Artifact id under which the plan is stored. Default `"plan"`. */
707
840
  artifactId?: string;
708
841
  }
842
+ /**
843
+ * Build the `plan_mode` tool, which flips a flag and returns instruction text telling the model to
844
+ * outline before it edits. It enforces nothing — no other tool consults the mode, so a model that
845
+ * ignores the instructions is not stopped, and the mode lives in this object rather than in the
846
+ * session.
847
+ *
848
+ * With no argument it returns a {@link PlanModeTool} whose handler is synchronous. With options it
849
+ * returns a {@link PlanModeToolWithStore} whose handler is async and accepts a `plan` string,
850
+ * persisted to `artifactStore` under `artifactId` (default `"plan"`) when the model exits plan mode.
851
+ * An empty or absent `plan` is not written, and `persisted` in the result says which happened.
852
+ *
853
+ * `name` and `description` are read on the options overload only. The no-argument form always
854
+ * publishes `plan_mode` with the built-in description, since it takes nothing to override it with.
855
+ */
709
856
  declare function createPlanModeTool(): PlanModeTool;
710
857
  declare function createPlanModeTool(options: PlanModeToolOptions): PlanModeToolWithStore;
711
858
 
@@ -714,7 +861,9 @@ declare function createPlanModeTool(options: PlanModeToolOptions): PlanModeToolW
714
861
  *
715
862
  * Return shape (always a JSON string):
716
863
  * - `{ ok: true, answer: string }`
717
- * - `{ ok: false, error: "timeout" }`
864
+ * - `{ ok: false, error: "timeout" }` — nobody answered within `timeoutMs`
865
+ * - `{ ok: false, error: "no_asker" }` — neither `ctx.context.askUser` nor the factory's `askUser`
866
+ * was available, so there was nobody to ask
718
867
  */
719
868
  interface QuestionToolOptions {
720
869
  /**
@@ -770,6 +919,23 @@ interface QuestionTool {
770
919
  threadId?: string;
771
920
  }) => Promise<string>;
772
921
  }
922
+ /**
923
+ * Build the `question` tool: the agent stops, asks the user something, and the turn waits for the
924
+ * answer.
925
+ *
926
+ * The asker is resolved per call — run context first (`ctx.context.askUser`), then the factory's
927
+ * `askUser` — so one tool object shared across sessions can still reach the right user. With neither,
928
+ * the call returns `{ ok: false, error: "no_asker" }` at once rather than hanging until the timeout;
929
+ * that error means the host is mis-wired, not that the user declined.
930
+ *
931
+ * After `timeoutMs` (default 5 minutes) the result is `{ ok: false, error: "timeout" }` and
932
+ * `onAbandon` fires. Supply `onAbandon` whenever your asker holds a slot per thread: the promise the
933
+ * tool stopped awaiting is still pending on your side, so without the callback the UI keeps rendering
934
+ * a prompt nobody is waiting on and rejects every later question as already pending.
935
+ *
936
+ * Anything else the asker rejects with propagates out of the handler — only its own timeout is
937
+ * converted.
938
+ */
773
939
  declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
774
940
 
775
941
  /**
@@ -851,6 +1017,26 @@ interface CreateReadFileToolOptions {
851
1017
  */
852
1018
  allowAbsolute?: boolean;
853
1019
  }
1020
+ /**
1021
+ * Build the `read_file` tool.
1022
+ *
1023
+ * Success is `{ ok: true, content, size }`. Every refusal is a `{ ok: false, error }` value rather
1024
+ * than a throw, so the model can correct itself: `not_found`, `forbidden_path` (the sensitive-file
1025
+ * blocklist), `path_traversal` (escapes `projectRoot`, or the backend refused it), `binary_file` (a
1026
+ * null byte in the first 8 KB) and `too_large` (over 5 MB — a fixed ceiling, not an option).
1027
+ *
1028
+ * `offset`/`limit` page by line and `lineNumbers` renders a `cat -n` view (`<n>\t<line>`). Both are
1029
+ * applied after the whole file has been read, so they bound what the model sees, not what is read
1030
+ * from disk; the 5 MB cap is what bounds the read.
1031
+ *
1032
+ * `allowAbsolute` widens the boundary to the entire filesystem for absolute paths, leaving only the
1033
+ * any-depth secret guard in front of it. It is for a trusted local agent; on a shared or
1034
+ * multi-tenant host pass `filesystem` instead and let the backend own the boundary.
1035
+ *
1036
+ * Pass one {@link ReadTracker} here and the same instance to {@link createWriteFileTool} to enable
1037
+ * read-before-write: this tool records the mtime it saw, and the write tool then refuses to
1038
+ * overwrite a file that was never read or has changed since.
1039
+ */
854
1040
  declare function createReadFileTool(opts: CreateReadFileToolOptions): CustomTool;
855
1041
 
856
1042
  /**
@@ -895,10 +1081,12 @@ declare class ReasoningTools {
895
1081
  * - `{ ok: false, error: 'path_traversal' | 'forbidden_path' | 'timeout' |
896
1082
  * 'no_vitest' | 'unparseable_output' }`
897
1083
  *
898
- * Implementation note: invokes vitest via `npx --no-install vitest`. The
899
- * `--no-install` avoids the agent triggering a multi-megabyte download
900
- * mid-turn if vitest is missingthe tool fails cleanly with
901
- * `no_vitest` instead.
1084
+ * Implementation note: invokes vitest via `npx --no-install vitest`. The `--no-install` avoids the
1085
+ * agent triggering a multi-megabyte download mid-turn when vitest is missing. That case surfaces as
1086
+ * `no_vitest` (#347), recognised from npm's own complaint on stderr `npx` itself starts, so the
1087
+ * spawn succeeds and only the text distinguishes a missing package from a run that produced no
1088
+ * parseable JSON. A spawn failure (`npx` not on PATH) is `no_vitest` too. If npm ever rewords its
1089
+ * message the case falls back to `unparseable_output`, with the real reason in the payload.
902
1090
  */
903
1091
 
904
1092
  interface CreateRunVitestToolOptions {
@@ -911,12 +1099,30 @@ interface CreateRunVitestToolOptions {
911
1099
  timeoutMs?: number;
912
1100
  maxStdoutBytes?: number;
913
1101
  }
1102
+ /**
1103
+ * The fields lifted from vitest's JSON report. All optional, because the object is the report's own
1104
+ * top level passed through unvalidated — a vitest version that renames a field yields `undefined`
1105
+ * here rather than an error, so treat a missing `success` as unknown, never as failed.
1106
+ */
914
1107
  interface VitestSummary {
915
1108
  numTotalTests?: number;
916
1109
  numPassedTests?: number;
917
1110
  numFailedTests?: number;
918
1111
  success?: boolean;
919
1112
  }
1113
+ /**
1114
+ * Build the `run_vitest` tool: run the project's suite and return the counts rather than the log.
1115
+ *
1116
+ * A FAILING suite is `{ ok: true, summary }` with `success: false`. `ok` reports only that vitest
1117
+ * ran; an agent that branches on `ok` reads a red suite as a green one. And the summary carries
1118
+ * counts alone — which test failed, and why, is not in the result, so chasing a failure means falling
1119
+ * back to `shell_exec`.
1120
+ *
1121
+ * Runs `npx --no-install vitest run --reporter=json`, so a project without vitest installed fails
1122
+ * instead of downloading it mid-turn. That failure arrives as `no_vitest` (#347), as does `npx` not
1123
+ * being on PATH. The remaining refusals are `path_traversal`, `forbidden_path` and `timeout`
1124
+ * (default 120s, process group killed).
1125
+ */
920
1126
  declare function createRunVitestTool(opts: CreateRunVitestToolOptions): CustomTool;
921
1127
 
922
1128
  /**
@@ -970,6 +1176,29 @@ interface CreateSearchTextToolOptions {
970
1176
  * sandbox). Forbidden dirs are still skipped. Default `false` ⇒ absolute scope rejected (unchanged). */
971
1177
  allowAbsolute?: boolean;
972
1178
  }
1179
+ /**
1180
+ * Build the `search_text` tool: scan file CONTENTS across the tree. Use it when you know what the
1181
+ * code says, and `glob_files` when you know what the file is called.
1182
+ *
1183
+ * `regex` is fixed at construction, not chosen per call — it changes both the input schema and the
1184
+ * description the model sees, so one instance is either literal-substring or regex, never both.
1185
+ * Literal matching is case-sensitive and there is no case-insensitive mode. In regex mode the pattern
1186
+ * is compiled with no flags (an inline `(?i)` is a syntax error, not a modifier) and an invalid
1187
+ * pattern returns `{ ok: false, error: "invalid_regex" }` before any file is opened.
1188
+ *
1189
+ * Matches carry `{ file, line, preview }` with `line` 1-based and `preview` cut at 200 characters.
1190
+ * The walk stops at the first match past `maxMatches` (default 100), so on a truncated run
1191
+ * `totalMatches` is `maxMatches + 1` — it counts what was scanned, not how many matches exist. Files
1192
+ * over `maxFileSize` (default 1 MB), files with a null byte in their first 8 KB, and anything
1193
+ * unreadable are skipped in silence.
1194
+ *
1195
+ * `allowAbsolute` honours an absolute `path` scope. Be aware that the sensitive-path filter applied
1196
+ * during the walk inspects only the first segment of each entry's path relative to `projectRoot`,
1197
+ * plus its basename. Under an absolute scope every entry's relative path begins with `..`, so a
1198
+ * nested `.env` is neither skipped nor refused and its matching lines are returned.
1199
+ * {@link createReadFileTool} and {@link createListDirTool} carry an any-depth guard for exactly this
1200
+ * case; this tool does not.
1201
+ */
973
1202
  declare function createSearchTextTool(opts: CreateSearchTextToolOptions): CustomTool;
974
1203
 
975
1204
  /**
@@ -1015,6 +1244,28 @@ interface CreateShellToolOptions {
1015
1244
  */
1016
1245
  sandbox?: SandboxProvider;
1017
1246
  }
1247
+ /**
1248
+ * Build the `shell_exec` tool: run a command through `/bin/sh -c` with `projectRoot` as the working
1249
+ * directory.
1250
+ *
1251
+ * Reserve it for what has no dedicated tool — tests, git, package managers, build steps. Reading,
1252
+ * writing, editing and searching files each have a path-checked tool in this package, and routing
1253
+ * them through a shell gives up every one of those checks.
1254
+ *
1255
+ * The model's `timeout_ms` and `defaultTimeoutMs` (30s) are both clamped to a 5-minute ceiling that
1256
+ * is not configurable. On expiry the process group is killed and the result is
1257
+ * `{ ok: false, error: "timeout" }`. stdout and stderr are capped at 5 MB each and cut silently at
1258
+ * the cap — there is no truncation flag, so a command producing more looks like one that produced
1259
+ * exactly 5 MB.
1260
+ *
1261
+ * A non-zero exit is `{ ok: true, ..., exit_code }`, not an error: `ok` reports that the command ran,
1262
+ * `exit_code` reports whether it worked.
1263
+ *
1264
+ * The catastrophic-command screen runs before either execution path and refuses on a heuristic match
1265
+ * over the command string. It is a guardrail, not a sandbox, and `allowCatastrophic: true` removes it
1266
+ * outright. For real confinement pass `sandbox`, which routes execution through the backend — the
1267
+ * 5 MB output caps belong to the local path and do not apply there.
1268
+ */
1018
1269
  declare function createShellTool(opts: CreateShellToolOptions): CustomTool;
1019
1270
 
1020
1271
  /**
@@ -1042,6 +1293,13 @@ interface TodoItem {
1042
1293
  createdAt: number;
1043
1294
  completedAt?: number;
1044
1295
  }
1296
+ /**
1297
+ * The `todolist` tool object. It is hand-built rather than produced by `Tool.create`, so
1298
+ * `inputSchema` is a plain JSON Schema value and `handler` is synchronous.
1299
+ *
1300
+ * `getItems` is a test seam onto the same session state the handler mutates, reachable without going
1301
+ * through an action.
1302
+ */
1045
1303
  interface TodolistTool {
1046
1304
  name: string;
1047
1305
  description: string;
@@ -1073,6 +1331,19 @@ type TodoInput = {
1073
1331
  } | {
1074
1332
  action: "clear_completed";
1075
1333
  };
1334
+ /**
1335
+ * Build the `todolist` tool: an in-memory checklist the agent keeps across the turns of one session.
1336
+ *
1337
+ * State lives in the closure. It is never written anywhere and dies with the process, so this is a
1338
+ * working memo for the model, not a task store. One tool object can serve many sessions — lists are
1339
+ * keyed by `ctx.threadId`, and every call arriving without one shares a single default list, which
1340
+ * means a multi-session host that forgets to thread the id merges all its users' tasks together.
1341
+ *
1342
+ * Ids are `todo-1`, `todo-2`, … per session and are never reused, so a removed item's id stays gone.
1343
+ * Each successful action returns the whole list twice: `items` structured for a UI, `items_summary`
1344
+ * formatted for the model. Failures are `missing_title`, `missing_id`, `not_found` and
1345
+ * `invalid_action`.
1346
+ */
1076
1347
  declare function createTodolistTool(): TodolistTool;
1077
1348
 
1078
1349
  /**
@@ -1129,6 +1400,10 @@ declare function todoItemsToPlanNodes(items: readonly TodoItem[]): PlanNode[];
1129
1400
  */
1130
1401
  /** How the middle is dropped when output exceeds the budget. */
1131
1402
  type TruncationMode = "head" | "head-tail";
1403
+ /**
1404
+ * Options for {@link truncateOutput}. Every field has a default, so `truncateOutput(text)` is a
1405
+ * complete call.
1406
+ */
1132
1407
  interface TruncationOptions {
1133
1408
  /** Maximum output size in bytes before truncation. Default: 30_000. */
1134
1409
  maxBytes?: number;
@@ -1141,6 +1416,13 @@ interface TruncationOptions {
1141
1416
  */
1142
1417
  mode?: TruncationMode;
1143
1418
  }
1419
+ /**
1420
+ * What {@link truncateOutput} returns.
1421
+ *
1422
+ * `truncated` is the field to branch on. On the false branch `content` is the input unchanged and
1423
+ * `overflowPath` is absent; reading `overflowPath` without checking `truncated` first is how a
1424
+ * consumer ends up joining `undefined` into a path.
1425
+ */
1144
1426
  interface TruncationResult {
1145
1427
  /** The (possibly truncated) content. */
1146
1428
  content: string;
@@ -1155,6 +1437,29 @@ interface TruncationResult {
1155
1437
  /** Path to the full output file, present only when truncated. */
1156
1438
  overflowPath?: string;
1157
1439
  }
1440
+ /**
1441
+ * Bound a block of tool output to `maxBytes`, spilling the full text to a file when it does not fit.
1442
+ *
1443
+ * Nothing is written and nothing is copied when the output already fits — the comparison is
1444
+ * `originalBytes <= maxBytes`, so a payload exactly at the limit passes through untouched. Above the
1445
+ * limit the WHOLE original is written under `outputDir` (created recursively) and the returned
1446
+ * `content` is the cut text plus a trailer naming that file. The trailer is appended after the cut,
1447
+ * so the returned string is longer than `maxBytes`: the budget bounds what is copied out of the
1448
+ * input, not what comes back.
1449
+ *
1450
+ * Choose the mode by where the information sits. `"head"` keeps the opening, which suits a document
1451
+ * or a listing. `"head-tail"` splits the budget between the start and the end, which is what command
1452
+ * output needs — the exit status, the failing assertion and the summary all live in the last lines,
1453
+ * and `"head"` discards exactly those.
1454
+ *
1455
+ * The head is cut on a UTF-8 code-point boundary and never ends in a replacement character. The tail
1456
+ * of `"head-tail"` starts mid-buffer, so it can open with one U+FFFD where a multi-byte character was
1457
+ * split; the omitted-byte count in the separator is computed from `maxBytes` and is approximate for
1458
+ * the same reason.
1459
+ *
1460
+ * Touches the filesystem on the truncating path — a `mkdirSync`/`writeFileSync` failure propagates
1461
+ * rather than degrading to an untruncated return.
1462
+ */
1158
1463
  declare function truncateOutput(output: string, opts?: TruncationOptions): TruncationResult;
1159
1464
 
1160
1465
  /**
@@ -1213,6 +1518,11 @@ declare function createUpdatePlanTool(): CustomTool;
1213
1518
  * not a slow request — it is a failed turn, and an expensive one.
1214
1519
  */
1215
1520
  declare const DEFAULT_MAX_IMAGE_BYTES: number;
1521
+ /**
1522
+ * Options for {@link createViewImageTool}. `maxBytes` is measured on disk, before base64 inflates the
1523
+ * payload by roughly a third on its way into the model's context — so the real context cost of a file
1524
+ * at the limit is about 6.7 MB of text, not 5 MB.
1525
+ */
1216
1526
  interface CreateViewImageToolOptions {
1217
1527
  /** Root the tool reads from. Every path is resolved inside it. */
1218
1528
  projectRoot: string;
@@ -1264,6 +1574,26 @@ interface CreateWebFetchToolOptions {
1264
1574
  /** DNS resolver (injectable for tests — drive the SSRF path with no real DNS). */
1265
1575
  lookup?: ScreenedFetchOptions["lookup"];
1266
1576
  }
1577
+ /**
1578
+ * Build the `web_fetch` tool: retrieve one URL over HTTP or HTTPS and hand the model the body as
1579
+ * text. Every option is optional — `createWebFetchTool()` gives an SSRF-guarded fetch with a
1580
+ * 30-second timeout.
1581
+ *
1582
+ * The body is decoded as UTF-8 whatever the content type says, so a PDF or an image comes back as
1583
+ * mojibake rather than an error: check `content_type` before trusting `content`. The 1 MB body cap is
1584
+ * fixed rather than an option, and is enforced twice — against `content-length` when the server sends
1585
+ * one, then against the bytes actually downloaded.
1586
+ *
1587
+ * The SSRF guard screens the resolved ADDRESS rather than the hostname, and screens every redirect
1588
+ * hop again. `maxRedirects: 0` refuses any 3xx outright with `redirect_blocked`, which is the strict
1589
+ * setting for a URL the model chose itself. `allowPrivateHosts: true` disables the guard entirely,
1590
+ * and with a model-supplied URL that is enough to reach a cloud metadata endpoint — it is for local
1591
+ * development against your own services, not for production.
1592
+ *
1593
+ * Refusals: `invalid_url` (unparseable, or a scheme other than http/https), `ssrf_blocked`,
1594
+ * `redirect_blocked`, `too_large`, `timeout`, `fetch_failed`. A 4xx or 5xx is NOT a refusal — it
1595
+ * returns `ok: true` with the body and `status_code`, so check the status.
1596
+ */
1267
1597
  declare function createWebFetchTool(opts?: CreateWebFetchToolOptions): CustomTool;
1268
1598
 
1269
1599
  /**
@@ -1282,7 +1612,22 @@ interface WebSearchResult {
1282
1612
  url: string;
1283
1613
  snippet: string;
1284
1614
  }
1615
+ /**
1616
+ * The search provider {@link createWebSearchTool} calls.
1617
+ *
1618
+ * `maxResults` is what the caller asked for, not a limit you must enforce — the tool slices the array
1619
+ * itself — but returning far more than asked wastes the round trip that produced them.
1620
+ *
1621
+ * Reject the promise to signal a failed search: the tool turns a rejection into
1622
+ * `{ ok: false, error: "search_failed" }` and never lets it escape into the agent turn. Resolving
1623
+ * with `[]` says something different — that the search ran and found nothing.
1624
+ */
1285
1625
  type WebSearchCallback = (query: string, maxResults: number) => Promise<WebSearchResult[]>;
1626
+ /**
1627
+ * Options for {@link createWebSearchTool}. `search` is required: the tool ships no provider of its
1628
+ * own, and the two adapters in this package — {@link createBraveWebSearchAdapter} and
1629
+ * {@link createGenericHttpSearchAdapter} — exist to fill it.
1630
+ */
1286
1631
  interface CreateWebSearchToolOptions {
1287
1632
  /** M76 — name exposed to the model. Omitted => today's literal (additive). The name is a contract:
1288
1633
  * the approval key, what the model sees and what telemetry records. */
@@ -1294,6 +1639,19 @@ interface CreateWebSearchToolOptions {
1294
1639
  /** Default max results if not specified by the LLM. */
1295
1640
  defaultMaxResults?: number;
1296
1641
  }
1642
+ /**
1643
+ * Build the `web_search` tool over a caller-supplied provider.
1644
+ *
1645
+ * The tool holds no API key and talks to no service; everything network-facing lives in the `search`
1646
+ * callback, which is why one tool serves Brave, a self-hosted endpoint, or a fixture in a test. Use
1647
+ * it to find pages and `web_fetch` to read one — results carry a snippet, not a body.
1648
+ *
1649
+ * The model's `max_results` (1..20) wins over `defaultMaxResults`, and the list is sliced to it even
1650
+ * when the provider returns more. A rejecting provider yields `{ ok: false, error: "search_failed" }`
1651
+ * with its message attached; a provider resolving `[]` yields `{ ok: true, results: [] }`. The two
1652
+ * are different signals, so an adapter that swallows its own failures — as
1653
+ * {@link createGenericHttpSearchAdapter} deliberately does — reports the first as the second.
1654
+ */
1297
1655
  declare function createWebSearchTool(opts: CreateWebSearchToolOptions): CustomTool;
1298
1656
 
1299
1657
  /**
@@ -1309,6 +1667,11 @@ declare function createWebSearchTool(opts: CreateWebSearchToolOptions): CustomTo
1309
1667
  */
1310
1668
 
1311
1669
  type FetchLike$1 = (url: string, init?: RequestInit) => Promise<Response>;
1670
+ /**
1671
+ * Options for {@link createBraveWebSearchAdapter}. `apiKey` falls back to `BRAVE_API_KEY` and
1672
+ * `endpoint` to Brave's public search URL, both read once at construction, so a process that exports
1673
+ * the env var can call the factory with no arguments.
1674
+ */
1312
1675
  interface CreateBraveWebSearchAdapterOptions {
1313
1676
  /** Brave API key. Defaults to `process.env.BRAVE_API_KEY`. */
1314
1677
  apiKey?: string;
@@ -1345,6 +1708,12 @@ declare function createBraveWebSearchAdapter(opts?: CreateBraveWebSearchAdapterO
1345
1708
  */
1346
1709
 
1347
1710
  type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
1711
+ /**
1712
+ * Options for {@link createGenericHttpSearchAdapter}. Unlike the Brave adapter, an absent `apiKey` or
1713
+ * `endpoint` is not an error — the callback is still built and simply returns no results — so if you
1714
+ * need to know whether search is actually configured, check both here rather than waiting for an
1715
+ * empty result set to tell you.
1716
+ */
1348
1717
  interface CreateGenericHttpSearchAdapterOptions {
1349
1718
  /** Bearer token. Defaults to `process.env.THEOKIT_SEARCH_API_KEY`. */
1350
1719
  apiKey?: string;
@@ -1377,6 +1746,10 @@ type WriteToolContext = {
1377
1746
  signal?: AbortSignal;
1378
1747
  context?: unknown;
1379
1748
  };
1749
+ /**
1750
+ * Options for {@link createWriteFileTool}. `requireReadBeforeWrite` and `readTracker` are a pair —
1751
+ * the flag without the tracker is refused at construction rather than quietly disabling the guard.
1752
+ */
1380
1753
  interface CreateWriteFileToolOptions {
1381
1754
  /** M76 — name exposed to the model. Omitted => today's literal (additive). The name is a contract:
1382
1755
  * the approval key, what the model sees and what telemetry records. */
@@ -1403,6 +1776,22 @@ interface CreateWriteFileToolOptions {
1403
1776
  /** SE32 — the per-run tracker populated by the paired `read_file` tool. */
1404
1777
  readTracker?: ReadTracker;
1405
1778
  }
1779
+ /**
1780
+ * Build the `write_file` tool. It OVERWRITES: reach for {@link createEditFileTool} when the file
1781
+ * exists and only part of it changes.
1782
+ *
1783
+ * Parent directories are created recursively. Refusals arrive as `{ ok: false, error }`:
1784
+ * `forbidden_path`, `path_traversal`, `binary_file`, plus `read_required` / `stale_file` when
1785
+ * read-before-write is on, plus `read_only` / `write_failed` on the backend path.
1786
+ *
1787
+ * The `binary_file` probe (a null byte in the first 8 KB of the file being replaced) runs on the
1788
+ * local path only. A `filesystem` backend has no equivalent, so overwriting a binary file through a
1789
+ * backend is not refused.
1790
+ *
1791
+ * Throws at construction — not at call time — when `requireReadBeforeWrite` is set without a
1792
+ * `readTracker`. A guard that silently does nothing is worse than no guard, and construction is the
1793
+ * only place the pairing can be checked.
1794
+ */
1406
1795
  declare function createWriteFileTool(opts: CreateWriteFileToolOptions): CustomTool;
1407
1796
 
1408
1797
  export { CatastrophicCommandError, type CommandPolicy, ContextMatchError, type ContextMatchReason, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateCurrentTimeToolOptions, type CreateEditFileToolOptions, type CreateGenericHttpSearchAdapterOptions, type CreateGitDiffToolOptions, type CreateGitStatusToolOptions, type CreateGlobToolOptions, type CreateInteractiveShellToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateViewImageToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_MAX_IMAGE_BYTES, DEFAULT_TOOL_GUIDANCE, type EnvContextOptions, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, ReadTracker, ReasoningTools, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationMode, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createCurrentTimeTool, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGitStatusTool, createGlobTool, createInteractiveShellTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createUpdatePlanTool, createViewImageTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, createWriteStdinTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, replaceUnique, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withName, withShellExitGuidance, withToolResultGuidance };