@theokit/sdk-tools 0.2.0 → 0.4.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/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/dist/index.cjs +97 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -14
- package/dist/index.d.ts +25 -14
- package/dist/index.js +97 -126
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/dist/index.d.cts
CHANGED
|
@@ -269,17 +269,28 @@ declare function buildEnvContext(cwd: string): string;
|
|
|
269
269
|
declare function buildRepoMap(cwd: string, opts?: RepoMapOptions): string;
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* Catastrophic-command guardrail for `shell_exec` (M3-2).
|
|
273
|
-
*
|
|
274
|
-
* `catastrophicShellReason` is a pure, segment-aware deny-list
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
272
|
+
* Catastrophic-command guardrail for `shell_exec` (M3-2; hardened V3-1).
|
|
273
|
+
*
|
|
274
|
+
* `catastrophicShellReason` is a pure, segment-aware deny-list ported from
|
|
275
|
+
* theocode's security-reviewed `shell-guard.ts` (the proven spec: 42-blocked +
|
|
276
|
+
* 24-allowed corpus, 0 misses / 0 false-positives). It splits a command on shell
|
|
277
|
+
* separators (`;`, `&&`, `||`, `|`, `&`, newline), inspects EVERY segment (so a
|
|
278
|
+
* chained `rm -rf <safe>; rm -rf /` cannot hide), and matches at COMMAND POSITION
|
|
279
|
+
* (the executable, not an arbitrary substring) so a mention like `echo "rm -rf /"`
|
|
280
|
+
* is not over-blocked. Returns a human reason for the first catastrophic segment,
|
|
281
|
+
* else `null`.
|
|
282
|
+
*
|
|
283
|
+
* Categories: recursive-force `rm` of an absolute/home/parent path; destructive git
|
|
284
|
+
* (force-push, `reset --hard`, `clean -fd`); remote-code-execution (curl/wget piped
|
|
285
|
+
* OR command-substitution `$( )` / `<( )` / eval / source); disk/raw-device wipe
|
|
286
|
+
* (mkfs, `dd of=/dev/`, `truncate /dev/`, `> /dev/<blockdev>`); recursive chmod/chown
|
|
287
|
+
* of a root path (SDK extra); fork bomb; `find -delete` / `-exec rm`; secret-file
|
|
288
|
+
* exfiltration over the network.
|
|
289
|
+
*
|
|
290
|
+
* This is a heuristic GUARDRAIL, NOT a sandbox: it is bypassable by deep obfuscation
|
|
291
|
+
* (base64/env-indirection) and is best-effort. POSIX `/bin/sh` only; Windows
|
|
292
|
+
* PowerShell is out of scope. True isolation needs a container.
|
|
293
|
+
* referencia: .claude/knowledge-base/references/theocode-shell-guard/server-lib/shell-guard.ts
|
|
283
294
|
*/
|
|
284
295
|
|
|
285
296
|
/** Thrown / reported when a command matches the catastrophic deny-list. */
|
|
@@ -288,10 +299,10 @@ declare class CatastrophicCommandError extends ConfigurationError {
|
|
|
288
299
|
constructor(reason: string);
|
|
289
300
|
}
|
|
290
301
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
302
|
+
* Return a human-readable reason when `command` is catastrophic/irreversible, or `null`.
|
|
303
|
+
* The message is surfaced to the model so it self-corrects.
|
|
293
304
|
*/
|
|
294
|
-
declare function catastrophicShellReason(
|
|
305
|
+
declare function catastrophicShellReason(command: string): string | null;
|
|
295
306
|
|
|
296
307
|
/**
|
|
297
308
|
* ACI (Agent-Computer Interface) helpers for tools (M3-5).
|
package/dist/index.d.ts
CHANGED
|
@@ -269,17 +269,28 @@ declare function buildEnvContext(cwd: string): string;
|
|
|
269
269
|
declare function buildRepoMap(cwd: string, opts?: RepoMapOptions): string;
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* Catastrophic-command guardrail for `shell_exec` (M3-2).
|
|
273
|
-
*
|
|
274
|
-
* `catastrophicShellReason` is a pure, segment-aware deny-list
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
272
|
+
* Catastrophic-command guardrail for `shell_exec` (M3-2; hardened V3-1).
|
|
273
|
+
*
|
|
274
|
+
* `catastrophicShellReason` is a pure, segment-aware deny-list ported from
|
|
275
|
+
* theocode's security-reviewed `shell-guard.ts` (the proven spec: 42-blocked +
|
|
276
|
+
* 24-allowed corpus, 0 misses / 0 false-positives). It splits a command on shell
|
|
277
|
+
* separators (`;`, `&&`, `||`, `|`, `&`, newline), inspects EVERY segment (so a
|
|
278
|
+
* chained `rm -rf <safe>; rm -rf /` cannot hide), and matches at COMMAND POSITION
|
|
279
|
+
* (the executable, not an arbitrary substring) so a mention like `echo "rm -rf /"`
|
|
280
|
+
* is not over-blocked. Returns a human reason for the first catastrophic segment,
|
|
281
|
+
* else `null`.
|
|
282
|
+
*
|
|
283
|
+
* Categories: recursive-force `rm` of an absolute/home/parent path; destructive git
|
|
284
|
+
* (force-push, `reset --hard`, `clean -fd`); remote-code-execution (curl/wget piped
|
|
285
|
+
* OR command-substitution `$( )` / `<( )` / eval / source); disk/raw-device wipe
|
|
286
|
+
* (mkfs, `dd of=/dev/`, `truncate /dev/`, `> /dev/<blockdev>`); recursive chmod/chown
|
|
287
|
+
* of a root path (SDK extra); fork bomb; `find -delete` / `-exec rm`; secret-file
|
|
288
|
+
* exfiltration over the network.
|
|
289
|
+
*
|
|
290
|
+
* This is a heuristic GUARDRAIL, NOT a sandbox: it is bypassable by deep obfuscation
|
|
291
|
+
* (base64/env-indirection) and is best-effort. POSIX `/bin/sh` only; Windows
|
|
292
|
+
* PowerShell is out of scope. True isolation needs a container.
|
|
293
|
+
* referencia: .claude/knowledge-base/references/theocode-shell-guard/server-lib/shell-guard.ts
|
|
283
294
|
*/
|
|
284
295
|
|
|
285
296
|
/** Thrown / reported when a command matches the catastrophic deny-list. */
|
|
@@ -288,10 +299,10 @@ declare class CatastrophicCommandError extends ConfigurationError {
|
|
|
288
299
|
constructor(reason: string);
|
|
289
300
|
}
|
|
290
301
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
302
|
+
* Return a human-readable reason when `command` is catastrophic/irreversible, or `null`.
|
|
303
|
+
* The message is surfaced to the model so it self-corrects.
|
|
293
304
|
*/
|
|
294
|
-
declare function catastrophicShellReason(
|
|
305
|
+
declare function catastrophicShellReason(command: string): string | null;
|
|
295
306
|
|
|
296
307
|
/**
|
|
297
308
|
* ACI (Agent-Computer Interface) helpers for tools (M3-5).
|
package/dist/index.js
CHANGED
|
@@ -94,8 +94,8 @@ function isForbiddenPath(input) {
|
|
|
94
94
|
if (first === ".git") return true;
|
|
95
95
|
if (first === "node_modules") return true;
|
|
96
96
|
if (first === ".theo") return true;
|
|
97
|
-
const
|
|
98
|
-
if (LOCK_FILES.has(
|
|
97
|
+
const basename = segments[segments.length - 1];
|
|
98
|
+
if (LOCK_FILES.has(basename)) return true;
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -266,7 +266,7 @@ function createEditFileTool(opts) {
|
|
|
266
266
|
const { projectRoot } = opts;
|
|
267
267
|
return defineTool({
|
|
268
268
|
name: "edit_file",
|
|
269
|
-
description: "
|
|
269
|
+
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 }.",
|
|
270
270
|
inputSchema: z.object({
|
|
271
271
|
path: z.string().min(1).describe("Project-relative file path."),
|
|
272
272
|
old_string: z.string().min(1).describe("String to find in the file."),
|
|
@@ -274,6 +274,9 @@ function createEditFileTool(opts) {
|
|
|
274
274
|
}),
|
|
275
275
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: unified diff parsing is inherently complex
|
|
276
276
|
handler: async ({ path, old_string, new_string }) => {
|
|
277
|
+
if (old_string === new_string) {
|
|
278
|
+
return JSON.stringify({ ok: false, error: "no_change", path });
|
|
279
|
+
}
|
|
277
280
|
if (isForbiddenPath(path)) {
|
|
278
281
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
279
282
|
}
|
|
@@ -522,7 +525,7 @@ function createGlobTool(opts) {
|
|
|
522
525
|
const { projectRoot } = opts;
|
|
523
526
|
return defineTool({
|
|
524
527
|
name: "glob_files",
|
|
525
|
-
description: "
|
|
528
|
+
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 }.",
|
|
526
529
|
inputSchema: z.object({
|
|
527
530
|
pattern: z.string().min(1).describe("Glob pattern (e.g. '**/*.ts', 'src/**/*.json')."),
|
|
528
531
|
cwd: z.string().optional().describe("Project-relative subdirectory to search from.")
|
|
@@ -599,128 +602,96 @@ var CatastrophicCommandError = class extends ConfigurationError {
|
|
|
599
602
|
});
|
|
600
603
|
}
|
|
601
604
|
};
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
"
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const a = t[0];
|
|
624
|
-
const b = t[t.length - 1];
|
|
625
|
-
if (a === '"' && b === '"' || a === "'" && b === "'") return t.slice(1, -1);
|
|
626
|
-
}
|
|
627
|
-
return t;
|
|
628
|
-
}
|
|
629
|
-
function tokenize(s) {
|
|
630
|
-
return s.trim().split(/\s+/).filter(Boolean);
|
|
631
|
-
}
|
|
632
|
-
function splitSegments(cmd) {
|
|
633
|
-
return cmd.split(/&&|\|\||;|\|/);
|
|
634
|
-
}
|
|
635
|
-
function stripPrefixTokens(tokens) {
|
|
636
|
-
let t = tokens;
|
|
637
|
-
let head = t[0];
|
|
638
|
-
while (head !== void 0 && PREFIX_TOKENS.has(basename(unquote(head)))) {
|
|
639
|
-
t = t.slice(1);
|
|
640
|
-
head = t[0];
|
|
641
|
-
}
|
|
642
|
-
return t;
|
|
643
|
-
}
|
|
644
|
-
function operandsOf(tokens) {
|
|
645
|
-
return tokens.slice(1).filter((t) => !t.startsWith("-")).map(unquote);
|
|
646
|
-
}
|
|
647
|
-
var HOME_VAR = /^\$\{?HOME\}?$/;
|
|
648
|
-
function isRootishPath(op) {
|
|
649
|
-
if (op === "~" || op === "*" || op === "." || HOME_VAR.test(op)) return true;
|
|
650
|
-
let collapsed = op.replace(/\/+/g, "/");
|
|
651
|
-
if (collapsed.length > 1 && collapsed.endsWith("/")) collapsed = collapsed.slice(0, -1);
|
|
652
|
-
if (collapsed === "/" || collapsed === "/*" || collapsed === "/.") return true;
|
|
653
|
-
return SYSTEM_DIR.test(collapsed);
|
|
654
|
-
}
|
|
655
|
-
function hasRecursiveForce(tokens) {
|
|
656
|
-
const flags = tokens.slice(1).filter((t) => t.startsWith("-"));
|
|
657
|
-
const recursive = flags.some(
|
|
658
|
-
(f) => f === "--recursive" || !f.startsWith("--") && /[rR]/.test(f)
|
|
659
|
-
);
|
|
660
|
-
const force = flags.some((f) => f === "--force" || !f.startsWith("--") && f.includes("f"));
|
|
605
|
+
function unquote(token) {
|
|
606
|
+
return token.replace(/^(['"])(.*)\1$/, "$2").replace(/^['"]|['"]$/g, "");
|
|
607
|
+
}
|
|
608
|
+
function commandSegments(command) {
|
|
609
|
+
return command.split(/&&|\|\||[;|&\n]/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
610
|
+
}
|
|
611
|
+
function commandArgs(segment, name) {
|
|
612
|
+
const tokens = segment.split(/\s+/);
|
|
613
|
+
let i = 0;
|
|
614
|
+
if (tokens[i] === "sudo") i += 1;
|
|
615
|
+
if (tokens[i] !== name) return null;
|
|
616
|
+
return tokens.slice(i + 1);
|
|
617
|
+
}
|
|
618
|
+
function commandSegmentsNamed(command, name) {
|
|
619
|
+
return commandSegments(command).map((s) => commandArgs(s, name)).filter((args) => args !== null);
|
|
620
|
+
}
|
|
621
|
+
function isRecursiveForce(args) {
|
|
622
|
+
const flags = args.filter((t) => t.startsWith("-")).join(" ");
|
|
623
|
+
if (flags.length === 0) return false;
|
|
624
|
+
const recursive = /-[a-z]*r/i.test(flags) || /--recursive/.test(flags);
|
|
625
|
+
const force = /-[a-z]*f/i.test(flags) || /--force/.test(flags);
|
|
661
626
|
return recursive && force;
|
|
662
627
|
}
|
|
663
|
-
function
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
628
|
+
function isRecursive(args) {
|
|
629
|
+
const flags = args.filter((t) => t.startsWith("-")).join(" ");
|
|
630
|
+
return /-[a-z]*r/i.test(flags) || /--recursive/.test(flags);
|
|
631
|
+
}
|
|
632
|
+
var SAFE_ABSOLUTE_TARGET = /^\/(tmp|var\/tmp)(\/|$)/;
|
|
633
|
+
function targetsDangerousPath(args) {
|
|
634
|
+
const targets = args.filter((token) => token.length > 0 && !token.startsWith("-")).map(unquote);
|
|
635
|
+
return targets.some((raw) => {
|
|
636
|
+
const t = raw.replace(/\/+/g, "/");
|
|
637
|
+
if (t === "/dev/null" || SAFE_ABSOLUTE_TARGET.test(t)) return false;
|
|
638
|
+
return /^\/($|\*)/.test(t) || // "/" or "/*"
|
|
639
|
+
/^\/[^/]/.test(t) || // an absolute path like /etc, /usr/local, /home/user/x
|
|
640
|
+
t === "~" || t.startsWith("~/") || /\$\{?HOME\b\}?/.test(t) || // $HOME or ${HOME}
|
|
641
|
+
t === ".." || t.startsWith("../") || t.includes("/..") || t === "*";
|
|
642
|
+
});
|
|
667
643
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
return ops.length === 0 || ops.some(isRootishPath) ? "rm -rf of a root/home/glob path" : null;
|
|
686
|
-
};
|
|
687
|
-
var mkfsCheck = (cmd0) => cmd0.startsWith("mkfs") ? "mkfs on a device" : null;
|
|
688
|
-
var ddCheck = (cmd0, tokens) => {
|
|
689
|
-
if (cmd0 !== "dd") return null;
|
|
690
|
-
return tokens.some((t) => unquote(t).startsWith("of=/dev/")) ? "dd writing to a device" : null;
|
|
691
|
-
};
|
|
692
|
-
var gitForceCheck = (cmd0, tokens) => {
|
|
693
|
-
if (cmd0 !== "git" || !tokens.includes("push") || tokens.includes("--force-with-lease")) {
|
|
694
|
-
return null;
|
|
644
|
+
var DEVICE_WIPE = [
|
|
645
|
+
/\bmkfs(\.\w+)?\b/,
|
|
646
|
+
/\bdd\b[^\n]*\bof=\/dev\//,
|
|
647
|
+
/\btruncate\b[^\n]*\s\/dev\//,
|
|
648
|
+
/>\s*\/dev\/(sd|nvme|hd|vd|mmcblk|disk|loop|dm-)/
|
|
649
|
+
];
|
|
650
|
+
var checkRemoteExec = (cmd) => /\b(curl|wget|fetch)\b[^\n]*\|\s*(sudo\s+)?(sh|bash|zsh|dash|python[0-9.]*|node|ruby|perl)\b/i.test(
|
|
651
|
+
cmd
|
|
652
|
+
) || /(\$\(|<\()\s*(sudo\s+)?(curl|wget|fetch)\b/i.test(cmd) || /\b(eval|source)\b[^\n]*\b(curl|wget|fetch)\b/i.test(cmd) ? "executes a remote download (pipe / command-substitution / eval) \u2014 remote code execution" : null;
|
|
653
|
+
var checkDeviceWipe = (cmd) => DEVICE_WIPE.some((re) => re.test(cmd)) ? "writes to a raw block device / formats a disk" : null;
|
|
654
|
+
var checkForkBomb = (cmd) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(cmd) ? "fork bomb" : null;
|
|
655
|
+
var checkDestructiveGit = (cmd) => {
|
|
656
|
+
if (/\bgit\b[^\n]*\bpush\b[^\n]*(--force(?!-with-lease)\b|\s-f\b|\s\+\S)/.test(cmd)) {
|
|
657
|
+
return "git force-push (overwrites remote history)";
|
|
658
|
+
}
|
|
659
|
+
if (/\bgit\b[^\n]*\breset\b[^\n]*--hard\b/.test(cmd)) {
|
|
660
|
+
return "git reset --hard (discards committed and working changes)";
|
|
695
661
|
}
|
|
696
|
-
|
|
697
|
-
|
|
662
|
+
if (/\bgit\b[^\n]*\bclean\b[^\n]*(-[a-z]*f[a-z]*d|-[a-z]*d[a-z]*f)/.test(cmd)) {
|
|
663
|
+
return "git clean -fd (permanently deletes untracked files)";
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
698
666
|
};
|
|
699
|
-
var
|
|
700
|
-
|
|
701
|
-
|
|
667
|
+
var checkRm = (cmd) => commandSegmentsNamed(cmd, "rm").some((a) => isRecursiveForce(a) && targetsDangerousPath(a)) ? "recursive force-delete of an absolute, home, or parent path" : null;
|
|
668
|
+
var checkPerm = (cmd) => ["chmod", "chown"].some(
|
|
669
|
+
(name) => commandSegmentsNamed(cmd, name).some((a) => isRecursive(a) && targetsDangerousPath(a))
|
|
670
|
+
) ? "recursive permission change on an absolute, home, or parent path" : null;
|
|
671
|
+
var checkFind = (cmd) => /\bfind\s+(\/\S*|~\S*|\$\{?HOME\}?\S*)\s[^\n]*(-delete\b|-exec\s+rm\b)/.test(cmd) ? "find -delete / -exec rm on an absolute or home path" : null;
|
|
672
|
+
var checkExfiltration = (cmd) => {
|
|
673
|
+
const touchesSecret = /(^|[\s/'"])(\.env(\.\w+)?|id_rsa|id_ed25519|\.ssh(\/|\b)|credentials|\.aws(\/|\b)|\.npmrc)\b/.test(
|
|
674
|
+
cmd
|
|
675
|
+
);
|
|
676
|
+
const sendsNetwork = /\b(curl|wget|nc|netcat|scp|ftp|telnet)\b/.test(cmd) || /\bpython[0-9.]*\s+-m\s+http/.test(cmd);
|
|
677
|
+
return touchesSecret && sendsNetwork ? "sends a secret/credential file over the network (exfiltration)" : null;
|
|
702
678
|
};
|
|
703
|
-
var
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
679
|
+
var CATEGORY_CHECKS = [
|
|
680
|
+
checkRemoteExec,
|
|
681
|
+
checkDeviceWipe,
|
|
682
|
+
checkForkBomb,
|
|
683
|
+
checkDestructiveGit,
|
|
684
|
+
checkRm,
|
|
685
|
+
checkPerm,
|
|
686
|
+
checkFind,
|
|
687
|
+
checkExfiltration
|
|
711
688
|
];
|
|
712
|
-
function catastrophicShellReason(
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
for (const
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
if (head === void 0) continue;
|
|
719
|
-
const cmd0 = basename(unquote(head));
|
|
720
|
-
for (const check of SEGMENT_CHECKS) {
|
|
721
|
-
const reason = check(cmd0, tokens, seg);
|
|
722
|
-
if (reason) return reason;
|
|
723
|
-
}
|
|
689
|
+
function catastrophicShellReason(command) {
|
|
690
|
+
const cmd = command.trim();
|
|
691
|
+
if (cmd.length === 0) return null;
|
|
692
|
+
for (const check of CATEGORY_CHECKS) {
|
|
693
|
+
const reason = check(cmd);
|
|
694
|
+
if (reason) return reason;
|
|
724
695
|
}
|
|
725
696
|
return null;
|
|
726
697
|
}
|
|
@@ -1241,7 +1212,7 @@ function createReadFileTool(opts) {
|
|
|
1241
1212
|
const { projectRoot } = opts;
|
|
1242
1213
|
return defineTool({
|
|
1243
1214
|
name: "read_file",
|
|
1244
|
-
description: "Read a
|
|
1215
|
+
description: "Read a project-relative 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. Returns the WHOLE file (there is no offset or line-range parameter); to locate a symbol inside a large file, use search_text instead of re-reading. Refuses paths that escape the project root, 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 }.",
|
|
1245
1216
|
inputSchema: z.object({
|
|
1246
1217
|
path: z.string().min(1).describe("Project-relative file path.")
|
|
1247
1218
|
}),
|
|
@@ -1431,7 +1402,7 @@ function createSearchTextTool(opts) {
|
|
|
1431
1402
|
} = opts;
|
|
1432
1403
|
return defineTool({
|
|
1433
1404
|
name: "search_text",
|
|
1434
|
-
description: `Search the project tree
|
|
1405
|
+
description: `Search file CONTENTS for a LITERAL, CASE-SENSITIVE query across the project tree (the query is matched as a substring, not a regex). 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 }.`,
|
|
1435
1406
|
inputSchema: z.object({
|
|
1436
1407
|
query: z.string().min(1).describe("Literal text to search for. Case-sensitive."),
|
|
1437
1408
|
path: z.string().optional().describe("Optional project-relative directory to scope the search.")
|
|
@@ -1542,7 +1513,7 @@ function createShellTool(opts) {
|
|
|
1542
1513
|
const { projectRoot, defaultTimeoutMs = DEFAULT_TIMEOUT_MS3, allowCatastrophic = false } = opts;
|
|
1543
1514
|
return defineTool({
|
|
1544
1515
|
name: "shell_exec",
|
|
1545
|
-
description: "Execute a shell command in the project directory.
|
|
1516
|
+
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 }.",
|
|
1546
1517
|
inputSchema: z.object({
|
|
1547
1518
|
command: z.string().min(1).describe("Shell command to execute."),
|
|
1548
1519
|
timeout_ms: z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
|
|
@@ -1720,7 +1691,7 @@ ${done}/${items.length} done | ${inProg} in progress | ${pending} pending`);
|
|
|
1720
1691
|
};
|
|
1721
1692
|
return {
|
|
1722
1693
|
name: "todolist",
|
|
1723
|
-
description: "
|
|
1694
|
+
description: "Create and maintain a structured task list for the current session \u2014 tracks progress and keeps a multi-step plan visible across turns. Use it proactively when the work has 3+ steps or the user gave multiple tasks; skip it for a single trivial step. Keep exactly ONE item 'in_progress' at a time, and mark 'complete' only after the work is actually done. Actions: 'add' (create with title), 'in_progress' (mark started by id), 'complete' (mark done by id), 'remove' (delete by id), 'list' (show all), 'clear_completed' (remove done items). Returns { ok, items, items_summary } (items = structured array; items_summary = formatted text).",
|
|
1724
1695
|
inputSchema: {
|
|
1725
1696
|
type: "object",
|
|
1726
1697
|
properties: {
|
|
@@ -1776,7 +1747,7 @@ function createWebFetchTool(opts) {
|
|
|
1776
1747
|
const allowPrivateHosts = opts?.allowPrivateHosts ?? false;
|
|
1777
1748
|
return defineTool({
|
|
1778
1749
|
name: "web_fetch",
|
|
1779
|
-
description: "Fetch
|
|
1750
|
+
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 }.",
|
|
1780
1751
|
inputSchema: z.object({
|
|
1781
1752
|
url: z.string().min(1).describe("URL to fetch (http or https only)."),
|
|
1782
1753
|
timeout_ms: z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000).")
|
|
@@ -1857,7 +1828,7 @@ function createWebSearchTool(opts) {
|
|
|
1857
1828
|
const { search, defaultMaxResults = 5 } = opts;
|
|
1858
1829
|
return defineTool({
|
|
1859
1830
|
name: "web_search",
|
|
1860
|
-
description: "Search the web for a query. Returns a list of results with title, URL, and snippet. The search provider is injected by the consumer. Returns { ok, results } or { ok: false, error }.",
|
|
1831
|
+
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 }.",
|
|
1861
1832
|
inputSchema: z.object({
|
|
1862
1833
|
query: z.string().min(1).describe("Search query."),
|
|
1863
1834
|
max_results: z.number().int().positive().max(20).optional().describe("Maximum results to return (default 5, max 20).")
|
|
@@ -1915,7 +1886,7 @@ function createWriteFileTool(opts) {
|
|
|
1915
1886
|
const { projectRoot } = opts;
|
|
1916
1887
|
return defineTool({
|
|
1917
1888
|
name: "write_file",
|
|
1918
|
-
description: "Write UTF-8 content to a project-relative file
|
|
1889
|
+
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 project root, sensitive files (.env, .git/, node_modules/, .theo/, lock files), and binary-file overwrites. Returns { ok, path, bytes } or { ok: false, error }.",
|
|
1919
1890
|
inputSchema: z.object({
|
|
1920
1891
|
path: z.string().min(1).describe("Project-relative file path."),
|
|
1921
1892
|
content: z.string().describe("UTF-8 content to write.")
|