@theokit/sdk-tools 0.1.0 → 0.3.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 +85 -0
- package/README.md +2 -0
- package/dist/index.cjs +634 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +362 -4
- package/dist/index.d.ts +362 -4
- package/dist/index.js +617 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
61
|
-
if (
|
|
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);
|
|
@@ -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,439 @@ 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
|
+
function unquote(token) {
|
|
605
|
+
return token.replace(/^(['"])(.*)\1$/, "$2").replace(/^['"]|['"]$/g, "");
|
|
606
|
+
}
|
|
607
|
+
function commandSegments(command) {
|
|
608
|
+
return command.split(/&&|\|\||[;|&\n]/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
609
|
+
}
|
|
610
|
+
function commandArgs(segment, name) {
|
|
611
|
+
const tokens = segment.split(/\s+/);
|
|
612
|
+
let i = 0;
|
|
613
|
+
if (tokens[i] === "sudo") i += 1;
|
|
614
|
+
if (tokens[i] !== name) return null;
|
|
615
|
+
return tokens.slice(i + 1);
|
|
616
|
+
}
|
|
617
|
+
function commandSegmentsNamed(command, name) {
|
|
618
|
+
return commandSegments(command).map((s) => commandArgs(s, name)).filter((args) => args !== null);
|
|
619
|
+
}
|
|
620
|
+
function isRecursiveForce(args) {
|
|
621
|
+
const flags = args.filter((t) => t.startsWith("-")).join(" ");
|
|
622
|
+
if (flags.length === 0) return false;
|
|
623
|
+
const recursive = /-[a-z]*r/i.test(flags) || /--recursive/.test(flags);
|
|
624
|
+
const force = /-[a-z]*f/i.test(flags) || /--force/.test(flags);
|
|
625
|
+
return recursive && force;
|
|
626
|
+
}
|
|
627
|
+
function isRecursive(args) {
|
|
628
|
+
const flags = args.filter((t) => t.startsWith("-")).join(" ");
|
|
629
|
+
return /-[a-z]*r/i.test(flags) || /--recursive/.test(flags);
|
|
630
|
+
}
|
|
631
|
+
var SAFE_ABSOLUTE_TARGET = /^\/(tmp|var\/tmp)(\/|$)/;
|
|
632
|
+
function targetsDangerousPath(args) {
|
|
633
|
+
const targets = args.filter((token) => token.length > 0 && !token.startsWith("-")).map(unquote);
|
|
634
|
+
return targets.some((raw) => {
|
|
635
|
+
const t = raw.replace(/\/+/g, "/");
|
|
636
|
+
if (t === "/dev/null" || SAFE_ABSOLUTE_TARGET.test(t)) return false;
|
|
637
|
+
return /^\/($|\*)/.test(t) || // "/" or "/*"
|
|
638
|
+
/^\/[^/]/.test(t) || // an absolute path like /etc, /usr/local, /home/user/x
|
|
639
|
+
t === "~" || t.startsWith("~/") || /\$\{?HOME\b\}?/.test(t) || // $HOME or ${HOME}
|
|
640
|
+
t === ".." || t.startsWith("../") || t.includes("/..") || t === "*";
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
var DEVICE_WIPE = [
|
|
644
|
+
/\bmkfs(\.\w+)?\b/,
|
|
645
|
+
/\bdd\b[^\n]*\bof=\/dev\//,
|
|
646
|
+
/\btruncate\b[^\n]*\s\/dev\//,
|
|
647
|
+
/>\s*\/dev\/(sd|nvme|hd|vd|mmcblk|disk|loop|dm-)/
|
|
648
|
+
];
|
|
649
|
+
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(
|
|
650
|
+
cmd
|
|
651
|
+
) || /(\$\(|<\()\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;
|
|
652
|
+
var checkDeviceWipe = (cmd) => DEVICE_WIPE.some((re) => re.test(cmd)) ? "writes to a raw block device / formats a disk" : null;
|
|
653
|
+
var checkForkBomb = (cmd) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(cmd) ? "fork bomb" : null;
|
|
654
|
+
var checkDestructiveGit = (cmd) => {
|
|
655
|
+
if (/\bgit\b[^\n]*\bpush\b[^\n]*(--force(?!-with-lease)\b|\s-f\b|\s\+\S)/.test(cmd)) {
|
|
656
|
+
return "git force-push (overwrites remote history)";
|
|
657
|
+
}
|
|
658
|
+
if (/\bgit\b[^\n]*\breset\b[^\n]*--hard\b/.test(cmd)) {
|
|
659
|
+
return "git reset --hard (discards committed and working changes)";
|
|
660
|
+
}
|
|
661
|
+
if (/\bgit\b[^\n]*\bclean\b[^\n]*(-[a-z]*f[a-z]*d|-[a-z]*d[a-z]*f)/.test(cmd)) {
|
|
662
|
+
return "git clean -fd (permanently deletes untracked files)";
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
};
|
|
666
|
+
var checkRm = (cmd) => commandSegmentsNamed(cmd, "rm").some((a) => isRecursiveForce(a) && targetsDangerousPath(a)) ? "recursive force-delete of an absolute, home, or parent path" : null;
|
|
667
|
+
var checkPerm = (cmd) => ["chmod", "chown"].some(
|
|
668
|
+
(name) => commandSegmentsNamed(cmd, name).some((a) => isRecursive(a) && targetsDangerousPath(a))
|
|
669
|
+
) ? "recursive permission change on an absolute, home, or parent path" : null;
|
|
670
|
+
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;
|
|
671
|
+
var checkExfiltration = (cmd) => {
|
|
672
|
+
const touchesSecret = /(^|[\s/'"])(\.env(\.\w+)?|id_rsa|id_ed25519|\.ssh(\/|\b)|credentials|\.aws(\/|\b)|\.npmrc)\b/.test(
|
|
673
|
+
cmd
|
|
674
|
+
);
|
|
675
|
+
const sendsNetwork = /\b(curl|wget|nc|netcat|scp|ftp|telnet)\b/.test(cmd) || /\bpython[0-9.]*\s+-m\s+http/.test(cmd);
|
|
676
|
+
return touchesSecret && sendsNetwork ? "sends a secret/credential file over the network (exfiltration)" : null;
|
|
677
|
+
};
|
|
678
|
+
var CATEGORY_CHECKS = [
|
|
679
|
+
checkRemoteExec,
|
|
680
|
+
checkDeviceWipe,
|
|
681
|
+
checkForkBomb,
|
|
682
|
+
checkDestructiveGit,
|
|
683
|
+
checkRm,
|
|
684
|
+
checkPerm,
|
|
685
|
+
checkFind,
|
|
686
|
+
checkExfiltration
|
|
687
|
+
];
|
|
688
|
+
function catastrophicShellReason(command) {
|
|
689
|
+
const cmd = command.trim();
|
|
690
|
+
if (cmd.length === 0) return null;
|
|
691
|
+
for (const check of CATEGORY_CHECKS) {
|
|
692
|
+
const reason = check(cmd);
|
|
693
|
+
if (reason) return reason;
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/internal/command-policy.ts
|
|
699
|
+
function denyCatastrophicCommands() {
|
|
700
|
+
return (command) => catastrophicShellReason(command);
|
|
701
|
+
}
|
|
702
|
+
function commandDenialReason(command, policies) {
|
|
703
|
+
for (const policy of policies) {
|
|
704
|
+
const reason = policy(command);
|
|
705
|
+
if (reason !== null) return reason;
|
|
706
|
+
}
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
function isCommandAllowed(command, policies) {
|
|
710
|
+
return commandDenialReason(command, policies) === null;
|
|
711
|
+
}
|
|
712
|
+
var SsrfBlockedError = class extends sdk.ConfigurationError {
|
|
713
|
+
name = "SsrfBlockedError";
|
|
714
|
+
constructor(host, detail) {
|
|
715
|
+
super(
|
|
716
|
+
`Blocked request to "${host}"${detail ? ` (${detail})` : ""}: address is private, loopback, link-local, or reserved (SSRF guard).`,
|
|
717
|
+
{ code: "ssrf_blocked" }
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
function v4ToInt(ip) {
|
|
722
|
+
const parts = ip.split(".");
|
|
723
|
+
return (Number(parts[0]) << 24 | Number(parts[1]) << 16 | Number(parts[2]) << 8 | Number(parts[3])) >>> 0;
|
|
724
|
+
}
|
|
725
|
+
function inV4Cidr(ipInt, base, prefix) {
|
|
726
|
+
const mask = prefix === 0 ? 0 : 4294967295 << 32 - prefix >>> 0;
|
|
727
|
+
return (ipInt & mask) === (v4ToInt(base) & mask);
|
|
728
|
+
}
|
|
729
|
+
var V4_BLOCKED = [
|
|
730
|
+
["0.0.0.0", 8],
|
|
731
|
+
// "this host"
|
|
732
|
+
["10.0.0.0", 8],
|
|
733
|
+
// private
|
|
734
|
+
["100.64.0.0", 10],
|
|
735
|
+
// CGNAT
|
|
736
|
+
["127.0.0.0", 8],
|
|
737
|
+
// loopback
|
|
738
|
+
["169.254.0.0", 16],
|
|
739
|
+
// link-local + cloud metadata
|
|
740
|
+
["172.16.0.0", 12],
|
|
741
|
+
// private
|
|
742
|
+
["192.168.0.0", 16],
|
|
743
|
+
// private
|
|
744
|
+
["224.0.0.0", 4],
|
|
745
|
+
// multicast
|
|
746
|
+
["240.0.0.0", 4]
|
|
747
|
+
// reserved
|
|
748
|
+
];
|
|
749
|
+
function isBlockedV4(ip) {
|
|
750
|
+
const n = v4ToInt(ip);
|
|
751
|
+
return V4_BLOCKED.some(([base, prefix]) => inV4Cidr(n, base, prefix));
|
|
752
|
+
}
|
|
753
|
+
function foldDottedTail(s) {
|
|
754
|
+
const lastColon = s.lastIndexOf(":");
|
|
755
|
+
const tail = s.slice(lastColon + 1);
|
|
756
|
+
if (!tail.includes(".")) return s;
|
|
757
|
+
if (net.isIP(tail) !== 4) return null;
|
|
758
|
+
const o = tail.split(".").map(Number);
|
|
759
|
+
const hi = (o[0] << 8 | o[1]).toString(16);
|
|
760
|
+
const lo = (o[2] << 8 | o[3]).toString(16);
|
|
761
|
+
return `${s.slice(0, lastColon + 1)}${hi}:${lo}`;
|
|
762
|
+
}
|
|
763
|
+
function expandHextets(s) {
|
|
764
|
+
const halves = s.split("::");
|
|
765
|
+
if (halves.length > 2) return null;
|
|
766
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
767
|
+
const tailGroups = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
768
|
+
if (halves.length === 1) return head.length === 8 ? head : null;
|
|
769
|
+
const missing = 8 - head.length - tailGroups.length;
|
|
770
|
+
if (missing < 0) return null;
|
|
771
|
+
return [...head, ...Array(missing).fill("0"), ...tailGroups];
|
|
772
|
+
}
|
|
773
|
+
function ipv6ToBytes(ip) {
|
|
774
|
+
const folded = foldDottedTail(ip.toLowerCase().split("%")[0] ?? "");
|
|
775
|
+
if (folded === null) return null;
|
|
776
|
+
const groups = expandHextets(folded);
|
|
777
|
+
if (groups === null || groups.length !== 8) return null;
|
|
778
|
+
const bytes = [];
|
|
779
|
+
for (const g of groups) {
|
|
780
|
+
const n = Number.parseInt(g || "0", 16);
|
|
781
|
+
bytes.push(n >> 8, n & 255);
|
|
782
|
+
}
|
|
783
|
+
return bytes;
|
|
784
|
+
}
|
|
785
|
+
function allZero(bytes, from, to) {
|
|
786
|
+
for (let i = from; i < to; i += 1) if (bytes[i] !== 0) return false;
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
function isBlockedV6Bytes(b) {
|
|
790
|
+
if (allZero(b, 0, 10) && b[10] === 255 && b[11] === 255) {
|
|
791
|
+
return isBlockedV4(`${b[12]}.${b[13]}.${b[14]}.${b[15]}`);
|
|
792
|
+
}
|
|
793
|
+
if (allZero(b, 0, 15) && (b[15] === 1 || b[15] === 0)) return true;
|
|
794
|
+
if (allZero(b, 0, 12)) return isBlockedV4(`${b[12]}.${b[13]}.${b[14]}.${b[15]}`);
|
|
795
|
+
if (b[0] === 254 && (b[1] & 192) === 128) return true;
|
|
796
|
+
return (b[0] & 254) === 252;
|
|
797
|
+
}
|
|
798
|
+
function isBlockedIp(ip) {
|
|
799
|
+
const fam = net.isIP(ip);
|
|
800
|
+
if (fam === 4) return isBlockedV4(ip);
|
|
801
|
+
if (fam === 6) {
|
|
802
|
+
const bytes = ipv6ToBytes(ip);
|
|
803
|
+
return bytes === null ? true : isBlockedV6Bytes(bytes);
|
|
804
|
+
}
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
async function resolveAndScreen(rawHost, options = {}) {
|
|
808
|
+
const host = rawHost.replace(/^\[|\]$/g, "");
|
|
809
|
+
if (net.isIP(host) !== 0) {
|
|
810
|
+
if (isBlockedIp(host)) throw new SsrfBlockedError(host);
|
|
811
|
+
return [host];
|
|
812
|
+
}
|
|
813
|
+
const lookup = options.lookup ?? promises$1.lookup;
|
|
814
|
+
const addrs = await lookup(host, { all: true });
|
|
815
|
+
if (addrs.length === 0) throw new SsrfBlockedError(host, "no addresses");
|
|
816
|
+
for (const a of addrs) {
|
|
817
|
+
if (isBlockedIp(a.address)) throw new SsrfBlockedError(host, a.address);
|
|
818
|
+
}
|
|
819
|
+
return addrs.map((a) => a.address);
|
|
820
|
+
}
|
|
821
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
822
|
+
function redirectTarget(res, current, originalUrl) {
|
|
823
|
+
const location = res.headers.get("location");
|
|
824
|
+
if (!REDIRECT_STATUSES.has(res.status) || !location) return void 0;
|
|
825
|
+
const next = new URL(location, current);
|
|
826
|
+
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
827
|
+
throw new SsrfBlockedError(originalUrl, `non-http redirect to ${next.protocol}`);
|
|
828
|
+
}
|
|
829
|
+
return next.href;
|
|
830
|
+
}
|
|
831
|
+
async function screenedFetch(url, options = {}) {
|
|
832
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
833
|
+
const maxRedirects = options.maxRedirects ?? 5;
|
|
834
|
+
let current = url;
|
|
835
|
+
for (let hop = 0; hop <= maxRedirects; hop += 1) {
|
|
836
|
+
if (!options.allowPrivateHosts) {
|
|
837
|
+
await resolveAndScreen(new URL(current).hostname, { lookup: options.lookup });
|
|
838
|
+
}
|
|
839
|
+
const res = await fetchImpl(current, { redirect: "manual", signal: options.signal });
|
|
840
|
+
const next = redirectTarget(res, current, url);
|
|
841
|
+
if (next === void 0) return res;
|
|
842
|
+
current = next;
|
|
843
|
+
}
|
|
844
|
+
throw new SsrfBlockedError(url, "too many redirects");
|
|
845
|
+
}
|
|
846
|
+
var DEFAULT_BUDGET = 8e3;
|
|
847
|
+
var DEFAULT_MAX_DEPTH = 4;
|
|
848
|
+
var PER_DIR_CAP = 200;
|
|
849
|
+
var TRUNCATED_MARKER = "\u2026 (truncated)";
|
|
850
|
+
var DOC_HEAD_CHARS = 200;
|
|
851
|
+
var DEFAULT_REPO_MAP_IGNORE = [
|
|
852
|
+
"node_modules",
|
|
853
|
+
".git",
|
|
854
|
+
"dist",
|
|
855
|
+
".theo",
|
|
856
|
+
".next",
|
|
857
|
+
"build",
|
|
858
|
+
"coverage",
|
|
859
|
+
"target",
|
|
860
|
+
"out"
|
|
861
|
+
];
|
|
862
|
+
var PROJECT_DOCS = ["AGENTS.md", "CLAUDE.md", "README.md"];
|
|
863
|
+
var MANIFESTS = ["package.json", "pyproject.toml", "Cargo.toml", "go.mod"];
|
|
864
|
+
function safeExists(p) {
|
|
865
|
+
try {
|
|
866
|
+
return fs.existsSync(p);
|
|
867
|
+
} catch {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function safeReadHead(p, n) {
|
|
872
|
+
try {
|
|
873
|
+
return fs.readFileSync(p, "utf-8").slice(0, n).replace(/\s+/g, " ").trim();
|
|
874
|
+
} catch {
|
|
875
|
+
return "";
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function buildEnvContext(cwd) {
|
|
879
|
+
const lines = [
|
|
880
|
+
"<env>",
|
|
881
|
+
` Working directory: ${cwd}`,
|
|
882
|
+
` Platform: ${process.platform} (${process.arch})`,
|
|
883
|
+
` Node: ${process.version}`,
|
|
884
|
+
` Is git repo: ${safeExists(path.join(cwd, ".git")) ? "yes" : "no"}`,
|
|
885
|
+
` Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}`
|
|
886
|
+
];
|
|
887
|
+
const docs = PROJECT_DOCS.filter((d) => safeExists(path.join(cwd, d)));
|
|
888
|
+
if (docs.length > 0) {
|
|
889
|
+
lines.push(` Project docs: ${docs.join(", ")}`);
|
|
890
|
+
const head = safeReadHead(path.join(cwd, docs[0]), DOC_HEAD_CHARS);
|
|
891
|
+
if (head) lines.push(` ${docs[0]} (head): ${head}`);
|
|
892
|
+
}
|
|
893
|
+
const manifests = MANIFESTS.filter((m) => safeExists(path.join(cwd, m)));
|
|
894
|
+
if (manifests.length > 0) lines.push(` Manifests: ${manifests.join(", ")}`);
|
|
895
|
+
lines.push("</env>");
|
|
896
|
+
return lines.join("\n");
|
|
897
|
+
}
|
|
898
|
+
function compareEntries(a, b) {
|
|
899
|
+
const ad = a.isDirectory() ? 0 : 1;
|
|
900
|
+
const bd = b.isDirectory() ? 0 : 1;
|
|
901
|
+
return ad !== bd ? ad - bd : a.name.localeCompare(b.name);
|
|
902
|
+
}
|
|
903
|
+
function visibleEntries(dir, ignore) {
|
|
904
|
+
let entries;
|
|
905
|
+
try {
|
|
906
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
907
|
+
} catch {
|
|
908
|
+
return [];
|
|
909
|
+
}
|
|
910
|
+
return entries.filter((e) => !ignore.has(e.name) && !e.name.startsWith(".")).sort(compareEntries);
|
|
911
|
+
}
|
|
912
|
+
function pushLine(ctx, line) {
|
|
913
|
+
if (ctx.used + line.length + 1 > ctx.budget) {
|
|
914
|
+
ctx.truncated = true;
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
ctx.lines.push(line);
|
|
918
|
+
ctx.used += line.length + 1;
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
function emitEntry(ctx, dir, e, depth, indent) {
|
|
922
|
+
const isDir = e.isDirectory();
|
|
923
|
+
if (!pushLine(ctx, `${indent}${isDir ? `${e.name}/` : e.name}`)) return false;
|
|
924
|
+
if (isDir && depth + 1 < ctx.maxDepth) return walkDir2(ctx, path.join(dir, e.name), depth + 1);
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
function walkDir2(ctx, dir, depth) {
|
|
928
|
+
const entries = visibleEntries(dir, ctx.ignore);
|
|
929
|
+
const shown = entries.slice(0, PER_DIR_CAP);
|
|
930
|
+
const indent = " ".repeat(depth);
|
|
931
|
+
for (const e of shown) {
|
|
932
|
+
if (!emitEntry(ctx, dir, e, depth, indent)) return false;
|
|
933
|
+
}
|
|
934
|
+
if (entries.length > shown.length) {
|
|
935
|
+
return pushLine(ctx, `${indent}\u2026 (${entries.length - shown.length} more)`);
|
|
936
|
+
}
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
function buildRepoMap(cwd, opts = {}) {
|
|
940
|
+
try {
|
|
941
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
942
|
+
return `(unavailable: ${cwd} is not a readable directory)`;
|
|
943
|
+
}
|
|
944
|
+
} catch {
|
|
945
|
+
return `(unavailable: ${cwd})`;
|
|
946
|
+
}
|
|
947
|
+
const ctx = {
|
|
948
|
+
ignore: /* @__PURE__ */ new Set([...DEFAULT_REPO_MAP_IGNORE, ...opts.ignore ?? []]),
|
|
949
|
+
maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
950
|
+
budget: opts.budget ?? DEFAULT_BUDGET,
|
|
951
|
+
lines: [],
|
|
952
|
+
used: 0,
|
|
953
|
+
truncated: false
|
|
954
|
+
};
|
|
955
|
+
walkDir2(ctx, cwd, 0);
|
|
956
|
+
return ctx.truncated ? `${ctx.lines.join("\n")}
|
|
957
|
+
${TRUNCATED_MARKER}` : ctx.lines.join("\n");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/internal/tool-aci.ts
|
|
961
|
+
function withDescription(tool, description) {
|
|
962
|
+
return {
|
|
963
|
+
name: tool.name,
|
|
964
|
+
description,
|
|
965
|
+
inputSchema: tool.inputSchema,
|
|
966
|
+
handler: tool.handler
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function esc(s) {
|
|
970
|
+
return String(s).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
971
|
+
}
|
|
972
|
+
function renderToolList(tools) {
|
|
973
|
+
if (tools.length === 0) return "<tools></tools>";
|
|
974
|
+
const lines = ["<tools>"];
|
|
975
|
+
for (const t of tools) {
|
|
976
|
+
lines.push(
|
|
977
|
+
" <tool>",
|
|
978
|
+
` <name>${esc(t.name)}</name>`,
|
|
979
|
+
` <description>${esc(t.description)}</description>`,
|
|
980
|
+
" </tool>"
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
lines.push("</tools>");
|
|
984
|
+
return lines.join("\n");
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/internal/tool-guidance.ts
|
|
988
|
+
var DEFAULT_TOOL_GUIDANCE = {
|
|
989
|
+
not_found: "The path does not exist. Use `list_dir` or `glob_files` to find the correct path, then retry.",
|
|
990
|
+
path_traversal: "That path escapes the project root. Use a path inside the project directory.",
|
|
991
|
+
forbidden_path: "That path is a protected file (.env, .git, lock files, etc.). Choose a different, non-sensitive path.",
|
|
992
|
+
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.",
|
|
993
|
+
timeout: "The operation timed out. Narrow the scope or pass a larger `timeout_ms`.",
|
|
994
|
+
invalid_url: "The URL is malformed. Provide a full absolute http(s):// URL.",
|
|
995
|
+
ssrf_blocked: "That host is private/loopback/reserved and is blocked. Use a public URL.",
|
|
996
|
+
catastrophic_command: "That command is blocked as catastrophic. Use a safer, scoped command (e.g. a relative path instead of `/`).",
|
|
997
|
+
binary_file: "The file is binary and cannot be read as text. Use a tool suited to binary data.",
|
|
998
|
+
too_large: "The target is too large to process directly. Use a bounded range or a narrower scope."
|
|
999
|
+
};
|
|
1000
|
+
function isRecord(value) {
|
|
1001
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1002
|
+
}
|
|
1003
|
+
function injectGuidance(handlerOutput, guidance) {
|
|
1004
|
+
let parsed;
|
|
1005
|
+
try {
|
|
1006
|
+
parsed = JSON.parse(handlerOutput);
|
|
1007
|
+
} catch {
|
|
1008
|
+
return handlerOutput;
|
|
1009
|
+
}
|
|
1010
|
+
if (!isRecord(parsed) || parsed.ok !== false) return handlerOutput;
|
|
1011
|
+
if ("guidance" in parsed) return handlerOutput;
|
|
1012
|
+
const code = parsed.error;
|
|
1013
|
+
if (typeof code !== "string") return handlerOutput;
|
|
1014
|
+
const hint = guidance[code];
|
|
1015
|
+
if (!hint) return handlerOutput;
|
|
1016
|
+
return JSON.stringify({ ...parsed, guidance: hint });
|
|
1017
|
+
}
|
|
1018
|
+
function withToolResultGuidance(tool, guidance) {
|
|
1019
|
+
return {
|
|
1020
|
+
name: tool.name,
|
|
1021
|
+
description: tool.description,
|
|
1022
|
+
inputSchema: tool.inputSchema,
|
|
1023
|
+
handler: async (input) => injectGuidance(await tool.handler(input), guidance)
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function withDefaultGuidance(tool) {
|
|
1027
|
+
return withToolResultGuidance(tool, DEFAULT_TOOL_GUIDANCE);
|
|
1028
|
+
}
|
|
553
1029
|
var DEFAULT_MAX_ENTRIES = 500;
|
|
554
1030
|
function createListDirTool(opts) {
|
|
555
1031
|
const { projectRoot, max = DEFAULT_MAX_ENTRIES } = opts;
|
|
@@ -615,39 +1091,82 @@ var PLAN_INSTRUCTIONS = [
|
|
|
615
1091
|
"When ready, use plan_mode with action 'exit' to return to normal mode."
|
|
616
1092
|
].join("\n");
|
|
617
1093
|
var NORMAL_INSTRUCTIONS = "Returned to NORMAL MODE. You may now execute changes.";
|
|
618
|
-
|
|
1094
|
+
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 }.";
|
|
1095
|
+
function planModeSchema(withPlan) {
|
|
1096
|
+
const properties = {
|
|
1097
|
+
action: {
|
|
1098
|
+
type: "string",
|
|
1099
|
+
enum: ["enter", "exit", "status"],
|
|
1100
|
+
description: "The action to perform."
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
if (withPlan) {
|
|
1104
|
+
properties.plan = {
|
|
1105
|
+
type: "string",
|
|
1106
|
+
description: "On 'exit', the plan text to persist to the artifact store."
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
return { type: "object", properties, required: ["action"] };
|
|
1110
|
+
}
|
|
1111
|
+
function renderMode(action, mode) {
|
|
1112
|
+
switch (action) {
|
|
1113
|
+
case "enter":
|
|
1114
|
+
return JSON.stringify({ ok: true, mode, message: PLAN_INSTRUCTIONS });
|
|
1115
|
+
case "exit":
|
|
1116
|
+
return JSON.stringify({ ok: true, mode, message: NORMAL_INSTRUCTIONS });
|
|
1117
|
+
case "status":
|
|
1118
|
+
return JSON.stringify({ ok: true, mode, message: `Current mode: ${mode}` });
|
|
1119
|
+
default:
|
|
1120
|
+
return void 0;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function invalidAction(action) {
|
|
1124
|
+
return JSON.stringify({
|
|
1125
|
+
ok: false,
|
|
1126
|
+
error: "invalid_action",
|
|
1127
|
+
message: `Unknown action '${action}'. Valid: enter, exit, status.`
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
function createPlanModeTool(options) {
|
|
619
1131
|
let mode = "normal";
|
|
1132
|
+
if (options === void 0) {
|
|
1133
|
+
return {
|
|
1134
|
+
name: "plan_mode",
|
|
1135
|
+
description: DESCRIPTION,
|
|
1136
|
+
inputSchema: planModeSchema(false),
|
|
1137
|
+
handler: (input) => {
|
|
1138
|
+
if (input.action === "enter") mode = "plan";
|
|
1139
|
+
else if (input.action === "exit") mode = "normal";
|
|
1140
|
+
return renderMode(input.action, mode) ?? invalidAction(input.action);
|
|
1141
|
+
},
|
|
1142
|
+
currentMode: () => mode
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const { artifactStore, artifactId = "plan" } = options;
|
|
620
1146
|
return {
|
|
621
1147
|
name: "plan_mode",
|
|
622
|
-
description:
|
|
623
|
-
inputSchema:
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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:
|
|
1148
|
+
description: DESCRIPTION,
|
|
1149
|
+
inputSchema: planModeSchema(true),
|
|
1150
|
+
handler: async (input) => {
|
|
1151
|
+
if (input.action === "enter") {
|
|
1152
|
+
mode = "plan";
|
|
1153
|
+
return JSON.stringify({ ok: true, mode, message: PLAN_INSTRUCTIONS });
|
|
1154
|
+
}
|
|
1155
|
+
if (input.action === "exit") {
|
|
1156
|
+
mode = "normal";
|
|
1157
|
+
if (typeof input.plan === "string" && input.plan.length > 0) {
|
|
1158
|
+
const path = await artifactStore.write(artifactId, input.plan);
|
|
645
1159
|
return JSON.stringify({
|
|
646
|
-
ok:
|
|
647
|
-
|
|
648
|
-
message:
|
|
1160
|
+
ok: true,
|
|
1161
|
+
mode,
|
|
1162
|
+
message: NORMAL_INSTRUCTIONS,
|
|
1163
|
+
persisted: true,
|
|
1164
|
+
path
|
|
649
1165
|
});
|
|
1166
|
+
}
|
|
1167
|
+
return JSON.stringify({ ok: true, mode, message: NORMAL_INSTRUCTIONS, persisted: false });
|
|
650
1168
|
}
|
|
1169
|
+
return renderMode(input.action, mode) ?? invalidAction(input.action);
|
|
651
1170
|
},
|
|
652
1171
|
currentMode: () => mode
|
|
653
1172
|
};
|
|
@@ -737,21 +1256,21 @@ async function openHandleSafe(absolutePath, path) {
|
|
|
737
1256
|
}
|
|
738
1257
|
}
|
|
739
1258
|
async function readContent(handle, path) {
|
|
740
|
-
const
|
|
741
|
-
if (
|
|
1259
|
+
const stat2 = await handle.stat();
|
|
1260
|
+
if (stat2.size > MAX_FILE_SIZE) {
|
|
742
1261
|
return JSON.stringify({
|
|
743
1262
|
ok: false,
|
|
744
1263
|
error: "too_large",
|
|
745
1264
|
path,
|
|
746
|
-
size:
|
|
1265
|
+
size: stat2.size,
|
|
747
1266
|
limit: MAX_FILE_SIZE
|
|
748
1267
|
});
|
|
749
1268
|
}
|
|
750
|
-
if (await isBinaryProbe(handle, Number(
|
|
751
|
-
return JSON.stringify({ ok: false, error: "binary_file", path, size:
|
|
1269
|
+
if (await isBinaryProbe(handle, Number(stat2.size))) {
|
|
1270
|
+
return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
|
|
752
1271
|
}
|
|
753
1272
|
const content = await handle.readFile({ encoding: "utf-8" });
|
|
754
|
-
return JSON.stringify({ ok: true, content, size:
|
|
1273
|
+
return JSON.stringify({ ok: true, content, size: stat2.size });
|
|
755
1274
|
}
|
|
756
1275
|
async function isBinaryProbe(handle, size) {
|
|
757
1276
|
const probeLen = Math.min(BINARY_PROBE_BYTES, size);
|
|
@@ -990,7 +1509,7 @@ var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
|
990
1509
|
var MAX_TIMEOUT_MS = 3e5;
|
|
991
1510
|
var MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
992
1511
|
function createShellTool(opts) {
|
|
993
|
-
const { projectRoot, defaultTimeoutMs = DEFAULT_TIMEOUT_MS3 } = opts;
|
|
1512
|
+
const { projectRoot, defaultTimeoutMs = DEFAULT_TIMEOUT_MS3, allowCatastrophic = false } = opts;
|
|
994
1513
|
return sdk.defineTool({
|
|
995
1514
|
name: "shell_exec",
|
|
996
1515
|
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 +1518,13 @@ function createShellTool(opts) {
|
|
|
999
1518
|
timeout_ms: zod.z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
|
|
1000
1519
|
}),
|
|
1001
1520
|
handler: async ({ command, timeout_ms }) => {
|
|
1521
|
+
if (!allowCatastrophic) {
|
|
1522
|
+
const reason = catastrophicShellReason(command);
|
|
1523
|
+
if (reason) {
|
|
1524
|
+
const err = new CatastrophicCommandError(reason);
|
|
1525
|
+
return JSON.stringify({ ok: false, error: err.code, reason });
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1002
1528
|
const timeoutMs = Math.min(timeout_ms ?? defaultTimeoutMs, MAX_TIMEOUT_MS);
|
|
1003
1529
|
const result = await runShell(projectRoot, command, timeoutMs);
|
|
1004
1530
|
return result;
|
|
@@ -1077,6 +1603,11 @@ function formatResult(result, timeoutMs) {
|
|
|
1077
1603
|
});
|
|
1078
1604
|
}
|
|
1079
1605
|
|
|
1606
|
+
// src/todo-plan-nodes.ts
|
|
1607
|
+
function todoItemsToPlanNodes(items) {
|
|
1608
|
+
return items.map((item) => ({ id: item.id, label: item.title, status: item.status }));
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1080
1611
|
// src/todolist.ts
|
|
1081
1612
|
function ok(data) {
|
|
1082
1613
|
return JSON.stringify({ ok: true, ...data });
|
|
@@ -1097,6 +1628,9 @@ function createTodolistTool() {
|
|
|
1097
1628
|
function findById(id) {
|
|
1098
1629
|
return items.find((i) => i.id === id);
|
|
1099
1630
|
}
|
|
1631
|
+
function listResult(extra) {
|
|
1632
|
+
return ok({ ...extra, items: [...items], items_summary: formatList() });
|
|
1633
|
+
}
|
|
1100
1634
|
function formatList() {
|
|
1101
1635
|
if (items.length === 0) return "No tasks. Use action 'add' to create one.";
|
|
1102
1636
|
const lines = items.map((item) => {
|
|
@@ -1119,7 +1653,7 @@ ${done}/${items.length} done | ${inProg} in progress | ${pending} pending`);
|
|
|
1119
1653
|
createdAt: Date.now()
|
|
1120
1654
|
};
|
|
1121
1655
|
items.push(item);
|
|
1122
|
-
return
|
|
1656
|
+
return listResult({ id: item.id, message: `Added: ${item.title}` });
|
|
1123
1657
|
}
|
|
1124
1658
|
function handleSetStatus(input, status) {
|
|
1125
1659
|
const id = requireId(input);
|
|
@@ -1129,7 +1663,7 @@ ${done}/${items.length} done | ${inProg} in progress | ${pending} pending`);
|
|
|
1129
1663
|
item.status = status;
|
|
1130
1664
|
if (status === "done") item.completedAt = Date.now();
|
|
1131
1665
|
const verb = status === "done" ? "Completed" : "Started";
|
|
1132
|
-
return
|
|
1666
|
+
return listResult({ message: `${verb}: ${item.title}` });
|
|
1133
1667
|
}
|
|
1134
1668
|
function handleRemove(input) {
|
|
1135
1669
|
const id = requireId(input);
|
|
@@ -1137,29 +1671,26 @@ ${done}/${items.length} done | ${inProg} in progress | ${pending} pending`);
|
|
|
1137
1671
|
const idx = items.findIndex((i) => i.id === id);
|
|
1138
1672
|
if (idx === -1) return fail({ error: "not_found", id });
|
|
1139
1673
|
const removed = items.splice(idx, 1)[0];
|
|
1140
|
-
return
|
|
1674
|
+
return listResult({ message: `Removed: ${removed.title}` });
|
|
1141
1675
|
}
|
|
1142
1676
|
function handleClearCompleted() {
|
|
1143
1677
|
const before = items.length;
|
|
1144
1678
|
const kept = items.filter((i) => i.status !== "done");
|
|
1145
1679
|
items.length = 0;
|
|
1146
1680
|
items.push(...kept);
|
|
1147
|
-
return
|
|
1148
|
-
message: `Cleared ${before - items.length} completed items`,
|
|
1149
|
-
items_summary: formatList()
|
|
1150
|
-
});
|
|
1681
|
+
return listResult({ message: `Cleared ${before - items.length} completed items` });
|
|
1151
1682
|
}
|
|
1152
1683
|
const actions = {
|
|
1153
1684
|
add: handleAdd,
|
|
1154
1685
|
in_progress: (input) => handleSetStatus(input, "in_progress"),
|
|
1155
1686
|
complete: (input) => handleSetStatus(input, "done"),
|
|
1156
1687
|
remove: handleRemove,
|
|
1157
|
-
list: () =>
|
|
1688
|
+
list: () => listResult({}),
|
|
1158
1689
|
clear_completed: handleClearCompleted
|
|
1159
1690
|
};
|
|
1160
1691
|
return {
|
|
1161
1692
|
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 }.",
|
|
1693
|
+
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
1694
|
inputSchema: {
|
|
1164
1695
|
type: "object",
|
|
1165
1696
|
properties: {
|
|
@@ -1212,6 +1743,7 @@ var DEFAULT_TIMEOUT_MS4 = 3e4;
|
|
|
1212
1743
|
var MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
1213
1744
|
function createWebFetchTool(opts) {
|
|
1214
1745
|
const defaultTimeoutMs = opts?.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS4;
|
|
1746
|
+
const allowPrivateHosts = opts?.allowPrivateHosts ?? false;
|
|
1215
1747
|
return sdk.defineTool({
|
|
1216
1748
|
name: "web_fetch",
|
|
1217
1749
|
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 +1771,10 @@ function createWebFetchTool(opts) {
|
|
|
1239
1771
|
const controller = new AbortController();
|
|
1240
1772
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1241
1773
|
try {
|
|
1242
|
-
const response = await
|
|
1774
|
+
const response = await screenedFetch(url, {
|
|
1775
|
+
signal: controller.signal,
|
|
1776
|
+
allowPrivateHosts
|
|
1777
|
+
});
|
|
1243
1778
|
clearTimeout(timer);
|
|
1244
1779
|
const contentLength = response.headers.get("content-length");
|
|
1245
1780
|
if (contentLength && Number(contentLength) > MAX_BODY_BYTES) {
|
|
@@ -1271,6 +1806,9 @@ function createWebFetchTool(opts) {
|
|
|
1271
1806
|
});
|
|
1272
1807
|
} catch (err) {
|
|
1273
1808
|
clearTimeout(timer);
|
|
1809
|
+
if (err instanceof SsrfBlockedError) {
|
|
1810
|
+
return JSON.stringify({ ok: false, error: "ssrf_blocked", url, reason: err.message });
|
|
1811
|
+
}
|
|
1274
1812
|
const e = err;
|
|
1275
1813
|
if (e.name === "AbortError") {
|
|
1276
1814
|
return JSON.stringify({ ok: false, error: "timeout", url, timeout_ms: timeoutMs });
|
|
@@ -1314,6 +1852,34 @@ function createWebSearchTool(opts) {
|
|
|
1314
1852
|
}
|
|
1315
1853
|
});
|
|
1316
1854
|
}
|
|
1855
|
+
var BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
|
1856
|
+
function createBraveWebSearchAdapter(opts = {}) {
|
|
1857
|
+
const apiKey = opts.apiKey ?? process.env.BRAVE_API_KEY;
|
|
1858
|
+
if (!apiKey) {
|
|
1859
|
+
throw new sdk.ConfigurationError("BRAVE_API_KEY is not set (pass { apiKey } or set the env var).", {
|
|
1860
|
+
code: "no_api_key"
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
1864
|
+
const endpoint = opts.endpoint ?? BRAVE_ENDPOINT;
|
|
1865
|
+
const base = new URL(endpoint);
|
|
1866
|
+
return async (query, maxResults) => {
|
|
1867
|
+
const url = new URL(base);
|
|
1868
|
+
url.searchParams.set("q", query);
|
|
1869
|
+
url.searchParams.set("count", String(maxResults));
|
|
1870
|
+
const res = await fetchImpl(url.toString(), {
|
|
1871
|
+
headers: { "X-Subscription-Token": apiKey, Accept: "application/json" }
|
|
1872
|
+
});
|
|
1873
|
+
if (!res.ok) throw new Error(`brave_search_failed: HTTP ${res.status}`);
|
|
1874
|
+
const json = await res.json();
|
|
1875
|
+
const results = json?.web?.results ?? [];
|
|
1876
|
+
return results.map((r) => ({
|
|
1877
|
+
title: String(r?.title ?? ""),
|
|
1878
|
+
url: String(r?.url ?? ""),
|
|
1879
|
+
snippet: String(r?.description ?? "")
|
|
1880
|
+
}));
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1317
1883
|
var BINARY_PROBE_BYTES3 = 8 * 1024;
|
|
1318
1884
|
function createWriteFileTool(opts) {
|
|
1319
1885
|
const { projectRoot } = opts;
|
|
@@ -1356,8 +1922,8 @@ async function isBinaryFile(absolutePath) {
|
|
|
1356
1922
|
return false;
|
|
1357
1923
|
}
|
|
1358
1924
|
try {
|
|
1359
|
-
const
|
|
1360
|
-
const probeLen = Math.min(BINARY_PROBE_BYTES3, Number(
|
|
1925
|
+
const stat2 = await handle.stat();
|
|
1926
|
+
const probeLen = Math.min(BINARY_PROBE_BYTES3, Number(stat2.size));
|
|
1361
1927
|
if (probeLen <= 0) return false;
|
|
1362
1928
|
const probe = Buffer.alloc(probeLen);
|
|
1363
1929
|
const { bytesRead } = await handle.read(probe, 0, probeLen, 0);
|
|
@@ -1370,7 +1936,15 @@ async function isBinaryFile(absolutePath) {
|
|
|
1370
1936
|
}
|
|
1371
1937
|
}
|
|
1372
1938
|
|
|
1939
|
+
exports.CatastrophicCommandError = CatastrophicCommandError;
|
|
1940
|
+
exports.DEFAULT_TOOL_GUIDANCE = DEFAULT_TOOL_GUIDANCE;
|
|
1941
|
+
exports.SsrfBlockedError = SsrfBlockedError;
|
|
1942
|
+
exports.buildEnvContext = buildEnvContext;
|
|
1943
|
+
exports.buildRepoMap = buildRepoMap;
|
|
1944
|
+
exports.catastrophicShellReason = catastrophicShellReason;
|
|
1945
|
+
exports.commandDenialReason = commandDenialReason;
|
|
1373
1946
|
exports.createApplyPatchTool = createApplyPatchTool;
|
|
1947
|
+
exports.createBraveWebSearchAdapter = createBraveWebSearchAdapter;
|
|
1374
1948
|
exports.createEditFileTool = createEditFileTool;
|
|
1375
1949
|
exports.createGitDiffTool = createGitDiffTool;
|
|
1376
1950
|
exports.createGlobTool = createGlobTool;
|
|
@@ -1380,15 +1954,27 @@ exports.createQuestionTool = createQuestionTool;
|
|
|
1380
1954
|
exports.createReadFileTool = createReadFileTool;
|
|
1381
1955
|
exports.createRunVitestTool = createRunVitestTool;
|
|
1382
1956
|
exports.createSearchTextTool = createSearchTextTool;
|
|
1957
|
+
exports.createSessionArtifactStore = createSessionArtifactStore;
|
|
1383
1958
|
exports.createShellTool = createShellTool;
|
|
1384
1959
|
exports.createTodolistTool = createTodolistTool;
|
|
1385
1960
|
exports.createWebFetchTool = createWebFetchTool;
|
|
1386
1961
|
exports.createWebSearchTool = createWebSearchTool;
|
|
1387
1962
|
exports.createWriteFileTool = createWriteFileTool;
|
|
1963
|
+
exports.denyCatastrophicCommands = denyCatastrophicCommands;
|
|
1388
1964
|
exports.formatCode = formatCode;
|
|
1389
1965
|
exports.formatDiff = formatDiff;
|
|
1390
1966
|
exports.formatError = formatError;
|
|
1391
1967
|
exports.formatFileList = formatFileList;
|
|
1968
|
+
exports.injectGuidance = injectGuidance;
|
|
1969
|
+
exports.isBlockedIp = isBlockedIp;
|
|
1970
|
+
exports.isCommandAllowed = isCommandAllowed;
|
|
1971
|
+
exports.renderToolList = renderToolList;
|
|
1972
|
+
exports.resolveAndScreen = resolveAndScreen;
|
|
1973
|
+
exports.screenedFetch = screenedFetch;
|
|
1974
|
+
exports.todoItemsToPlanNodes = todoItemsToPlanNodes;
|
|
1392
1975
|
exports.truncateOutput = truncateOutput;
|
|
1976
|
+
exports.withDefaultGuidance = withDefaultGuidance;
|
|
1977
|
+
exports.withDescription = withDescription;
|
|
1978
|
+
exports.withToolResultGuidance = withToolResultGuidance;
|
|
1393
1979
|
//# sourceMappingURL=index.cjs.map
|
|
1394
1980
|
//# sourceMappingURL=index.cjs.map
|