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