@theokit/sdk-tools 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,7 +5,11 @@ var path = require('path');
5
5
  var sdk = require('@theokit/sdk');
6
6
  var zod = require('zod');
7
7
  var fs = require('fs');
8
+ var persistence = require('@theokit/sdk/internal/persistence');
9
+ var pathSafety = require('@theokit/sdk/path-safety');
8
10
  var child_process = require('child_process');
11
+ var promises$1 = require('dns/promises');
12
+ var net = require('net');
9
13
 
10
14
  // src/apply-patch.ts
11
15
  var PathTraversalError = class extends sdk.ConfigurationError {
@@ -57,8 +61,8 @@ function realpathOfDeepestExisting(path$1) {
57
61
  } catch {
58
62
  }
59
63
  try {
60
- const stat = fs.lstatSync(path$1);
61
- if (stat.isSymbolicLink()) {
64
+ const stat2 = fs.lstatSync(path$1);
65
+ if (stat2.isSymbolicLink()) {
62
66
  const target = fs.readlinkSync(path$1);
63
67
  const parentReal = realpathOfDeepestExisting(path.dirname(path$1));
64
68
  const parentBase = parentReal ?? path.dirname(path$1);
@@ -92,8 +96,8 @@ function isForbiddenPath(input) {
92
96
  if (first === ".git") return true;
93
97
  if (first === "node_modules") return true;
94
98
  if (first === ".theo") return true;
95
- const basename = segments[segments.length - 1];
96
- if (LOCK_FILES.has(basename)) return true;
99
+ const basename2 = segments[segments.length - 1];
100
+ if (LOCK_FILES.has(basename2)) return true;
97
101
  return false;
98
102
  }
99
103
 
@@ -221,6 +225,45 @@ function applyHunks(content, changes) {
221
225
  }
222
226
  return result.join("\n");
223
227
  }
228
+ function createSessionArtifactStore(options) {
229
+ const { dir } = options;
230
+ const idStrategy = options.idStrategy ?? ((id) => pathSafety.safeFilenameForId(id));
231
+ const extension = options.extension ?? ".md";
232
+ function path(id) {
233
+ return pathSafety.safePathJoin(dir, `${idStrategy(id)}${extension}`);
234
+ }
235
+ async function write(id, content) {
236
+ const target = path(id);
237
+ await promises.mkdir(dir, { recursive: true });
238
+ await persistence.replaceFileAtomic(target, content);
239
+ return target;
240
+ }
241
+ async function read(id) {
242
+ try {
243
+ return await promises.readFile(path(id), "utf8");
244
+ } catch {
245
+ return void 0;
246
+ }
247
+ }
248
+ async function has(id) {
249
+ try {
250
+ await promises.stat(path(id));
251
+ return true;
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+ async function list() {
257
+ let entries;
258
+ try {
259
+ entries = await promises.readdir(dir);
260
+ } catch {
261
+ return [];
262
+ }
263
+ return entries.filter((name) => name.endsWith(extension)).map((name) => name.slice(0, name.length - extension.length));
264
+ }
265
+ return { write, read, has, list, path };
266
+ }
224
267
  function createEditFileTool(opts) {
225
268
  const { projectRoot } = opts;
226
269
  return sdk.defineTool({
@@ -550,6 +593,471 @@ function globToRegex(pattern) {
550
593
  }
551
594
  return new RegExp(`^${regexStr}$`);
552
595
  }
596
+ var CatastrophicCommandError = class extends sdk.ConfigurationError {
597
+ name = "CatastrophicCommandError";
598
+ constructor(reason) {
599
+ super(`Refused a catastrophic shell command: ${reason} (shell guardrail).`, {
600
+ code: "catastrophic_command"
601
+ });
602
+ }
603
+ };
604
+ var SHELL_NAMES = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh", "ash"]);
605
+ var PREFIX_TOKENS = /* @__PURE__ */ new Set([
606
+ "sudo",
607
+ "doas",
608
+ "env",
609
+ "command",
610
+ "time",
611
+ "nice",
612
+ "nohup",
613
+ "exec",
614
+ "builtin"
615
+ ]);
616
+ var FORK_BOMB = /:\s*\(\s*\)\s*\{[^}]*\|[^}]*&[^}]*\}/;
617
+ var DEVICE_REDIRECT = /[>]\s*\/dev\/(?:sd|nvme|hd|vd|mmcblk|disk|loop|dm-)\w*/;
618
+ var SYSTEM_DIR = /^\/(?:etc|usr|bin|sbin|lib|lib64|var|boot|home|root|opt|sys|proc|dev)(?:\/\*?)?$/;
619
+ function basename(p) {
620
+ const i = p.lastIndexOf("/");
621
+ return i >= 0 ? p.slice(i + 1) : p;
622
+ }
623
+ function unquote(t) {
624
+ if (t.length >= 2) {
625
+ const a = t[0];
626
+ const b = t[t.length - 1];
627
+ if (a === '"' && b === '"' || a === "'" && b === "'") return t.slice(1, -1);
628
+ }
629
+ return t;
630
+ }
631
+ function tokenize(s) {
632
+ return s.trim().split(/\s+/).filter(Boolean);
633
+ }
634
+ function splitSegments(cmd) {
635
+ return cmd.split(/&&|\|\||;|\|/);
636
+ }
637
+ function stripPrefixTokens(tokens) {
638
+ let t = tokens;
639
+ let head = t[0];
640
+ while (head !== void 0 && PREFIX_TOKENS.has(basename(unquote(head)))) {
641
+ t = t.slice(1);
642
+ head = t[0];
643
+ }
644
+ return t;
645
+ }
646
+ function operandsOf(tokens) {
647
+ return tokens.slice(1).filter((t) => !t.startsWith("-")).map(unquote);
648
+ }
649
+ var HOME_VAR = /^\$\{?HOME\}?$/;
650
+ function isRootishPath(op) {
651
+ if (op === "~" || op === "*" || op === "." || HOME_VAR.test(op)) return true;
652
+ let collapsed = op.replace(/\/+/g, "/");
653
+ if (collapsed.length > 1 && collapsed.endsWith("/")) collapsed = collapsed.slice(0, -1);
654
+ if (collapsed === "/" || collapsed === "/*" || collapsed === "/.") return true;
655
+ return SYSTEM_DIR.test(collapsed);
656
+ }
657
+ function hasRecursiveForce(tokens) {
658
+ const flags = tokens.slice(1).filter((t) => t.startsWith("-"));
659
+ const recursive = flags.some(
660
+ (f) => f === "--recursive" || !f.startsWith("--") && /[rR]/.test(f)
661
+ );
662
+ const force = flags.some((f) => f === "--force" || !f.startsWith("--") && f.includes("f"));
663
+ return recursive && force;
664
+ }
665
+ function hasRecursiveFlag(tokens) {
666
+ return tokens.slice(1).some(
667
+ (t) => t === "--recursive" || t.startsWith("-") && !t.startsWith("--") && /[rR]/.test(t)
668
+ );
669
+ }
670
+ function isCurlPipedToShell(cmd) {
671
+ const segs = cmd.replace(/\|\|/g, ";").split(/[;|]/).map((s) => s.trim()).filter(Boolean);
672
+ let fetcher = false;
673
+ let shell = false;
674
+ for (const s of segs) {
675
+ const tk = stripPrefixTokens(tokenize(s));
676
+ const head = tk[0];
677
+ if (head === void 0) continue;
678
+ const c = basename(unquote(head));
679
+ if (c === "curl" || c === "wget") fetcher = true;
680
+ if (SHELL_NAMES.has(c)) shell = true;
681
+ }
682
+ return fetcher && shell;
683
+ }
684
+ var rmCheck = (cmd0, tokens) => {
685
+ if (cmd0 !== "rm" || !hasRecursiveForce(tokens)) return null;
686
+ const ops = operandsOf(tokens);
687
+ return ops.length === 0 || ops.some(isRootishPath) ? "rm -rf of a root/home/glob path" : null;
688
+ };
689
+ var mkfsCheck = (cmd0) => cmd0.startsWith("mkfs") ? "mkfs on a device" : null;
690
+ var ddCheck = (cmd0, tokens) => {
691
+ if (cmd0 !== "dd") return null;
692
+ return tokens.some((t) => unquote(t).startsWith("of=/dev/")) ? "dd writing to a device" : null;
693
+ };
694
+ var gitForceCheck = (cmd0, tokens) => {
695
+ if (cmd0 !== "git" || !tokens.includes("push") || tokens.includes("--force-with-lease")) {
696
+ return null;
697
+ }
698
+ const force = tokens.includes("--force") || tokens.some((t) => /^-[a-z]*f[a-z]*$/.test(t)) || operandsOf(tokens).some((op) => /^\+[^+]/.test(op));
699
+ return force ? "git push --force" : null;
700
+ };
701
+ var permCheck = (cmd0, tokens) => {
702
+ if (cmd0 !== "chmod" && cmd0 !== "chown" || !hasRecursiveFlag(tokens)) return null;
703
+ return operandsOf(tokens).some(isRootishPath) ? `${cmd0} -R on a root path` : null;
704
+ };
705
+ var redirectCheck = (_cmd0, _tokens, seg) => DEVICE_REDIRECT.test(seg) ? "redirect to a device" : null;
706
+ var SEGMENT_CHECKS = [
707
+ rmCheck,
708
+ mkfsCheck,
709
+ ddCheck,
710
+ gitForceCheck,
711
+ permCheck,
712
+ redirectCheck
713
+ ];
714
+ function catastrophicShellReason(cmd) {
715
+ if (FORK_BOMB.test(cmd)) return "fork bomb";
716
+ if (isCurlPipedToShell(cmd)) return "curl/wget piped into a shell";
717
+ for (const seg of splitSegments(cmd)) {
718
+ const tokens = stripPrefixTokens(tokenize(seg));
719
+ const head = tokens[0];
720
+ if (head === void 0) continue;
721
+ const cmd0 = basename(unquote(head));
722
+ for (const check of SEGMENT_CHECKS) {
723
+ const reason = check(cmd0, tokens, seg);
724
+ if (reason) return reason;
725
+ }
726
+ }
727
+ return null;
728
+ }
729
+
730
+ // src/internal/command-policy.ts
731
+ function denyCatastrophicCommands() {
732
+ return (command) => catastrophicShellReason(command);
733
+ }
734
+ function commandDenialReason(command, policies) {
735
+ for (const policy of policies) {
736
+ const reason = policy(command);
737
+ if (reason !== null) return reason;
738
+ }
739
+ return null;
740
+ }
741
+ function isCommandAllowed(command, policies) {
742
+ return commandDenialReason(command, policies) === null;
743
+ }
744
+ var SsrfBlockedError = class extends sdk.ConfigurationError {
745
+ name = "SsrfBlockedError";
746
+ constructor(host, detail) {
747
+ super(
748
+ `Blocked request to "${host}"${detail ? ` (${detail})` : ""}: address is private, loopback, link-local, or reserved (SSRF guard).`,
749
+ { code: "ssrf_blocked" }
750
+ );
751
+ }
752
+ };
753
+ function v4ToInt(ip) {
754
+ const parts = ip.split(".");
755
+ return (Number(parts[0]) << 24 | Number(parts[1]) << 16 | Number(parts[2]) << 8 | Number(parts[3])) >>> 0;
756
+ }
757
+ function inV4Cidr(ipInt, base, prefix) {
758
+ const mask = prefix === 0 ? 0 : 4294967295 << 32 - prefix >>> 0;
759
+ return (ipInt & mask) === (v4ToInt(base) & mask);
760
+ }
761
+ var V4_BLOCKED = [
762
+ ["0.0.0.0", 8],
763
+ // "this host"
764
+ ["10.0.0.0", 8],
765
+ // private
766
+ ["100.64.0.0", 10],
767
+ // CGNAT
768
+ ["127.0.0.0", 8],
769
+ // loopback
770
+ ["169.254.0.0", 16],
771
+ // link-local + cloud metadata
772
+ ["172.16.0.0", 12],
773
+ // private
774
+ ["192.168.0.0", 16],
775
+ // private
776
+ ["224.0.0.0", 4],
777
+ // multicast
778
+ ["240.0.0.0", 4]
779
+ // reserved
780
+ ];
781
+ function isBlockedV4(ip) {
782
+ const n = v4ToInt(ip);
783
+ return V4_BLOCKED.some(([base, prefix]) => inV4Cidr(n, base, prefix));
784
+ }
785
+ function foldDottedTail(s) {
786
+ const lastColon = s.lastIndexOf(":");
787
+ const tail = s.slice(lastColon + 1);
788
+ if (!tail.includes(".")) return s;
789
+ if (net.isIP(tail) !== 4) return null;
790
+ const o = tail.split(".").map(Number);
791
+ const hi = (o[0] << 8 | o[1]).toString(16);
792
+ const lo = (o[2] << 8 | o[3]).toString(16);
793
+ return `${s.slice(0, lastColon + 1)}${hi}:${lo}`;
794
+ }
795
+ function expandHextets(s) {
796
+ const halves = s.split("::");
797
+ if (halves.length > 2) return null;
798
+ const head = halves[0] ? halves[0].split(":") : [];
799
+ const tailGroups = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
800
+ if (halves.length === 1) return head.length === 8 ? head : null;
801
+ const missing = 8 - head.length - tailGroups.length;
802
+ if (missing < 0) return null;
803
+ return [...head, ...Array(missing).fill("0"), ...tailGroups];
804
+ }
805
+ function ipv6ToBytes(ip) {
806
+ const folded = foldDottedTail(ip.toLowerCase().split("%")[0] ?? "");
807
+ if (folded === null) return null;
808
+ const groups = expandHextets(folded);
809
+ if (groups === null || groups.length !== 8) return null;
810
+ const bytes = [];
811
+ for (const g of groups) {
812
+ const n = Number.parseInt(g || "0", 16);
813
+ bytes.push(n >> 8, n & 255);
814
+ }
815
+ return bytes;
816
+ }
817
+ function allZero(bytes, from, to) {
818
+ for (let i = from; i < to; i += 1) if (bytes[i] !== 0) return false;
819
+ return true;
820
+ }
821
+ function isBlockedV6Bytes(b) {
822
+ if (allZero(b, 0, 10) && b[10] === 255 && b[11] === 255) {
823
+ return isBlockedV4(`${b[12]}.${b[13]}.${b[14]}.${b[15]}`);
824
+ }
825
+ if (allZero(b, 0, 15) && (b[15] === 1 || b[15] === 0)) return true;
826
+ if (allZero(b, 0, 12)) return isBlockedV4(`${b[12]}.${b[13]}.${b[14]}.${b[15]}`);
827
+ if (b[0] === 254 && (b[1] & 192) === 128) return true;
828
+ return (b[0] & 254) === 252;
829
+ }
830
+ function isBlockedIp(ip) {
831
+ const fam = net.isIP(ip);
832
+ if (fam === 4) return isBlockedV4(ip);
833
+ if (fam === 6) {
834
+ const bytes = ipv6ToBytes(ip);
835
+ return bytes === null ? true : isBlockedV6Bytes(bytes);
836
+ }
837
+ return true;
838
+ }
839
+ async function resolveAndScreen(rawHost, options = {}) {
840
+ const host = rawHost.replace(/^\[|\]$/g, "");
841
+ if (net.isIP(host) !== 0) {
842
+ if (isBlockedIp(host)) throw new SsrfBlockedError(host);
843
+ return [host];
844
+ }
845
+ const lookup = options.lookup ?? promises$1.lookup;
846
+ const addrs = await lookup(host, { all: true });
847
+ if (addrs.length === 0) throw new SsrfBlockedError(host, "no addresses");
848
+ for (const a of addrs) {
849
+ if (isBlockedIp(a.address)) throw new SsrfBlockedError(host, a.address);
850
+ }
851
+ return addrs.map((a) => a.address);
852
+ }
853
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
854
+ function redirectTarget(res, current, originalUrl) {
855
+ const location = res.headers.get("location");
856
+ if (!REDIRECT_STATUSES.has(res.status) || !location) return void 0;
857
+ const next = new URL(location, current);
858
+ if (next.protocol !== "http:" && next.protocol !== "https:") {
859
+ throw new SsrfBlockedError(originalUrl, `non-http redirect to ${next.protocol}`);
860
+ }
861
+ return next.href;
862
+ }
863
+ async function screenedFetch(url, options = {}) {
864
+ const fetchImpl = options.fetchImpl ?? fetch;
865
+ const maxRedirects = options.maxRedirects ?? 5;
866
+ let current = url;
867
+ for (let hop = 0; hop <= maxRedirects; hop += 1) {
868
+ if (!options.allowPrivateHosts) {
869
+ await resolveAndScreen(new URL(current).hostname, { lookup: options.lookup });
870
+ }
871
+ const res = await fetchImpl(current, { redirect: "manual", signal: options.signal });
872
+ const next = redirectTarget(res, current, url);
873
+ if (next === void 0) return res;
874
+ current = next;
875
+ }
876
+ throw new SsrfBlockedError(url, "too many redirects");
877
+ }
878
+ var DEFAULT_BUDGET = 8e3;
879
+ var DEFAULT_MAX_DEPTH = 4;
880
+ var PER_DIR_CAP = 200;
881
+ var TRUNCATED_MARKER = "\u2026 (truncated)";
882
+ var DOC_HEAD_CHARS = 200;
883
+ var DEFAULT_REPO_MAP_IGNORE = [
884
+ "node_modules",
885
+ ".git",
886
+ "dist",
887
+ ".theo",
888
+ ".next",
889
+ "build",
890
+ "coverage",
891
+ "target",
892
+ "out"
893
+ ];
894
+ var PROJECT_DOCS = ["AGENTS.md", "CLAUDE.md", "README.md"];
895
+ var MANIFESTS = ["package.json", "pyproject.toml", "Cargo.toml", "go.mod"];
896
+ function safeExists(p) {
897
+ try {
898
+ return fs.existsSync(p);
899
+ } catch {
900
+ return false;
901
+ }
902
+ }
903
+ function safeReadHead(p, n) {
904
+ try {
905
+ return fs.readFileSync(p, "utf-8").slice(0, n).replace(/\s+/g, " ").trim();
906
+ } catch {
907
+ return "";
908
+ }
909
+ }
910
+ function buildEnvContext(cwd) {
911
+ const lines = [
912
+ "<env>",
913
+ ` Working directory: ${cwd}`,
914
+ ` Platform: ${process.platform} (${process.arch})`,
915
+ ` Node: ${process.version}`,
916
+ ` Is git repo: ${safeExists(path.join(cwd, ".git")) ? "yes" : "no"}`,
917
+ ` Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}`
918
+ ];
919
+ const docs = PROJECT_DOCS.filter((d) => safeExists(path.join(cwd, d)));
920
+ if (docs.length > 0) {
921
+ lines.push(` Project docs: ${docs.join(", ")}`);
922
+ const head = safeReadHead(path.join(cwd, docs[0]), DOC_HEAD_CHARS);
923
+ if (head) lines.push(` ${docs[0]} (head): ${head}`);
924
+ }
925
+ const manifests = MANIFESTS.filter((m) => safeExists(path.join(cwd, m)));
926
+ if (manifests.length > 0) lines.push(` Manifests: ${manifests.join(", ")}`);
927
+ lines.push("</env>");
928
+ return lines.join("\n");
929
+ }
930
+ function compareEntries(a, b) {
931
+ const ad = a.isDirectory() ? 0 : 1;
932
+ const bd = b.isDirectory() ? 0 : 1;
933
+ return ad !== bd ? ad - bd : a.name.localeCompare(b.name);
934
+ }
935
+ function visibleEntries(dir, ignore) {
936
+ let entries;
937
+ try {
938
+ entries = fs.readdirSync(dir, { withFileTypes: true });
939
+ } catch {
940
+ return [];
941
+ }
942
+ return entries.filter((e) => !ignore.has(e.name) && !e.name.startsWith(".")).sort(compareEntries);
943
+ }
944
+ function pushLine(ctx, line) {
945
+ if (ctx.used + line.length + 1 > ctx.budget) {
946
+ ctx.truncated = true;
947
+ return false;
948
+ }
949
+ ctx.lines.push(line);
950
+ ctx.used += line.length + 1;
951
+ return true;
952
+ }
953
+ function emitEntry(ctx, dir, e, depth, indent) {
954
+ const isDir = e.isDirectory();
955
+ if (!pushLine(ctx, `${indent}${isDir ? `${e.name}/` : e.name}`)) return false;
956
+ if (isDir && depth + 1 < ctx.maxDepth) return walkDir2(ctx, path.join(dir, e.name), depth + 1);
957
+ return true;
958
+ }
959
+ function walkDir2(ctx, dir, depth) {
960
+ const entries = visibleEntries(dir, ctx.ignore);
961
+ const shown = entries.slice(0, PER_DIR_CAP);
962
+ const indent = " ".repeat(depth);
963
+ for (const e of shown) {
964
+ if (!emitEntry(ctx, dir, e, depth, indent)) return false;
965
+ }
966
+ if (entries.length > shown.length) {
967
+ return pushLine(ctx, `${indent}\u2026 (${entries.length - shown.length} more)`);
968
+ }
969
+ return true;
970
+ }
971
+ function buildRepoMap(cwd, opts = {}) {
972
+ try {
973
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
974
+ return `(unavailable: ${cwd} is not a readable directory)`;
975
+ }
976
+ } catch {
977
+ return `(unavailable: ${cwd})`;
978
+ }
979
+ const ctx = {
980
+ ignore: /* @__PURE__ */ new Set([...DEFAULT_REPO_MAP_IGNORE, ...opts.ignore ?? []]),
981
+ maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH,
982
+ budget: opts.budget ?? DEFAULT_BUDGET,
983
+ lines: [],
984
+ used: 0,
985
+ truncated: false
986
+ };
987
+ walkDir2(ctx, cwd, 0);
988
+ return ctx.truncated ? `${ctx.lines.join("\n")}
989
+ ${TRUNCATED_MARKER}` : ctx.lines.join("\n");
990
+ }
991
+
992
+ // src/internal/tool-aci.ts
993
+ function withDescription(tool, description) {
994
+ return {
995
+ name: tool.name,
996
+ description,
997
+ inputSchema: tool.inputSchema,
998
+ handler: tool.handler
999
+ };
1000
+ }
1001
+ function esc(s) {
1002
+ return String(s).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1003
+ }
1004
+ function renderToolList(tools) {
1005
+ if (tools.length === 0) return "<tools></tools>";
1006
+ const lines = ["<tools>"];
1007
+ for (const t of tools) {
1008
+ lines.push(
1009
+ " <tool>",
1010
+ ` <name>${esc(t.name)}</name>`,
1011
+ ` <description>${esc(t.description)}</description>`,
1012
+ " </tool>"
1013
+ );
1014
+ }
1015
+ lines.push("</tools>");
1016
+ return lines.join("\n");
1017
+ }
1018
+
1019
+ // src/internal/tool-guidance.ts
1020
+ var DEFAULT_TOOL_GUIDANCE = {
1021
+ not_found: "The path does not exist. Use `list_dir` or `glob_files` to find the correct path, then retry.",
1022
+ path_traversal: "That path escapes the project root. Use a path inside the project directory.",
1023
+ forbidden_path: "That path is a protected file (.env, .git, lock files, etc.). Choose a different, non-sensitive path.",
1024
+ no_match: "The search text was not found verbatim. Re-read the file with `read_file` and copy the exact text (including whitespace/indentation) before editing.",
1025
+ timeout: "The operation timed out. Narrow the scope or pass a larger `timeout_ms`.",
1026
+ invalid_url: "The URL is malformed. Provide a full absolute http(s):// URL.",
1027
+ ssrf_blocked: "That host is private/loopback/reserved and is blocked. Use a public URL.",
1028
+ catastrophic_command: "That command is blocked as catastrophic. Use a safer, scoped command (e.g. a relative path instead of `/`).",
1029
+ binary_file: "The file is binary and cannot be read as text. Use a tool suited to binary data.",
1030
+ too_large: "The target is too large to process directly. Use a bounded range or a narrower scope."
1031
+ };
1032
+ function isRecord(value) {
1033
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1034
+ }
1035
+ function injectGuidance(handlerOutput, guidance) {
1036
+ let parsed;
1037
+ try {
1038
+ parsed = JSON.parse(handlerOutput);
1039
+ } catch {
1040
+ return handlerOutput;
1041
+ }
1042
+ if (!isRecord(parsed) || parsed.ok !== false) return handlerOutput;
1043
+ if ("guidance" in parsed) return handlerOutput;
1044
+ const code = parsed.error;
1045
+ if (typeof code !== "string") return handlerOutput;
1046
+ const hint = guidance[code];
1047
+ if (!hint) return handlerOutput;
1048
+ return JSON.stringify({ ...parsed, guidance: hint });
1049
+ }
1050
+ function withToolResultGuidance(tool, guidance) {
1051
+ return {
1052
+ name: tool.name,
1053
+ description: tool.description,
1054
+ inputSchema: tool.inputSchema,
1055
+ handler: async (input) => injectGuidance(await tool.handler(input), guidance)
1056
+ };
1057
+ }
1058
+ function withDefaultGuidance(tool) {
1059
+ return withToolResultGuidance(tool, DEFAULT_TOOL_GUIDANCE);
1060
+ }
553
1061
  var DEFAULT_MAX_ENTRIES = 500;
554
1062
  function createListDirTool(opts) {
555
1063
  const { projectRoot, max = DEFAULT_MAX_ENTRIES } = opts;
@@ -615,39 +1123,82 @@ var PLAN_INSTRUCTIONS = [
615
1123
  "When ready, use plan_mode with action 'exit' to return to normal mode."
616
1124
  ].join("\n");
617
1125
  var NORMAL_INSTRUCTIONS = "Returned to NORMAL MODE. You may now execute changes.";
618
- function createPlanModeTool() {
1126
+ var DESCRIPTION = "Toggle between normal and plan mode. Actions: 'enter' (switch to plan mode), 'exit' (return to normal), 'status' (check current mode). Returns { ok, mode, message }.";
1127
+ function planModeSchema(withPlan) {
1128
+ const properties = {
1129
+ action: {
1130
+ type: "string",
1131
+ enum: ["enter", "exit", "status"],
1132
+ description: "The action to perform."
1133
+ }
1134
+ };
1135
+ if (withPlan) {
1136
+ properties.plan = {
1137
+ type: "string",
1138
+ description: "On 'exit', the plan text to persist to the artifact store."
1139
+ };
1140
+ }
1141
+ return { type: "object", properties, required: ["action"] };
1142
+ }
1143
+ function renderMode(action, mode) {
1144
+ switch (action) {
1145
+ case "enter":
1146
+ return JSON.stringify({ ok: true, mode, message: PLAN_INSTRUCTIONS });
1147
+ case "exit":
1148
+ return JSON.stringify({ ok: true, mode, message: NORMAL_INSTRUCTIONS });
1149
+ case "status":
1150
+ return JSON.stringify({ ok: true, mode, message: `Current mode: ${mode}` });
1151
+ default:
1152
+ return void 0;
1153
+ }
1154
+ }
1155
+ function invalidAction(action) {
1156
+ return JSON.stringify({
1157
+ ok: false,
1158
+ error: "invalid_action",
1159
+ message: `Unknown action '${action}'. Valid: enter, exit, status.`
1160
+ });
1161
+ }
1162
+ function createPlanModeTool(options) {
619
1163
  let mode = "normal";
1164
+ if (options === void 0) {
1165
+ return {
1166
+ name: "plan_mode",
1167
+ description: DESCRIPTION,
1168
+ inputSchema: planModeSchema(false),
1169
+ handler: (input) => {
1170
+ if (input.action === "enter") mode = "plan";
1171
+ else if (input.action === "exit") mode = "normal";
1172
+ return renderMode(input.action, mode) ?? invalidAction(input.action);
1173
+ },
1174
+ currentMode: () => mode
1175
+ };
1176
+ }
1177
+ const { artifactStore, artifactId = "plan" } = options;
620
1178
  return {
621
1179
  name: "plan_mode",
622
- description: "Toggle between normal and plan mode. Actions: 'enter' (switch to plan mode), 'exit' (return to normal), 'status' (check current mode). Returns { ok, mode, message }.",
623
- inputSchema: {
624
- type: "object",
625
- properties: {
626
- action: {
627
- type: "string",
628
- enum: ["enter", "exit", "status"],
629
- description: "The action to perform."
630
- }
631
- },
632
- required: ["action"]
633
- },
634
- handler: (input) => {
635
- switch (input.action) {
636
- case "enter":
637
- mode = "plan";
638
- return JSON.stringify({ ok: true, mode, message: PLAN_INSTRUCTIONS });
639
- case "exit":
640
- mode = "normal";
641
- return JSON.stringify({ ok: true, mode, message: NORMAL_INSTRUCTIONS });
642
- case "status":
643
- return JSON.stringify({ ok: true, mode, message: `Current mode: ${mode}` });
644
- default:
1180
+ description: DESCRIPTION,
1181
+ inputSchema: planModeSchema(true),
1182
+ handler: async (input) => {
1183
+ if (input.action === "enter") {
1184
+ mode = "plan";
1185
+ return JSON.stringify({ ok: true, mode, message: PLAN_INSTRUCTIONS });
1186
+ }
1187
+ if (input.action === "exit") {
1188
+ mode = "normal";
1189
+ if (typeof input.plan === "string" && input.plan.length > 0) {
1190
+ const path = await artifactStore.write(artifactId, input.plan);
645
1191
  return JSON.stringify({
646
- ok: false,
647
- error: "invalid_action",
648
- message: `Unknown action '${input.action}'. Valid: enter, exit, status.`
1192
+ ok: true,
1193
+ mode,
1194
+ message: NORMAL_INSTRUCTIONS,
1195
+ persisted: true,
1196
+ path
649
1197
  });
1198
+ }
1199
+ return JSON.stringify({ ok: true, mode, message: NORMAL_INSTRUCTIONS, persisted: false });
650
1200
  }
1201
+ return renderMode(input.action, mode) ?? invalidAction(input.action);
651
1202
  },
652
1203
  currentMode: () => mode
653
1204
  };
@@ -737,21 +1288,21 @@ async function openHandleSafe(absolutePath, path) {
737
1288
  }
738
1289
  }
739
1290
  async function readContent(handle, path) {
740
- const stat = await handle.stat();
741
- if (stat.size > MAX_FILE_SIZE) {
1291
+ const stat2 = await handle.stat();
1292
+ if (stat2.size > MAX_FILE_SIZE) {
742
1293
  return JSON.stringify({
743
1294
  ok: false,
744
1295
  error: "too_large",
745
1296
  path,
746
- size: stat.size,
1297
+ size: stat2.size,
747
1298
  limit: MAX_FILE_SIZE
748
1299
  });
749
1300
  }
750
- if (await isBinaryProbe(handle, Number(stat.size))) {
751
- return JSON.stringify({ ok: false, error: "binary_file", path, size: stat.size });
1301
+ if (await isBinaryProbe(handle, Number(stat2.size))) {
1302
+ return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
752
1303
  }
753
1304
  const content = await handle.readFile({ encoding: "utf-8" });
754
- return JSON.stringify({ ok: true, content, size: stat.size });
1305
+ return JSON.stringify({ ok: true, content, size: stat2.size });
755
1306
  }
756
1307
  async function isBinaryProbe(handle, size) {
757
1308
  const probeLen = Math.min(BINARY_PROBE_BYTES, size);
@@ -990,7 +1541,7 @@ var DEFAULT_TIMEOUT_MS3 = 3e4;
990
1541
  var MAX_TIMEOUT_MS = 3e5;
991
1542
  var MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
992
1543
  function createShellTool(opts) {
993
- const { projectRoot, defaultTimeoutMs = DEFAULT_TIMEOUT_MS3 } = opts;
1544
+ const { projectRoot, defaultTimeoutMs = DEFAULT_TIMEOUT_MS3, allowCatastrophic = false } = opts;
994
1545
  return sdk.defineTool({
995
1546
  name: "shell_exec",
996
1547
  description: "Execute a shell command in the project directory. Returns stdout, stderr, and exit code. Default timeout 30s, max 5 minutes. Output capped at 5 MB. Returns { ok, stdout, stderr, exit_code } or { ok: false, error }.",
@@ -999,6 +1550,13 @@ function createShellTool(opts) {
999
1550
  timeout_ms: zod.z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
1000
1551
  }),
1001
1552
  handler: async ({ command, timeout_ms }) => {
1553
+ if (!allowCatastrophic) {
1554
+ const reason = catastrophicShellReason(command);
1555
+ if (reason) {
1556
+ const err = new CatastrophicCommandError(reason);
1557
+ return JSON.stringify({ ok: false, error: err.code, reason });
1558
+ }
1559
+ }
1002
1560
  const timeoutMs = Math.min(timeout_ms ?? defaultTimeoutMs, MAX_TIMEOUT_MS);
1003
1561
  const result = await runShell(projectRoot, command, timeoutMs);
1004
1562
  return result;
@@ -1077,6 +1635,11 @@ function formatResult(result, timeoutMs) {
1077
1635
  });
1078
1636
  }
1079
1637
 
1638
+ // src/todo-plan-nodes.ts
1639
+ function todoItemsToPlanNodes(items) {
1640
+ return items.map((item) => ({ id: item.id, label: item.title, status: item.status }));
1641
+ }
1642
+
1080
1643
  // src/todolist.ts
1081
1644
  function ok(data) {
1082
1645
  return JSON.stringify({ ok: true, ...data });
@@ -1097,6 +1660,9 @@ function createTodolistTool() {
1097
1660
  function findById(id) {
1098
1661
  return items.find((i) => i.id === id);
1099
1662
  }
1663
+ function listResult(extra) {
1664
+ return ok({ ...extra, items: [...items], items_summary: formatList() });
1665
+ }
1100
1666
  function formatList() {
1101
1667
  if (items.length === 0) return "No tasks. Use action 'add' to create one.";
1102
1668
  const lines = items.map((item) => {
@@ -1119,7 +1685,7 @@ ${done}/${items.length} done | ${inProg} in progress | ${pending} pending`);
1119
1685
  createdAt: Date.now()
1120
1686
  };
1121
1687
  items.push(item);
1122
- return ok({ id: item.id, message: `Added: ${item.title}`, items_summary: formatList() });
1688
+ return listResult({ id: item.id, message: `Added: ${item.title}` });
1123
1689
  }
1124
1690
  function handleSetStatus(input, status) {
1125
1691
  const id = requireId(input);
@@ -1129,7 +1695,7 @@ ${done}/${items.length} done | ${inProg} in progress | ${pending} pending`);
1129
1695
  item.status = status;
1130
1696
  if (status === "done") item.completedAt = Date.now();
1131
1697
  const verb = status === "done" ? "Completed" : "Started";
1132
- return ok({ message: `${verb}: ${item.title}`, items_summary: formatList() });
1698
+ return listResult({ message: `${verb}: ${item.title}` });
1133
1699
  }
1134
1700
  function handleRemove(input) {
1135
1701
  const id = requireId(input);
@@ -1137,29 +1703,26 @@ ${done}/${items.length} done | ${inProg} in progress | ${pending} pending`);
1137
1703
  const idx = items.findIndex((i) => i.id === id);
1138
1704
  if (idx === -1) return fail({ error: "not_found", id });
1139
1705
  const removed = items.splice(idx, 1)[0];
1140
- return ok({ message: `Removed: ${removed.title}`, items_summary: formatList() });
1706
+ return listResult({ message: `Removed: ${removed.title}` });
1141
1707
  }
1142
1708
  function handleClearCompleted() {
1143
1709
  const before = items.length;
1144
1710
  const kept = items.filter((i) => i.status !== "done");
1145
1711
  items.length = 0;
1146
1712
  items.push(...kept);
1147
- return ok({
1148
- message: `Cleared ${before - items.length} completed items`,
1149
- items_summary: formatList()
1150
- });
1713
+ return listResult({ message: `Cleared ${before - items.length} completed items` });
1151
1714
  }
1152
1715
  const actions = {
1153
1716
  add: handleAdd,
1154
1717
  in_progress: (input) => handleSetStatus(input, "in_progress"),
1155
1718
  complete: (input) => handleSetStatus(input, "done"),
1156
1719
  remove: handleRemove,
1157
- list: () => ok({ items_summary: formatList() }),
1720
+ list: () => listResult({}),
1158
1721
  clear_completed: handleClearCompleted
1159
1722
  };
1160
1723
  return {
1161
1724
  name: "todolist",
1162
- description: "Track multi-step task progress. Actions: 'add' (create task with title), 'complete' (mark done by id), 'in_progress' (mark started by id), 'remove' (delete by id), 'list' (show all), 'clear_completed' (remove done items). Returns { ok, items_summary }.",
1725
+ description: "Track multi-step task progress. Actions: 'add' (create task with title), 'complete' (mark done by id), 'in_progress' (mark started 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).",
1163
1726
  inputSchema: {
1164
1727
  type: "object",
1165
1728
  properties: {
@@ -1212,6 +1775,7 @@ var DEFAULT_TIMEOUT_MS4 = 3e4;
1212
1775
  var MAX_BODY_BYTES = 1 * 1024 * 1024;
1213
1776
  function createWebFetchTool(opts) {
1214
1777
  const defaultTimeoutMs = opts?.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS4;
1778
+ const allowPrivateHosts = opts?.allowPrivateHosts ?? false;
1215
1779
  return sdk.defineTool({
1216
1780
  name: "web_fetch",
1217
1781
  description: "Fetch content from a URL via HTTP/HTTPS. Rejects non-http(s) URLs. Response body capped at 1 MB. Returns { ok, content, status_code } or { ok: false, error }.",
@@ -1239,7 +1803,10 @@ function createWebFetchTool(opts) {
1239
1803
  const controller = new AbortController();
1240
1804
  const timer = setTimeout(() => controller.abort(), timeoutMs);
1241
1805
  try {
1242
- const response = await fetch(url, { signal: controller.signal });
1806
+ const response = await screenedFetch(url, {
1807
+ signal: controller.signal,
1808
+ allowPrivateHosts
1809
+ });
1243
1810
  clearTimeout(timer);
1244
1811
  const contentLength = response.headers.get("content-length");
1245
1812
  if (contentLength && Number(contentLength) > MAX_BODY_BYTES) {
@@ -1271,6 +1838,9 @@ function createWebFetchTool(opts) {
1271
1838
  });
1272
1839
  } catch (err) {
1273
1840
  clearTimeout(timer);
1841
+ if (err instanceof SsrfBlockedError) {
1842
+ return JSON.stringify({ ok: false, error: "ssrf_blocked", url, reason: err.message });
1843
+ }
1274
1844
  const e = err;
1275
1845
  if (e.name === "AbortError") {
1276
1846
  return JSON.stringify({ ok: false, error: "timeout", url, timeout_ms: timeoutMs });
@@ -1314,6 +1884,34 @@ function createWebSearchTool(opts) {
1314
1884
  }
1315
1885
  });
1316
1886
  }
1887
+ var BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
1888
+ function createBraveWebSearchAdapter(opts = {}) {
1889
+ const apiKey = opts.apiKey ?? process.env.BRAVE_API_KEY;
1890
+ if (!apiKey) {
1891
+ throw new sdk.ConfigurationError("BRAVE_API_KEY is not set (pass { apiKey } or set the env var).", {
1892
+ code: "no_api_key"
1893
+ });
1894
+ }
1895
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
1896
+ const endpoint = opts.endpoint ?? BRAVE_ENDPOINT;
1897
+ const base = new URL(endpoint);
1898
+ return async (query, maxResults) => {
1899
+ const url = new URL(base);
1900
+ url.searchParams.set("q", query);
1901
+ url.searchParams.set("count", String(maxResults));
1902
+ const res = await fetchImpl(url.toString(), {
1903
+ headers: { "X-Subscription-Token": apiKey, Accept: "application/json" }
1904
+ });
1905
+ if (!res.ok) throw new Error(`brave_search_failed: HTTP ${res.status}`);
1906
+ const json = await res.json();
1907
+ const results = json?.web?.results ?? [];
1908
+ return results.map((r) => ({
1909
+ title: String(r?.title ?? ""),
1910
+ url: String(r?.url ?? ""),
1911
+ snippet: String(r?.description ?? "")
1912
+ }));
1913
+ };
1914
+ }
1317
1915
  var BINARY_PROBE_BYTES3 = 8 * 1024;
1318
1916
  function createWriteFileTool(opts) {
1319
1917
  const { projectRoot } = opts;
@@ -1356,8 +1954,8 @@ async function isBinaryFile(absolutePath) {
1356
1954
  return false;
1357
1955
  }
1358
1956
  try {
1359
- const stat = await handle.stat();
1360
- const probeLen = Math.min(BINARY_PROBE_BYTES3, Number(stat.size));
1957
+ const stat2 = await handle.stat();
1958
+ const probeLen = Math.min(BINARY_PROBE_BYTES3, Number(stat2.size));
1361
1959
  if (probeLen <= 0) return false;
1362
1960
  const probe = Buffer.alloc(probeLen);
1363
1961
  const { bytesRead } = await handle.read(probe, 0, probeLen, 0);
@@ -1370,7 +1968,15 @@ async function isBinaryFile(absolutePath) {
1370
1968
  }
1371
1969
  }
1372
1970
 
1971
+ exports.CatastrophicCommandError = CatastrophicCommandError;
1972
+ exports.DEFAULT_TOOL_GUIDANCE = DEFAULT_TOOL_GUIDANCE;
1973
+ exports.SsrfBlockedError = SsrfBlockedError;
1974
+ exports.buildEnvContext = buildEnvContext;
1975
+ exports.buildRepoMap = buildRepoMap;
1976
+ exports.catastrophicShellReason = catastrophicShellReason;
1977
+ exports.commandDenialReason = commandDenialReason;
1373
1978
  exports.createApplyPatchTool = createApplyPatchTool;
1979
+ exports.createBraveWebSearchAdapter = createBraveWebSearchAdapter;
1374
1980
  exports.createEditFileTool = createEditFileTool;
1375
1981
  exports.createGitDiffTool = createGitDiffTool;
1376
1982
  exports.createGlobTool = createGlobTool;
@@ -1380,15 +1986,27 @@ exports.createQuestionTool = createQuestionTool;
1380
1986
  exports.createReadFileTool = createReadFileTool;
1381
1987
  exports.createRunVitestTool = createRunVitestTool;
1382
1988
  exports.createSearchTextTool = createSearchTextTool;
1989
+ exports.createSessionArtifactStore = createSessionArtifactStore;
1383
1990
  exports.createShellTool = createShellTool;
1384
1991
  exports.createTodolistTool = createTodolistTool;
1385
1992
  exports.createWebFetchTool = createWebFetchTool;
1386
1993
  exports.createWebSearchTool = createWebSearchTool;
1387
1994
  exports.createWriteFileTool = createWriteFileTool;
1995
+ exports.denyCatastrophicCommands = denyCatastrophicCommands;
1388
1996
  exports.formatCode = formatCode;
1389
1997
  exports.formatDiff = formatDiff;
1390
1998
  exports.formatError = formatError;
1391
1999
  exports.formatFileList = formatFileList;
2000
+ exports.injectGuidance = injectGuidance;
2001
+ exports.isBlockedIp = isBlockedIp;
2002
+ exports.isCommandAllowed = isCommandAllowed;
2003
+ exports.renderToolList = renderToolList;
2004
+ exports.resolveAndScreen = resolveAndScreen;
2005
+ exports.screenedFetch = screenedFetch;
2006
+ exports.todoItemsToPlanNodes = todoItemsToPlanNodes;
1392
2007
  exports.truncateOutput = truncateOutput;
2008
+ exports.withDefaultGuidance = withDefaultGuidance;
2009
+ exports.withDescription = withDescription;
2010
+ exports.withToolResultGuidance = withToolResultGuidance;
1393
2011
  //# sourceMappingURL=index.cjs.map
1394
2012
  //# sourceMappingURL=index.cjs.map