@cryer/star-cli 0.3.2 → 0.3.3
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/README.md +2 -0
- package/README.zh-CN.md +317 -0
- package/dist/{chunk-BV4PLM6X.js → chunk-3L6QKJM4.js} +1350 -671
- package/dist/chunk-3L6QKJM4.js.map +1 -0
- package/dist/{chunk-LADQCYKL.js → chunk-B6P4VUBE.js} +16 -7
- package/dist/chunk-B6P4VUBE.js.map +1 -0
- package/dist/{loop-XKC5HQBT.js → loop-TQ7GFTTE.js} +3 -3
- package/dist/main.js +569 -177
- package/dist/main.js.map +1 -1
- package/dist/{stream-OY2MKWYE.js → stream-H4BYBVAZ.js} +22 -4
- package/dist/stream-H4BYBVAZ.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-BV4PLM6X.js.map +0 -1
- package/dist/chunk-LADQCYKL.js.map +0 -1
- package/dist/stream-OY2MKWYE.js.map +0 -1
- /package/dist/{loop-XKC5HQBT.js.map → loop-TQ7GFTTE.js.map} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
computeRetryDelayMs,
|
|
4
|
+
gitTreesDir,
|
|
4
5
|
isRetryableStreamError,
|
|
6
|
+
responseBodyOf,
|
|
5
7
|
starHome,
|
|
6
8
|
summarizeStreamError,
|
|
7
9
|
userMemoryPath
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-B6P4VUBE.js";
|
|
9
11
|
|
|
10
12
|
// src/agent/loop.ts
|
|
11
13
|
import { tool as aiTool, generateText as generateText3 } from "ai";
|
|
@@ -89,14 +91,18 @@ function serializeMessage(message) {
|
|
|
89
91
|
});
|
|
90
92
|
return `${message.role}: ${parts.join("\n")}`;
|
|
91
93
|
}
|
|
92
|
-
async function summarizeMessages(messages, model) {
|
|
94
|
+
async function summarizeMessages(messages, model, signal) {
|
|
93
95
|
const transcript = messages.map(serializeMessage).join("\n");
|
|
94
96
|
const { text } = await generateText({
|
|
95
97
|
model,
|
|
96
98
|
system: SUMMARY_SYSTEM_PROMPT,
|
|
97
99
|
prompt: `Summarize this conversation so far:
|
|
98
100
|
|
|
99
|
-
${transcript}
|
|
101
|
+
${transcript}`,
|
|
102
|
+
// A hung relay must not stall the whole turn on the summary: cap it at
|
|
103
|
+
// 60s and let the caller's abort (Esc) cut it short as well; the caller
|
|
104
|
+
// falls back to the truncation placeholder on any failure.
|
|
105
|
+
abortSignal: signal ? AbortSignal.any([signal, AbortSignal.timeout(6e4)]) : AbortSignal.timeout(6e4)
|
|
100
106
|
});
|
|
101
107
|
return text.trim();
|
|
102
108
|
}
|
|
@@ -172,6 +178,16 @@ function getGitSummary(cwd) {
|
|
|
172
178
|
const log = runGit(["log", "--oneline", "-3"], cwd) ?? "";
|
|
173
179
|
return { branch, dirtyCount, recentCommits: log === "" ? [] : log.split("\n") };
|
|
174
180
|
}
|
|
181
|
+
var GIT_SUMMARY_CACHE_TTL_MS = 15e3;
|
|
182
|
+
var gitSummaryCache = /* @__PURE__ */ new Map();
|
|
183
|
+
function getGitSummaryCached(cwd) {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const hit = gitSummaryCache.get(cwd);
|
|
186
|
+
if (hit && now - hit.at < GIT_SUMMARY_CACHE_TTL_MS) return hit.summary;
|
|
187
|
+
const summary = getGitSummary(cwd);
|
|
188
|
+
gitSummaryCache.set(cwd, { at: now, summary });
|
|
189
|
+
return summary;
|
|
190
|
+
}
|
|
175
191
|
function formatGitSummary(summary) {
|
|
176
192
|
const lines = [
|
|
177
193
|
"Git context for the working directory (refreshed each turn):",
|
|
@@ -332,7 +348,8 @@ var IMAGE_SIZE_KEYWORDS = /too[\s-]*(large|big)|exceeds?|dimensions?|resolution|
|
|
|
332
348
|
function isOversizedImageError(error) {
|
|
333
349
|
const status = error.statusCode;
|
|
334
350
|
if (typeof status !== "number" || status < 400 || status >= 500) return false;
|
|
335
|
-
|
|
351
|
+
const text = [error.message, responseBodyOf(error)].filter((part) => typeof part === "string").join("\n");
|
|
352
|
+
return /image/i.test(text) && IMAGE_SIZE_KEYWORDS.test(text);
|
|
336
353
|
}
|
|
337
354
|
function imagePartBytes(image) {
|
|
338
355
|
if (typeof image === "string") {
|
|
@@ -541,8 +558,50 @@ async function runHooks(event, hooks2, ctx) {
|
|
|
541
558
|
}
|
|
542
559
|
|
|
543
560
|
// src/permissions/gate.ts
|
|
561
|
+
import fs from "fs";
|
|
544
562
|
import path from "path";
|
|
545
563
|
|
|
564
|
+
// src/core/sensitive.ts
|
|
565
|
+
var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
|
|
566
|
+
".env",
|
|
567
|
+
"id_rsa",
|
|
568
|
+
"id_dsa",
|
|
569
|
+
"id_ecdsa",
|
|
570
|
+
"id_ed25519",
|
|
571
|
+
".npmrc",
|
|
572
|
+
".netrc",
|
|
573
|
+
".pgpass"
|
|
574
|
+
]);
|
|
575
|
+
var SENSITIVE_SUFFIXES = [".pem", ".key", ".p12", ".pfx"];
|
|
576
|
+
var SENSITIVE_ENV_EXEMPTIONS = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
|
|
577
|
+
function pathSegments(p) {
|
|
578
|
+
return p.replace(/\\/g, "/").split("/").filter(Boolean).map((s) => s.toLowerCase());
|
|
579
|
+
}
|
|
580
|
+
function isSensitivePath(p) {
|
|
581
|
+
const segments = pathSegments(p);
|
|
582
|
+
const base = segments[segments.length - 1] ?? "";
|
|
583
|
+
if (SENSITIVE_ENV_EXEMPTIONS.has(base)) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
if (base === ".env" || base.startsWith(".env.")) {
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
if (SENSITIVE_BASENAMES.has(base)) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
if (SENSITIVE_SUFFIXES.some((suffix) => base.endsWith(suffix))) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
const dir = segments[segments.length - 2] ?? "";
|
|
596
|
+
if (dir === ".aws" && base === "credentials") {
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
if (dir === ".kube" && base === "config") {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
546
605
|
// src/permissions/allow.ts
|
|
547
606
|
function parseAllowRule(rule) {
|
|
548
607
|
const match = /^([\w-]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
@@ -566,11 +625,32 @@ function matchAllowRule(rule, req) {
|
|
|
566
625
|
const target = requestTarget(req);
|
|
567
626
|
return target !== void 0 && globMatch(rule.pattern, target);
|
|
568
627
|
}
|
|
628
|
+
function splitCommandChain(command) {
|
|
629
|
+
if (command.includes("$(") || command.includes("`")) return null;
|
|
630
|
+
return command.split(/&&|\|\||[;|\n]/).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
631
|
+
}
|
|
632
|
+
function withCommand(req, command) {
|
|
633
|
+
return {
|
|
634
|
+
...req,
|
|
635
|
+
args: { ...req.args ?? {}, command }
|
|
636
|
+
};
|
|
637
|
+
}
|
|
569
638
|
function isAllowedByRules(rules, req) {
|
|
570
|
-
return matchesAnyRule(rules, req);
|
|
639
|
+
if (req.toolName !== "bash") return matchesAnyRule(rules, req);
|
|
640
|
+
const command = requestTarget(req);
|
|
641
|
+
if (command === void 0) return matchesAnyRule(rules, req);
|
|
642
|
+
const segments = splitCommandChain(command);
|
|
643
|
+
if (segments === null) return false;
|
|
644
|
+
if (segments.length === 0) return matchesAnyRule(rules, req);
|
|
645
|
+
return segments.every((segment) => matchesAnyRule(rules, withCommand(req, segment)));
|
|
571
646
|
}
|
|
572
647
|
function isDeniedByRules(rules, req) {
|
|
573
|
-
return matchesAnyRule(rules, req);
|
|
648
|
+
if (req.toolName !== "bash") return matchesAnyRule(rules, req);
|
|
649
|
+
const command = requestTarget(req);
|
|
650
|
+
if (command === void 0) return matchesAnyRule(rules, req);
|
|
651
|
+
const segments = splitCommandChain(command);
|
|
652
|
+
if (segments === null || segments.length === 0) return matchesAnyRule(rules, req);
|
|
653
|
+
return segments.some((segment) => matchesAnyRule(rules, withCommand(req, segment)));
|
|
574
654
|
}
|
|
575
655
|
function matchesAnyRule(rules, req) {
|
|
576
656
|
return rules.some((raw) => {
|
|
@@ -584,10 +664,17 @@ function buildAllowRule(req) {
|
|
|
584
664
|
}
|
|
585
665
|
|
|
586
666
|
// src/permissions/gate.ts
|
|
587
|
-
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file"]);
|
|
667
|
+
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file", "grep", "glob"]);
|
|
588
668
|
var DANGEROUS_COMMAND_PATTERNS = [
|
|
589
669
|
/rm\s+-rf\s+\/(?:\s|$|;|&)/i,
|
|
590
670
|
/rm\s+-rf\s+~(?:\s|\/|$|;|&)/i,
|
|
671
|
+
/rm\s+-rf\s+\.\/?(?:\s|$|;|&)/i,
|
|
672
|
+
/rm\s+-rf\s+\*(?:\s|$|;|&)/i,
|
|
673
|
+
/\bsudo\s+rm\b/i,
|
|
674
|
+
/\bdel\s+\/s\b/i,
|
|
675
|
+
/\brd\s+\/s\b/i,
|
|
676
|
+
/\bformat\s+[a-z]:/i,
|
|
677
|
+
/\bdiskpart\b/i,
|
|
591
678
|
/:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
|
|
592
679
|
/\bmkfs\b/i,
|
|
593
680
|
/dd\s+[^|;]*\bif=/i,
|
|
@@ -617,12 +704,30 @@ function isOutsideCwd(p, cwd) {
|
|
|
617
704
|
const base = normalizeAbsolute(cwd, cwd);
|
|
618
705
|
return abs !== base && !abs.startsWith(`${base}/`);
|
|
619
706
|
}
|
|
620
|
-
function
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
707
|
+
function resolveRealPath(p, cwd) {
|
|
708
|
+
let current = path.isAbsolute(p) ? path.normalize(p) : path.resolve(cwd, p);
|
|
709
|
+
const missing = [];
|
|
710
|
+
for (; ; ) {
|
|
711
|
+
try {
|
|
712
|
+
return path.join(fs.realpathSync(current), ...missing.reverse());
|
|
713
|
+
} catch {
|
|
714
|
+
const parent = path.dirname(current);
|
|
715
|
+
if (parent === current) return null;
|
|
716
|
+
missing.push(path.basename(current));
|
|
717
|
+
current = parent;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function isOutsideForWrite(p, cwd) {
|
|
722
|
+
const resolved = resolveRealPath(p, cwd);
|
|
723
|
+
if (resolved === null) return isOutsideCwd(p, cwd);
|
|
724
|
+
let realCwd;
|
|
725
|
+
try {
|
|
726
|
+
realCwd = fs.realpathSync(cwd);
|
|
727
|
+
} catch {
|
|
728
|
+
return isOutsideCwd(p, cwd);
|
|
624
729
|
}
|
|
625
|
-
return
|
|
730
|
+
return isOutsideCwd(resolved, realCwd);
|
|
626
731
|
}
|
|
627
732
|
function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
|
|
628
733
|
if (mode === "yolo") {
|
|
@@ -634,10 +739,12 @@ function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
|
|
|
634
739
|
return "deny";
|
|
635
740
|
}
|
|
636
741
|
if (FILE_TOOLS.has(req.toolName) && filePath !== void 0) {
|
|
637
|
-
|
|
742
|
+
const isWriteTool = req.toolName === "write_file" || req.toolName === "edit_file";
|
|
743
|
+
if (isWriteTool && isSensitivePath(filePath)) {
|
|
638
744
|
return "deny";
|
|
639
745
|
}
|
|
640
|
-
|
|
746
|
+
const outside = isWriteTool ? isOutsideForWrite(filePath, ctx.cwd) : isOutsideCwd(filePath, ctx.cwd);
|
|
747
|
+
if (outside) {
|
|
641
748
|
if (mode === "ask" && req.level === "read") {
|
|
642
749
|
return "ask";
|
|
643
750
|
}
|
|
@@ -708,10 +815,93 @@ function scheduleSessionTitle(store, userText, model) {
|
|
|
708
815
|
});
|
|
709
816
|
}
|
|
710
817
|
|
|
711
|
-
// src/
|
|
712
|
-
import {
|
|
818
|
+
// src/snapshot/git-tree.ts
|
|
819
|
+
import { execFile } from "child_process";
|
|
820
|
+
import { existsSync } from "fs";
|
|
821
|
+
import { mkdir } from "fs/promises";
|
|
822
|
+
import os from "os";
|
|
713
823
|
import path2 from "path";
|
|
824
|
+
var GIT_TIMEOUT_MS2 = 1e4;
|
|
825
|
+
var GIT_MAX_BUFFER2 = 8 * 1024 * 1024;
|
|
826
|
+
function runGit2(args) {
|
|
827
|
+
return new Promise((resolve) => {
|
|
828
|
+
execFile(
|
|
829
|
+
"git",
|
|
830
|
+
args,
|
|
831
|
+
{ timeout: GIT_TIMEOUT_MS2, maxBuffer: GIT_MAX_BUFFER2, encoding: "utf8" },
|
|
832
|
+
(error, stdout) => {
|
|
833
|
+
resolve(error ? null : stdout.trim());
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
var gitAvailable = null;
|
|
839
|
+
async function isGitAvailable() {
|
|
840
|
+
if (gitAvailable === null) {
|
|
841
|
+
gitAvailable = await runGit2(["--version"]) !== null;
|
|
842
|
+
}
|
|
843
|
+
return gitAvailable;
|
|
844
|
+
}
|
|
845
|
+
function treeRepoDir(cwd) {
|
|
846
|
+
const key = Buffer.from(path2.resolve(cwd), "utf8").toString("base64url");
|
|
847
|
+
return path2.join(gitTreesDir(), key);
|
|
848
|
+
}
|
|
849
|
+
function isGuardedDir(cwd) {
|
|
850
|
+
const resolved = path2.resolve(cwd);
|
|
851
|
+
const home = path2.resolve(os.homedir());
|
|
852
|
+
const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
853
|
+
return same(resolved, home) || same(resolved, path2.parse(resolved).root);
|
|
854
|
+
}
|
|
855
|
+
async function ensureTreeRepo(cwd) {
|
|
856
|
+
const dir = treeRepoDir(cwd);
|
|
857
|
+
if (existsSync(path2.join(dir, "HEAD"))) return dir;
|
|
858
|
+
await mkdir(path2.dirname(dir), { recursive: true });
|
|
859
|
+
const out = await runGit2(["init", "--bare", dir]);
|
|
860
|
+
return out === null ? null : dir;
|
|
861
|
+
}
|
|
862
|
+
function repoArgs(dir, cwd) {
|
|
863
|
+
return [`--git-dir=${dir}`, `--work-tree=${cwd}`];
|
|
864
|
+
}
|
|
865
|
+
async function trackTree(cwd) {
|
|
866
|
+
if (isGuardedDir(cwd)) return null;
|
|
867
|
+
if (!await isGitAvailable()) return null;
|
|
868
|
+
const dir = await ensureTreeRepo(cwd);
|
|
869
|
+
if (!dir) return null;
|
|
870
|
+
const base = repoArgs(dir, cwd);
|
|
871
|
+
if (await runGit2([...base, "add", "-A"]) === null) return null;
|
|
872
|
+
return runGit2([...base, "write-tree"]);
|
|
873
|
+
}
|
|
874
|
+
async function restoreTree(cwd, hash) {
|
|
875
|
+
if (isGuardedDir(cwd)) return false;
|
|
876
|
+
if (!await isGitAvailable()) return false;
|
|
877
|
+
const dir = treeRepoDir(cwd);
|
|
878
|
+
if (!existsSync(path2.join(dir, "HEAD"))) return false;
|
|
879
|
+
const base = repoArgs(dir, cwd);
|
|
880
|
+
if (await runGit2([...base, "read-tree", hash]) === null) return false;
|
|
881
|
+
if (await runGit2([...base, "checkout-index", "-f", "-a"]) === null) return false;
|
|
882
|
+
if (await runGit2([...base, "clean", "-fd"]) === null) return false;
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
async function diffTreeNames(cwd, hash) {
|
|
886
|
+
if (!await isGitAvailable()) return null;
|
|
887
|
+
const dir = treeRepoDir(cwd);
|
|
888
|
+
if (!existsSync(path2.join(dir, "HEAD"))) return null;
|
|
889
|
+
const base = repoArgs(dir, cwd);
|
|
890
|
+
const changed = await runGit2([...base, "diff", "--name-only", hash]);
|
|
891
|
+
const untracked = await runGit2([...base, "ls-files", "--others", "--exclude-standard"]);
|
|
892
|
+
if (changed === null || untracked === null) return null;
|
|
893
|
+
const names = /* @__PURE__ */ new Set();
|
|
894
|
+
for (const line of [...changed.split("\n"), ...untracked.split("\n")]) {
|
|
895
|
+
if (line !== "") names.add(line);
|
|
896
|
+
}
|
|
897
|
+
return [...names];
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/tools/fs/snapshots.ts
|
|
901
|
+
import { mkdir as mkdir2, readFile, rm, stat, writeFile } from "fs/promises";
|
|
902
|
+
import path3 from "path";
|
|
714
903
|
var MAX_SNAPSHOTS = 50;
|
|
904
|
+
var MAX_SNAPSHOT_CONTENT_BYTES = 5 * 1024 * 1024;
|
|
715
905
|
var stack = [];
|
|
716
906
|
var activeTurn = 0;
|
|
717
907
|
var activeTurnMessageIndex = -1;
|
|
@@ -733,28 +923,48 @@ function currentTurnSeq() {
|
|
|
733
923
|
function currentTurnMessageIndex() {
|
|
734
924
|
return activeTurnMessageIndex;
|
|
735
925
|
}
|
|
736
|
-
|
|
926
|
+
function ownerOf(snapshot) {
|
|
927
|
+
return snapshot.owner ?? "root";
|
|
928
|
+
}
|
|
929
|
+
async function captureSnapshot(filePath, toolName, snapshotContext) {
|
|
737
930
|
let content = null;
|
|
738
931
|
let existed = false;
|
|
739
|
-
|
|
740
|
-
|
|
932
|
+
let contentTooLarge = false;
|
|
933
|
+
const st = await stat(filePath).catch(() => null);
|
|
934
|
+
if (st?.isFile()) {
|
|
741
935
|
existed = true;
|
|
742
|
-
|
|
936
|
+
if (st.size > MAX_SNAPSHOT_CONTENT_BYTES) {
|
|
937
|
+
contentTooLarge = true;
|
|
938
|
+
} else {
|
|
939
|
+
try {
|
|
940
|
+
content = await readFile(filePath, "utf8");
|
|
941
|
+
} catch {
|
|
942
|
+
existed = false;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
743
945
|
}
|
|
744
946
|
return {
|
|
745
947
|
id: nextSnapshotId(),
|
|
746
948
|
path: filePath,
|
|
747
949
|
existed,
|
|
748
950
|
content,
|
|
951
|
+
contentTooLarge: contentTooLarge || void 0,
|
|
749
952
|
toolName,
|
|
750
953
|
timestamp: Date.now(),
|
|
751
|
-
turn: activeTurn,
|
|
752
|
-
messageIndex: activeTurnMessageIndex
|
|
954
|
+
turn: snapshotContext?.turn ?? activeTurn,
|
|
955
|
+
messageIndex: snapshotContext?.messageIndex ?? activeTurnMessageIndex,
|
|
956
|
+
owner: snapshotContext?.owner ?? "root"
|
|
753
957
|
};
|
|
754
958
|
}
|
|
755
959
|
async function pushSnapshot(snapshot) {
|
|
756
960
|
stack.push(snapshot);
|
|
757
|
-
|
|
961
|
+
try {
|
|
962
|
+
await hooks?.onPush?.(snapshot);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
if (process.env.STAR_DEBUG) {
|
|
965
|
+
console.error("[star] failed to persist snapshot checkpoint:", err);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
758
968
|
if (stack.length > MAX_SNAPSHOTS) {
|
|
759
969
|
const evicted = stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
760
970
|
await removeFromStore(evicted.map((s) => s.id));
|
|
@@ -787,191 +997,156 @@ async function resolveContent(snapshot) {
|
|
|
787
997
|
}
|
|
788
998
|
async function revert(snapshot) {
|
|
789
999
|
if (snapshot.existed) {
|
|
1000
|
+
if (snapshot.contentTooLarge) {
|
|
1001
|
+
return `Skipped ${snapshot.path}: content too large, not restored.`;
|
|
1002
|
+
}
|
|
790
1003
|
const content = await resolveContent(snapshot);
|
|
791
1004
|
if (content === null) {
|
|
792
1005
|
return `Skipped ${snapshot.path}: the snapshot content is no longer available.`;
|
|
793
1006
|
}
|
|
794
|
-
await
|
|
1007
|
+
await mkdir2(path3.dirname(snapshot.path), { recursive: true });
|
|
795
1008
|
await writeFile(snapshot.path, content, "utf8");
|
|
796
1009
|
return `Restored ${snapshot.path} to its state before ${snapshot.toolName}.`;
|
|
797
1010
|
}
|
|
798
1011
|
await rm(snapshot.path, { force: true });
|
|
799
1012
|
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
800
1013
|
}
|
|
801
|
-
function listTurnSnapshots(turn) {
|
|
802
|
-
return stack.filter(
|
|
1014
|
+
function listTurnSnapshots(turn, includeSubagent = false) {
|
|
1015
|
+
return stack.filter(
|
|
1016
|
+
(snapshot) => snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")
|
|
1017
|
+
).reverse();
|
|
803
1018
|
}
|
|
804
1019
|
function resolveSnapshotContent(snapshot) {
|
|
805
1020
|
return resolveContent(snapshot);
|
|
806
1021
|
}
|
|
807
|
-
async function undoTurnSnapshots(turn) {
|
|
1022
|
+
async function undoTurnSnapshots(turn, includeSubagent = false) {
|
|
808
1023
|
const matches = [];
|
|
809
1024
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
810
1025
|
const snapshot = stack[i];
|
|
811
|
-
if (snapshot && snapshot.turn === turn) {
|
|
1026
|
+
if (snapshot && snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
812
1027
|
matches.push(snapshot);
|
|
813
|
-
stack.splice(i, 1);
|
|
814
1028
|
}
|
|
815
1029
|
}
|
|
816
|
-
await removeFromStore(matches.map((s) => s.id));
|
|
817
1030
|
const messages = [];
|
|
1031
|
+
const revertedIds = [];
|
|
818
1032
|
for (const snapshot of matches) {
|
|
819
|
-
|
|
1033
|
+
try {
|
|
1034
|
+
messages.push(await revert(snapshot));
|
|
1035
|
+
const idx = stack.indexOf(snapshot);
|
|
1036
|
+
if (idx >= 0) stack.splice(idx, 1);
|
|
1037
|
+
revertedIds.push(snapshot.id);
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
messages.push(
|
|
1040
|
+
`Failed to revert ${snapshot.path}: ${err.message} (checkpoint kept)`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
820
1043
|
}
|
|
1044
|
+
await removeFromStore(revertedIds);
|
|
821
1045
|
return messages;
|
|
822
1046
|
}
|
|
823
|
-
async function
|
|
1047
|
+
async function dropTurnSnapshots(turn, includeSubagent = false) {
|
|
1048
|
+
const dropped = [];
|
|
1049
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
1050
|
+
const snapshot = stack[i];
|
|
1051
|
+
if (snapshot && snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
1052
|
+
dropped.push(snapshot.id);
|
|
1053
|
+
stack.splice(i, 1);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
await removeFromStore(dropped);
|
|
1057
|
+
return dropped.length;
|
|
1058
|
+
}
|
|
1059
|
+
async function rewindToSnapshot(id, includeSubagent = false) {
|
|
824
1060
|
if (!stack.some((snapshot) => snapshot.id === id)) return null;
|
|
825
1061
|
const matches = [];
|
|
826
1062
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
827
1063
|
const snapshot = stack[i];
|
|
828
|
-
if (snapshot && snapshot.id >= id) {
|
|
1064
|
+
if (snapshot && snapshot.id >= id && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
829
1065
|
matches.push(snapshot);
|
|
830
|
-
stack.splice(i, 1);
|
|
831
1066
|
}
|
|
832
1067
|
}
|
|
833
|
-
await removeFromStore(matches.map((s) => s.id));
|
|
834
1068
|
const reverted = [];
|
|
1069
|
+
const revertedIds = [];
|
|
835
1070
|
let messageIndex = null;
|
|
836
1071
|
for (const snapshot of matches) {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
reverted.push(await revert(snapshot));
|
|
1074
|
+
const idx = stack.indexOf(snapshot);
|
|
1075
|
+
if (idx >= 0) stack.splice(idx, 1);
|
|
1076
|
+
revertedIds.push(snapshot.id);
|
|
1077
|
+
if (snapshot.messageIndex >= 0) {
|
|
1078
|
+
messageIndex = messageIndex === null ? snapshot.messageIndex : Math.min(messageIndex, snapshot.messageIndex);
|
|
1079
|
+
}
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
reverted.push(
|
|
1082
|
+
`Failed to revert ${snapshot.path}: ${err.message} (checkpoint kept)`
|
|
1083
|
+
);
|
|
840
1084
|
}
|
|
841
1085
|
}
|
|
1086
|
+
await removeFromStore(revertedIds);
|
|
842
1087
|
return { reverted, messageIndex };
|
|
843
1088
|
}
|
|
844
1089
|
|
|
845
|
-
// src/tools/
|
|
846
|
-
import {
|
|
847
|
-
import
|
|
1090
|
+
// src/tools/bash.ts
|
|
1091
|
+
import { spawn as spawn3 } from "child_process";
|
|
1092
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1093
|
+
import path4 from "path";
|
|
848
1094
|
import { z } from "zod";
|
|
849
|
-
var todoItemSchema = z.object({
|
|
850
|
-
id: z.number().int(),
|
|
851
|
-
title: z.string(),
|
|
852
|
-
status: z.enum(["pending", "in_progress", "done"])
|
|
853
|
-
});
|
|
854
|
-
var writeSchema = z.object({
|
|
855
|
-
todos: z.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
856
|
-
});
|
|
857
|
-
var readSchema = z.object({});
|
|
858
|
-
function fileFor(cwd) {
|
|
859
|
-
return path3.join(cwd, ".star", "todos.json");
|
|
860
|
-
}
|
|
861
|
-
var TodoStore = class {
|
|
862
|
-
items = /* @__PURE__ */ new Map();
|
|
863
|
-
list() {
|
|
864
|
-
return [...this.items.values()];
|
|
865
|
-
}
|
|
866
|
-
replace(items) {
|
|
867
|
-
this.items.clear();
|
|
868
|
-
for (const item of items) {
|
|
869
|
-
this.items.set(item.id, item);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
async load(cwd) {
|
|
873
|
-
let raw;
|
|
874
|
-
try {
|
|
875
|
-
raw = await readFile2(fileFor(cwd), "utf8");
|
|
876
|
-
} catch {
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
try {
|
|
880
|
-
const parsed = JSON.parse(raw);
|
|
881
|
-
if (Array.isArray(parsed)) {
|
|
882
|
-
this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
|
|
883
|
-
}
|
|
884
|
-
} catch {
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
async save(cwd) {
|
|
888
|
-
await mkdir2(path3.join(cwd, ".star"), { recursive: true });
|
|
889
|
-
await writeFile2(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
|
|
890
|
-
`);
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
var SYMBOLS = {
|
|
894
|
-
pending: "[ ]",
|
|
895
|
-
in_progress: "[~]",
|
|
896
|
-
done: "[x]"
|
|
897
|
-
};
|
|
898
|
-
function formatTodos(items) {
|
|
899
|
-
if (items.length === 0) {
|
|
900
|
-
return "No todos.";
|
|
901
|
-
}
|
|
902
|
-
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
903
|
-
const width = Math.max(...lines.map((l) => l.length));
|
|
904
|
-
return lines.map((line, i) => {
|
|
905
|
-
const status = items[i]?.status;
|
|
906
|
-
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
907
|
-
}).join("\n");
|
|
908
|
-
}
|
|
909
|
-
var defaultStore = new TodoStore();
|
|
910
|
-
function parseTodoArgs(args) {
|
|
911
|
-
const parsed = writeSchema.safeParse(args);
|
|
912
|
-
return parsed.success ? parsed.data.todos : null;
|
|
913
|
-
}
|
|
914
|
-
function pendingTodoTitles(args) {
|
|
915
|
-
return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
|
|
916
|
-
}
|
|
917
|
-
async function loadTodos(cwd) {
|
|
918
|
-
await defaultStore.load(cwd);
|
|
919
|
-
return defaultStore.list();
|
|
920
|
-
}
|
|
921
|
-
function resetTodos() {
|
|
922
|
-
defaultStore.replace([]);
|
|
923
|
-
}
|
|
924
|
-
function createTodoTools(store = defaultStore) {
|
|
925
|
-
const todoWrite = {
|
|
926
|
-
name: "todo_write",
|
|
927
|
-
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
928
|
-
permission: "read",
|
|
929
|
-
parameters: writeSchema,
|
|
930
|
-
async execute(args, ctx) {
|
|
931
|
-
const parsed = writeSchema.safeParse(args);
|
|
932
|
-
if (!parsed.success) {
|
|
933
|
-
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
934
|
-
}
|
|
935
|
-
store.replace(parsed.data.todos);
|
|
936
|
-
await store.save(ctx.cwd);
|
|
937
|
-
const items = store.list();
|
|
938
|
-
const done = items.filter((it) => it.status === "done").length;
|
|
939
|
-
return { content: `Todo list updated (${done}/${items.length} done).` };
|
|
940
|
-
}
|
|
941
|
-
};
|
|
942
|
-
const todoRead = {
|
|
943
|
-
name: "todo_read",
|
|
944
|
-
description: "Read the current todo list.",
|
|
945
|
-
permission: "read",
|
|
946
|
-
parameters: readSchema,
|
|
947
|
-
async execute() {
|
|
948
|
-
return { content: formatTodos(store.list()) };
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
return [todoWrite, todoRead];
|
|
952
|
-
}
|
|
953
1095
|
|
|
954
|
-
// src/
|
|
1096
|
+
// src/tasks/manager.ts
|
|
1097
|
+
import { spawn as spawn2 } from "child_process";
|
|
955
1098
|
import { EventEmitter } from "events";
|
|
956
|
-
var
|
|
1099
|
+
var MAX_TASK_OUTPUT = 1e5;
|
|
1100
|
+
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
1101
|
+
var MAX_FINISHED_RECORDS = 50;
|
|
1102
|
+
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
1103
|
+
var TaskManager = class extends EventEmitter {
|
|
957
1104
|
records = /* @__PURE__ */ new Map();
|
|
958
|
-
|
|
959
|
-
start(
|
|
960
|
-
const id = `
|
|
1105
|
+
seq = 0;
|
|
1106
|
+
start(opts) {
|
|
1107
|
+
const id = `task-${++this.seq}`;
|
|
1108
|
+
const spec = resolveShell();
|
|
1109
|
+
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
1110
|
+
cwd: opts.cwd,
|
|
1111
|
+
windowsHide: true,
|
|
1112
|
+
// POSIX: the child leads its own process group so killTree can SIGKILL
|
|
1113
|
+
// the whole tree with a negative pid (Windows uses taskkill /t).
|
|
1114
|
+
detached: process.platform !== "win32"
|
|
1115
|
+
});
|
|
961
1116
|
const rec = {
|
|
962
1117
|
id,
|
|
963
|
-
|
|
964
|
-
description:
|
|
1118
|
+
command: opts.command,
|
|
1119
|
+
description: opts.description,
|
|
965
1120
|
status: "running",
|
|
966
1121
|
startedAt: Date.now(),
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1122
|
+
output: "",
|
|
1123
|
+
truncated: false,
|
|
1124
|
+
child
|
|
970
1125
|
};
|
|
971
1126
|
this.records.set(id, rec);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1127
|
+
const append = (d) => {
|
|
1128
|
+
rec.output += d.toString("utf8");
|
|
1129
|
+
if (rec.output.length > MAX_TASK_OUTPUT) {
|
|
1130
|
+
rec.output = rec.output.slice(-MAX_TASK_OUTPUT);
|
|
1131
|
+
rec.truncated = true;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
child.stdout.on("data", append);
|
|
1135
|
+
child.stderr.on("data", append);
|
|
1136
|
+
child.on("error", (err) => {
|
|
1137
|
+
append(Buffer.from(`Failed to start shell '${spec.label}': ${err.message}
|
|
1138
|
+
`));
|
|
1139
|
+
if (rec.status === "running") {
|
|
1140
|
+
this.finish(rec, "failed", null);
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
child.on("close", (code) => {
|
|
1144
|
+
if (rec.status !== "running") return;
|
|
1145
|
+
this.finish(rec, code === 0 ? "completed" : "failed", code);
|
|
1146
|
+
});
|
|
1147
|
+
const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_TASK_TIMEOUT;
|
|
1148
|
+
rec.timer = setTimeout(() => this.terminate(rec, "timed_out"), timeoutSeconds * 1e3);
|
|
1149
|
+
rec.timer.unref();
|
|
975
1150
|
this.emitUpdate(rec);
|
|
976
1151
|
return this.snapshot(rec);
|
|
977
1152
|
}
|
|
@@ -992,347 +1167,75 @@ var AgentTaskManager = class extends EventEmitter {
|
|
|
992
1167
|
kill(id) {
|
|
993
1168
|
const rec = this.records.get(id);
|
|
994
1169
|
if (!rec || rec.status !== "running") return void 0;
|
|
995
|
-
|
|
996
|
-
this.finish(rec, "stopped", rec.result || "Stopped by user.");
|
|
1170
|
+
this.terminate(rec, "stopped");
|
|
997
1171
|
return this.snapshot(rec);
|
|
998
1172
|
}
|
|
999
1173
|
cleanup() {
|
|
1000
1174
|
const killed = [];
|
|
1001
1175
|
for (const rec of this.records.values()) {
|
|
1002
1176
|
if (rec.status === "running") {
|
|
1003
|
-
|
|
1004
|
-
this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
|
|
1177
|
+
this.terminate(rec, "stopped");
|
|
1005
1178
|
killed.push(this.snapshot(rec));
|
|
1006
1179
|
}
|
|
1007
1180
|
}
|
|
1008
1181
|
return killed;
|
|
1009
1182
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
for (const rec of this.records.values()) {
|
|
1014
|
-
if (rec.status !== "running" && !rec.notified) {
|
|
1015
|
-
rec.notified = true;
|
|
1016
|
-
drained.push(this.snapshot(rec));
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
return drained;
|
|
1183
|
+
terminate(rec, status) {
|
|
1184
|
+
killTree2(rec.child);
|
|
1185
|
+
this.finish(rec, status, null);
|
|
1020
1186
|
}
|
|
1021
|
-
finish(rec, status,
|
|
1022
|
-
if (rec.
|
|
1187
|
+
finish(rec, status, exitCode) {
|
|
1188
|
+
if (rec.timer) clearTimeout(rec.timer);
|
|
1023
1189
|
rec.status = status;
|
|
1024
|
-
rec.
|
|
1190
|
+
rec.exitCode = exitCode;
|
|
1025
1191
|
rec.endedAt = Date.now();
|
|
1192
|
+
this.pruneFinished();
|
|
1026
1193
|
this.emitUpdate(rec);
|
|
1027
1194
|
}
|
|
1195
|
+
pruneFinished() {
|
|
1196
|
+
const finished = [...this.records.values()].filter((rec) => rec.status !== "running");
|
|
1197
|
+
if (finished.length <= MAX_FINISHED_RECORDS) return;
|
|
1198
|
+
finished.sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0));
|
|
1199
|
+
for (const rec of finished.slice(0, finished.length - MAX_FINISHED_RECORDS)) {
|
|
1200
|
+
this.records.delete(rec.id);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1028
1203
|
emitUpdate(rec) {
|
|
1029
1204
|
this.emit("update", this.snapshot(rec));
|
|
1030
1205
|
}
|
|
1031
1206
|
snapshot(rec) {
|
|
1032
1207
|
return {
|
|
1033
1208
|
id: rec.id,
|
|
1034
|
-
|
|
1209
|
+
command: rec.command,
|
|
1035
1210
|
description: rec.description,
|
|
1036
1211
|
status: rec.status,
|
|
1037
1212
|
startedAt: rec.startedAt,
|
|
1038
1213
|
endedAt: rec.endedAt,
|
|
1039
|
-
|
|
1214
|
+
exitCode: rec.exitCode,
|
|
1215
|
+
output: rec.truncated ? TRUNCATED_PREFIX + rec.output : rec.output
|
|
1040
1216
|
};
|
|
1041
1217
|
}
|
|
1042
1218
|
};
|
|
1043
|
-
var
|
|
1044
|
-
|
|
1045
|
-
// src/agent/project-memory.ts
|
|
1046
|
-
import { readFileSync, statSync } from "fs";
|
|
1047
|
-
import path4 from "path";
|
|
1048
|
-
var PROJECT_MEMORY_FILE = "AGENTS.md";
|
|
1049
|
-
var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
|
|
1050
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1051
|
-
function readProjectMemory(cwd) {
|
|
1052
|
-
const file = path4.join(cwd, PROJECT_MEMORY_FILE);
|
|
1053
|
-
let mtimeMs;
|
|
1054
|
-
try {
|
|
1055
|
-
mtimeMs = statSync(file).mtimeMs;
|
|
1056
|
-
} catch {
|
|
1057
|
-
return null;
|
|
1058
|
-
}
|
|
1059
|
-
const hit = cache.get(file);
|
|
1060
|
-
if (hit && hit.mtimeMs === mtimeMs) return hit.block;
|
|
1061
|
-
let block = null;
|
|
1062
|
-
try {
|
|
1063
|
-
let body = readFileSync(file, "utf8").trim();
|
|
1064
|
-
if (body) {
|
|
1065
|
-
if (body.length > PROJECT_MEMORY_MAX_CHARS) {
|
|
1066
|
-
body = `${body.slice(0, PROJECT_MEMORY_MAX_CHARS)}
|
|
1067
|
-
|
|
1068
|
-
[AGENTS.md truncated to ${PROJECT_MEMORY_MAX_CHARS} characters]`;
|
|
1069
|
-
}
|
|
1070
|
-
block = `# Project instructions (AGENTS.md)
|
|
1071
|
-
${body}`;
|
|
1072
|
-
}
|
|
1073
|
-
} catch {
|
|
1074
|
-
block = null;
|
|
1075
|
-
}
|
|
1076
|
-
cache.set(file, { mtimeMs, block });
|
|
1077
|
-
return block;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// src/agent/skills.ts
|
|
1081
|
-
import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
1082
|
-
import path5 from "path";
|
|
1083
|
-
import { z as z2 } from "zod";
|
|
1084
|
-
var SKILL_FILE = "SKILL.md";
|
|
1085
|
-
var SKILL_MAX_CHARS = 32 * 1024;
|
|
1086
|
-
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
1087
|
-
function parseFrontmatter(raw) {
|
|
1088
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
1089
|
-
if (!match) return null;
|
|
1090
|
-
const meta = {};
|
|
1091
|
-
for (const line of (match[1] ?? "").split(/\r?\n/)) {
|
|
1092
|
-
const kv = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
1093
|
-
if (kv?.[1]) meta[kv[1]] = (kv[2] ?? "").trim();
|
|
1094
|
-
}
|
|
1095
|
-
return { meta, body: raw.slice(match[0].length).trim() };
|
|
1096
|
-
}
|
|
1097
|
-
var cache2 = /* @__PURE__ */ new Map();
|
|
1098
|
-
function parseSkillFile(filePath, dirName, scope) {
|
|
1099
|
-
let mtimeMs;
|
|
1100
|
-
try {
|
|
1101
|
-
mtimeMs = statSync2(filePath).mtimeMs;
|
|
1102
|
-
} catch {
|
|
1103
|
-
return null;
|
|
1104
|
-
}
|
|
1105
|
-
const hit = cache2.get(filePath);
|
|
1106
|
-
if (hit && hit.mtimeMs === mtimeMs) return hit.skill;
|
|
1107
|
-
let skill = null;
|
|
1108
|
-
try {
|
|
1109
|
-
const parsed = parseFrontmatter(readFileSync2(filePath, "utf8"));
|
|
1110
|
-
const name = parsed?.meta.name || dirName;
|
|
1111
|
-
const description = parsed?.meta.description;
|
|
1112
|
-
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
1113
|
-
skill = { name, description, filePath, dir: path5.dirname(filePath), scope };
|
|
1114
|
-
}
|
|
1115
|
-
} catch {
|
|
1116
|
-
skill = null;
|
|
1117
|
-
}
|
|
1118
|
-
cache2.set(filePath, { mtimeMs, skill });
|
|
1119
|
-
return skill;
|
|
1120
|
-
}
|
|
1121
|
-
function scanDir(base, scope, into) {
|
|
1122
|
-
let entries;
|
|
1123
|
-
try {
|
|
1124
|
-
entries = readdirSync(base);
|
|
1125
|
-
} catch {
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
for (const entry of entries.sort()) {
|
|
1129
|
-
if (!NAME_PATTERN.test(entry)) continue;
|
|
1130
|
-
const parsed = parseSkillFile(path5.join(base, entry, SKILL_FILE), entry, scope);
|
|
1131
|
-
if (parsed) into.set(parsed.name, parsed);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
function discoverSkills(cwd, home = starHome()) {
|
|
1135
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1136
|
-
scanDir(path5.join(home, "skills"), "user", byName);
|
|
1137
|
-
scanDir(path5.join(cwd, ".star", "skills"), "project", byName);
|
|
1138
|
-
return [...byName.values()];
|
|
1139
|
-
}
|
|
1140
|
-
function formatSkillsBlock(skills) {
|
|
1141
|
-
const lines = skills.map((s) => `- ${s.name}: ${s.description}`);
|
|
1142
|
-
return [
|
|
1143
|
-
"# Available skills",
|
|
1144
|
-
"When the user's request matches one of these skills, call the skill tool with its name to load the full instructions before proceeding:",
|
|
1145
|
-
...lines
|
|
1146
|
-
].join("\n");
|
|
1147
|
-
}
|
|
1148
|
-
function loadSkillBody(skill) {
|
|
1149
|
-
let parsed = null;
|
|
1150
|
-
try {
|
|
1151
|
-
parsed = parseFrontmatter(readFileSync2(skill.filePath, "utf8"));
|
|
1152
|
-
} catch {
|
|
1153
|
-
return null;
|
|
1154
|
-
}
|
|
1155
|
-
if (!parsed?.body) return null;
|
|
1156
|
-
if (parsed.body.length > SKILL_MAX_CHARS) {
|
|
1157
|
-
return `${parsed.body.slice(0, SKILL_MAX_CHARS)}
|
|
1158
|
-
|
|
1159
|
-
[skill truncated to ${SKILL_MAX_CHARS} characters]`;
|
|
1160
|
-
}
|
|
1161
|
-
return parsed.body;
|
|
1162
|
-
}
|
|
1163
|
-
function createSkillTool(deps) {
|
|
1164
|
-
return {
|
|
1165
|
-
name: "skill",
|
|
1166
|
-
description: "Load the full instructions of a skill by name. Available skills are listed in the system prompt when present; call this before starting work that matches one of them.",
|
|
1167
|
-
permission: "read",
|
|
1168
|
-
parameters: z2.object({
|
|
1169
|
-
name: z2.string().describe("Name of the skill to load, as listed in the system prompt.")
|
|
1170
|
-
}),
|
|
1171
|
-
async execute(args) {
|
|
1172
|
-
const skills = discoverSkills(deps.cwd, deps.home);
|
|
1173
|
-
const skill = skills.find((s) => s.name === args.name);
|
|
1174
|
-
if (!skill) {
|
|
1175
|
-
const available = skills.map((s) => s.name).join(", ") || "none";
|
|
1176
|
-
return {
|
|
1177
|
-
content: `Unknown skill: "${args.name}". Available skills: ${available}`,
|
|
1178
|
-
isError: true
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
const body = loadSkillBody(skill);
|
|
1182
|
-
if (!body) {
|
|
1183
|
-
return { content: `Skill "${skill.name}" is empty or unreadable.`, isError: true };
|
|
1184
|
-
}
|
|
1185
|
-
return {
|
|
1186
|
-
content: [
|
|
1187
|
-
`# Skill: ${skill.name}`,
|
|
1188
|
-
"",
|
|
1189
|
-
body,
|
|
1190
|
-
"",
|
|
1191
|
-
`Base directory for relative file references in this skill: ${skill.dir}`
|
|
1192
|
-
].join("\n")
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// src/agent/subagent.ts
|
|
1199
|
-
import { z as z12 } from "zod";
|
|
1200
|
-
|
|
1201
|
-
// src/tools/bash.ts
|
|
1202
|
-
import { spawn as spawn3 } from "child_process";
|
|
1203
|
-
import { existsSync } from "fs";
|
|
1204
|
-
import path6 from "path";
|
|
1205
|
-
import { z as z3 } from "zod";
|
|
1206
|
-
|
|
1207
|
-
// src/tasks/manager.ts
|
|
1208
|
-
import { spawn as spawn2 } from "child_process";
|
|
1209
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
1210
|
-
var MAX_TASK_OUTPUT = 1e5;
|
|
1211
|
-
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
1212
|
-
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
1213
|
-
var TaskManager = class extends EventEmitter2 {
|
|
1214
|
-
records = /* @__PURE__ */ new Map();
|
|
1215
|
-
seq = 0;
|
|
1216
|
-
start(opts) {
|
|
1217
|
-
const id = `task-${++this.seq}`;
|
|
1218
|
-
const spec = resolveShell();
|
|
1219
|
-
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
1220
|
-
cwd: opts.cwd,
|
|
1221
|
-
windowsHide: true
|
|
1222
|
-
});
|
|
1223
|
-
const rec = {
|
|
1224
|
-
id,
|
|
1225
|
-
command: opts.command,
|
|
1226
|
-
description: opts.description,
|
|
1227
|
-
status: "running",
|
|
1228
|
-
startedAt: Date.now(),
|
|
1229
|
-
output: "",
|
|
1230
|
-
truncated: false,
|
|
1231
|
-
child
|
|
1232
|
-
};
|
|
1233
|
-
this.records.set(id, rec);
|
|
1234
|
-
const append = (d) => {
|
|
1235
|
-
rec.output += d.toString("utf8");
|
|
1236
|
-
if (rec.output.length > MAX_TASK_OUTPUT) {
|
|
1237
|
-
rec.output = rec.output.slice(-MAX_TASK_OUTPUT);
|
|
1238
|
-
rec.truncated = true;
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1241
|
-
child.stdout.on("data", append);
|
|
1242
|
-
child.stderr.on("data", append);
|
|
1243
|
-
child.on("error", (err) => {
|
|
1244
|
-
append(Buffer.from(`Failed to start shell '${spec.label}': ${err.message}
|
|
1245
|
-
`));
|
|
1246
|
-
if (rec.status === "running") {
|
|
1247
|
-
this.finish(rec, "failed", null);
|
|
1248
|
-
}
|
|
1249
|
-
});
|
|
1250
|
-
child.on("close", (code) => {
|
|
1251
|
-
if (rec.status !== "running") return;
|
|
1252
|
-
this.finish(rec, code === 0 ? "completed" : "failed", code);
|
|
1253
|
-
});
|
|
1254
|
-
const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_TASK_TIMEOUT;
|
|
1255
|
-
rec.timer = setTimeout(() => this.terminate(rec, "timed_out"), timeoutSeconds * 1e3);
|
|
1256
|
-
rec.timer.unref();
|
|
1257
|
-
this.emitUpdate(rec);
|
|
1258
|
-
return this.snapshot(rec);
|
|
1259
|
-
}
|
|
1260
|
-
get(id) {
|
|
1261
|
-
const rec = this.records.get(id);
|
|
1262
|
-
return rec ? this.snapshot(rec) : void 0;
|
|
1263
|
-
}
|
|
1264
|
-
list() {
|
|
1265
|
-
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
1266
|
-
}
|
|
1267
|
-
runningCount() {
|
|
1268
|
-
let count = 0;
|
|
1269
|
-
for (const rec of this.records.values()) {
|
|
1270
|
-
if (rec.status === "running") count += 1;
|
|
1271
|
-
}
|
|
1272
|
-
return count;
|
|
1273
|
-
}
|
|
1274
|
-
kill(id) {
|
|
1275
|
-
const rec = this.records.get(id);
|
|
1276
|
-
if (!rec || rec.status !== "running") return void 0;
|
|
1277
|
-
this.terminate(rec, "stopped");
|
|
1278
|
-
return this.snapshot(rec);
|
|
1279
|
-
}
|
|
1280
|
-
cleanup() {
|
|
1281
|
-
const killed = [];
|
|
1282
|
-
for (const rec of this.records.values()) {
|
|
1283
|
-
if (rec.status === "running") {
|
|
1284
|
-
this.terminate(rec, "stopped");
|
|
1285
|
-
killed.push(this.snapshot(rec));
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
return killed;
|
|
1289
|
-
}
|
|
1290
|
-
terminate(rec, status) {
|
|
1291
|
-
killTree2(rec.child);
|
|
1292
|
-
this.finish(rec, status, null);
|
|
1293
|
-
}
|
|
1294
|
-
finish(rec, status, exitCode) {
|
|
1295
|
-
if (rec.timer) clearTimeout(rec.timer);
|
|
1296
|
-
rec.status = status;
|
|
1297
|
-
rec.exitCode = exitCode;
|
|
1298
|
-
rec.endedAt = Date.now();
|
|
1299
|
-
this.emitUpdate(rec);
|
|
1300
|
-
}
|
|
1301
|
-
emitUpdate(rec) {
|
|
1302
|
-
this.emit("update", this.snapshot(rec));
|
|
1303
|
-
}
|
|
1304
|
-
snapshot(rec) {
|
|
1305
|
-
return {
|
|
1306
|
-
id: rec.id,
|
|
1307
|
-
command: rec.command,
|
|
1308
|
-
description: rec.description,
|
|
1309
|
-
status: rec.status,
|
|
1310
|
-
startedAt: rec.startedAt,
|
|
1311
|
-
endedAt: rec.endedAt,
|
|
1312
|
-
exitCode: rec.exitCode,
|
|
1313
|
-
output: rec.truncated ? TRUNCATED_PREFIX + rec.output : rec.output
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
};
|
|
1317
|
-
var defaultTaskManager = new TaskManager();
|
|
1219
|
+
var defaultTaskManager = new TaskManager();
|
|
1318
1220
|
|
|
1319
1221
|
// src/tools/bash.ts
|
|
1320
1222
|
var MAX_OUTPUT = 3e4;
|
|
1223
|
+
var HALF_OUTPUT = MAX_OUTPUT / 2;
|
|
1321
1224
|
var DEFAULT_TIMEOUT = 120;
|
|
1322
1225
|
var MAX_TIMEOUT = 600;
|
|
1323
1226
|
function findOnPath(exe, exclude) {
|
|
1324
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1227
|
+
for (const dir of (process.env.PATH ?? "").split(path4.delimiter)) {
|
|
1325
1228
|
if (!dir || exclude?.(dir)) {
|
|
1326
1229
|
continue;
|
|
1327
1230
|
}
|
|
1328
|
-
const full =
|
|
1329
|
-
if (
|
|
1231
|
+
const full = path4.join(dir, exe);
|
|
1232
|
+
if (existsSync2(full)) {
|
|
1330
1233
|
return full;
|
|
1331
1234
|
}
|
|
1332
1235
|
}
|
|
1333
1236
|
return null;
|
|
1334
1237
|
}
|
|
1335
|
-
function
|
|
1238
|
+
function resolveShellUncached() {
|
|
1336
1239
|
if (process.platform !== "win32") {
|
|
1337
1240
|
return { shell: "sh", wrap: (c) => ["-c", c], label: "sh" };
|
|
1338
1241
|
}
|
|
@@ -1343,8 +1246,8 @@ function resolveShell() {
|
|
|
1343
1246
|
}
|
|
1344
1247
|
const gitExe = findOnPath("git.exe");
|
|
1345
1248
|
if (gitExe) {
|
|
1346
|
-
const sibling =
|
|
1347
|
-
if (
|
|
1249
|
+
const sibling = path4.join(path4.dirname(path4.dirname(gitExe)), "bin", "bash.exe");
|
|
1250
|
+
if (existsSync2(sibling)) {
|
|
1348
1251
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
1349
1252
|
}
|
|
1350
1253
|
}
|
|
@@ -1353,42 +1256,82 @@ function resolveShell() {
|
|
|
1353
1256
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1354
1257
|
"D:\\Git\\bin\\bash.exe"
|
|
1355
1258
|
]) {
|
|
1356
|
-
if (
|
|
1259
|
+
if (existsSync2(candidate)) {
|
|
1357
1260
|
return { shell: candidate, wrap: (c) => ["-c", c], label: "bash" };
|
|
1358
1261
|
}
|
|
1359
1262
|
}
|
|
1360
1263
|
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
1361
1264
|
return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
|
|
1362
1265
|
}
|
|
1363
|
-
var
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1266
|
+
var cachedSpec = null;
|
|
1267
|
+
function resolveShell() {
|
|
1268
|
+
if (!cachedSpec) {
|
|
1269
|
+
cachedSpec = resolveShellUncached();
|
|
1270
|
+
}
|
|
1271
|
+
return cachedSpec;
|
|
1272
|
+
}
|
|
1273
|
+
var schema = z.object({
|
|
1274
|
+
command: z.string().describe("Shell command to execute"),
|
|
1275
|
+
timeout: z.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
|
|
1276
|
+
description: z.string().optional().describe("Short description of what the command does"),
|
|
1277
|
+
run_in_background: z.boolean().optional().describe(
|
|
1368
1278
|
"Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
|
|
1369
1279
|
)
|
|
1370
1280
|
});
|
|
1371
1281
|
function killTree2(child) {
|
|
1372
|
-
if (process.platform === "win32"
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1282
|
+
if (process.platform === "win32") {
|
|
1283
|
+
if (child.pid) {
|
|
1284
|
+
spawn3("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
1285
|
+
windowsHide: true,
|
|
1286
|
+
stdio: "ignore"
|
|
1287
|
+
}).unref();
|
|
1288
|
+
}
|
|
1377
1289
|
return;
|
|
1378
1290
|
}
|
|
1291
|
+
if (child.pid) {
|
|
1292
|
+
try {
|
|
1293
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1294
|
+
return;
|
|
1295
|
+
} catch {
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1379
1298
|
child.kill("SIGKILL");
|
|
1380
1299
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1300
|
+
var BoundedOutput = class {
|
|
1301
|
+
head = "";
|
|
1302
|
+
tail = "";
|
|
1303
|
+
dropped = 0;
|
|
1304
|
+
overflow = false;
|
|
1305
|
+
append(text) {
|
|
1306
|
+
if (!this.overflow) {
|
|
1307
|
+
this.head += text;
|
|
1308
|
+
if (this.head.length <= MAX_OUTPUT) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
this.overflow = true;
|
|
1312
|
+
this.tail = this.head.slice(HALF_OUTPUT);
|
|
1313
|
+
this.head = this.head.slice(0, HALF_OUTPUT);
|
|
1314
|
+
if (this.tail.length > HALF_OUTPUT) {
|
|
1315
|
+
this.dropped = this.tail.length - HALF_OUTPUT;
|
|
1316
|
+
this.tail = this.tail.slice(-HALF_OUTPUT);
|
|
1317
|
+
}
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
this.tail += text;
|
|
1321
|
+
if (this.tail.length > HALF_OUTPUT) {
|
|
1322
|
+
this.dropped += this.tail.length - HALF_OUTPUT;
|
|
1323
|
+
this.tail = this.tail.slice(-HALF_OUTPUT);
|
|
1324
|
+
}
|
|
1384
1325
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
${
|
|
1391
|
-
}
|
|
1326
|
+
toString() {
|
|
1327
|
+
if (!this.overflow) {
|
|
1328
|
+
return this.head;
|
|
1329
|
+
}
|
|
1330
|
+
return `${this.head}
|
|
1331
|
+
... [${this.dropped} characters truncated] ...
|
|
1332
|
+
${this.tail}`;
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1392
1335
|
var bashTool = {
|
|
1393
1336
|
name: "bash",
|
|
1394
1337
|
description: "Execute a shell command (Git Bash on Windows when available, otherwise cmd; sh elsewhere). Stdout and stderr are merged. Output is truncated to 30000 characters. Set run_in_background for long-running commands; it returns a task id for task_list/task_output/task_kill.",
|
|
@@ -1413,9 +1356,10 @@ description: ${args.description ?? "(none)"}`
|
|
|
1413
1356
|
return new Promise((resolve) => {
|
|
1414
1357
|
const child = spawn3(spec.shell, spec.wrap(args.command), {
|
|
1415
1358
|
cwd: ctx.cwd,
|
|
1416
|
-
windowsHide: true
|
|
1359
|
+
windowsHide: true,
|
|
1360
|
+
detached: process.platform !== "win32"
|
|
1417
1361
|
});
|
|
1418
|
-
|
|
1362
|
+
const output = new BoundedOutput();
|
|
1419
1363
|
let settled = false;
|
|
1420
1364
|
const finish = (result) => {
|
|
1421
1365
|
if (settled) {
|
|
@@ -1428,36 +1372,32 @@ description: ${args.description ?? "(none)"}`
|
|
|
1428
1372
|
};
|
|
1429
1373
|
const onAbort = () => {
|
|
1430
1374
|
killTree2(child);
|
|
1431
|
-
finish({ content: `${
|
|
1375
|
+
finish({ content: `${output}
|
|
1432
1376
|
Command aborted`, isError: true });
|
|
1433
1377
|
};
|
|
1434
1378
|
const timer = setTimeout(() => {
|
|
1435
1379
|
killTree2(child);
|
|
1436
1380
|
finish({
|
|
1437
|
-
content: `${
|
|
1381
|
+
content: `${output}
|
|
1438
1382
|
Command timed out after ${timeoutSeconds}s`,
|
|
1439
1383
|
isError: true
|
|
1440
1384
|
});
|
|
1441
1385
|
}, timeoutSeconds * 1e3);
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
child.stderr.on("data", (d) => {
|
|
1446
|
-
output += d.toString("utf8");
|
|
1447
|
-
});
|
|
1386
|
+
timer.unref();
|
|
1387
|
+
child.stdout.on("data", (d) => output.append(d.toString("utf8")));
|
|
1388
|
+
child.stderr.on("data", (d) => output.append(d.toString("utf8")));
|
|
1448
1389
|
child.on("error", (err) => {
|
|
1449
1390
|
finish({ content: `Failed to start shell '${spec.label}': ${err.message}`, isError: true });
|
|
1450
1391
|
});
|
|
1451
1392
|
ctx.abortSignal?.addEventListener("abort", onAbort);
|
|
1452
1393
|
child.on("close", (code) => {
|
|
1453
|
-
const trimmed = output.replace(/\s+$/, "");
|
|
1394
|
+
const trimmed = output.toString().replace(/\s+$/, "");
|
|
1454
1395
|
if (code === 0) {
|
|
1455
|
-
finish({ content:
|
|
1396
|
+
finish({ content: trimmed || "(no output)" });
|
|
1456
1397
|
return;
|
|
1457
1398
|
}
|
|
1458
|
-
const body = truncateMiddle(trimmed);
|
|
1459
1399
|
finish({
|
|
1460
|
-
content: `${
|
|
1400
|
+
content: `${trimmed}${trimmed ? "\n" : ""}Exit code: ${code ?? "unknown"}`,
|
|
1461
1401
|
isError: true
|
|
1462
1402
|
});
|
|
1463
1403
|
});
|
|
@@ -1466,14 +1406,14 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
1466
1406
|
};
|
|
1467
1407
|
|
|
1468
1408
|
// src/tools/fs/edit.ts
|
|
1469
|
-
import { readFile as
|
|
1470
|
-
import
|
|
1471
|
-
import { z as
|
|
1472
|
-
var schema2 =
|
|
1473
|
-
path:
|
|
1474
|
-
old_string:
|
|
1475
|
-
new_string:
|
|
1476
|
-
replace_all:
|
|
1409
|
+
import { readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
1410
|
+
import path5 from "path";
|
|
1411
|
+
import { z as z2 } from "zod";
|
|
1412
|
+
var schema2 = z2.object({
|
|
1413
|
+
path: z2.string().describe("File path, absolute or relative to the working directory"),
|
|
1414
|
+
old_string: z2.string().min(1).describe("Exact text to replace"),
|
|
1415
|
+
new_string: z2.string().describe("Replacement text"),
|
|
1416
|
+
replace_all: z2.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
|
|
1477
1417
|
});
|
|
1478
1418
|
var editFileTool = {
|
|
1479
1419
|
name: "edit_file",
|
|
@@ -1481,10 +1421,12 @@ var editFileTool = {
|
|
|
1481
1421
|
permission: "write",
|
|
1482
1422
|
parameters: schema2,
|
|
1483
1423
|
async execute(args, ctx) {
|
|
1484
|
-
const filePath =
|
|
1424
|
+
const filePath = path5.resolve(ctx.cwd, args.path);
|
|
1485
1425
|
let content;
|
|
1426
|
+
let readMtimeMs;
|
|
1486
1427
|
try {
|
|
1487
|
-
content = await
|
|
1428
|
+
content = await readFile2(filePath, "utf8");
|
|
1429
|
+
readMtimeMs = (await stat2(filePath)).mtimeMs;
|
|
1488
1430
|
} catch (err) {
|
|
1489
1431
|
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
1490
1432
|
}
|
|
@@ -1503,34 +1445,44 @@ var editFileTool = {
|
|
|
1503
1445
|
isError: true
|
|
1504
1446
|
};
|
|
1505
1447
|
}
|
|
1506
|
-
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
1448
|
+
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, () => args.new_string);
|
|
1507
1449
|
try {
|
|
1508
|
-
await
|
|
1450
|
+
const now = await stat2(filePath).catch(() => null);
|
|
1451
|
+
if (!now || now.mtimeMs !== readMtimeMs) {
|
|
1452
|
+
return {
|
|
1453
|
+
content: `Failed to edit ${args.path}: file changed since it was read; re-read and retry`,
|
|
1454
|
+
isError: true
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
await writeFile2(filePath, updated, "utf8");
|
|
1509
1458
|
} catch (err) {
|
|
1510
1459
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1511
1460
|
}
|
|
1461
|
+
const contentTooLarge = Buffer.byteLength(content, "utf8") > MAX_SNAPSHOT_CONTENT_BYTES;
|
|
1512
1462
|
await pushSnapshot({
|
|
1513
1463
|
id: nextSnapshotId(),
|
|
1514
1464
|
path: filePath,
|
|
1515
1465
|
existed: true,
|
|
1516
|
-
content,
|
|
1466
|
+
content: contentTooLarge ? null : content,
|
|
1467
|
+
contentTooLarge: contentTooLarge || void 0,
|
|
1517
1468
|
toolName: "edit_file",
|
|
1518
1469
|
timestamp: Date.now(),
|
|
1519
|
-
turn: currentTurnSeq(),
|
|
1520
|
-
messageIndex: currentTurnMessageIndex()
|
|
1470
|
+
turn: ctx.snapshotContext?.turn ?? currentTurnSeq(),
|
|
1471
|
+
messageIndex: ctx.snapshotContext?.messageIndex ?? currentTurnMessageIndex(),
|
|
1472
|
+
owner: ctx.snapshotContext?.owner ?? "root"
|
|
1521
1473
|
});
|
|
1522
1474
|
return { content: `Edited ${args.path}: ${count} replacement${count > 1 ? "s" : ""}` };
|
|
1523
1475
|
}
|
|
1524
1476
|
};
|
|
1525
1477
|
|
|
1526
1478
|
// src/tools/fs/glob.ts
|
|
1527
|
-
import
|
|
1528
|
-
import { z as
|
|
1479
|
+
import path7 from "path";
|
|
1480
|
+
import { z as z3 } from "zod";
|
|
1529
1481
|
|
|
1530
1482
|
// src/tools/fs/util.ts
|
|
1531
|
-
import { readFileSync
|
|
1532
|
-
import { readdir, stat } from "fs/promises";
|
|
1533
|
-
import
|
|
1483
|
+
import { readFileSync, statSync } from "fs";
|
|
1484
|
+
import { readdir, stat as stat3 } from "fs/promises";
|
|
1485
|
+
import path6 from "path";
|
|
1534
1486
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1535
1487
|
async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
1536
1488
|
const out = [];
|
|
@@ -1540,7 +1492,7 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
|
1540
1492
|
return;
|
|
1541
1493
|
}
|
|
1542
1494
|
for (const entry of entries) {
|
|
1543
|
-
const abs =
|
|
1495
|
+
const abs = path6.join(dir, entry.name);
|
|
1544
1496
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1545
1497
|
if (entry.isDirectory()) {
|
|
1546
1498
|
if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
|
|
@@ -1551,8 +1503,12 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
|
1551
1503
|
if (options.ignore?.(abs, false)) {
|
|
1552
1504
|
continue;
|
|
1553
1505
|
}
|
|
1506
|
+
if (options.withMtime === false) {
|
|
1507
|
+
out.push({ abs, rel, mtimeMs: 0 });
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1554
1510
|
try {
|
|
1555
|
-
const st = await
|
|
1511
|
+
const st = await stat3(abs);
|
|
1556
1512
|
out.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
1557
1513
|
} catch {
|
|
1558
1514
|
}
|
|
@@ -1563,32 +1519,107 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
|
1563
1519
|
return out;
|
|
1564
1520
|
}
|
|
1565
1521
|
var REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
1522
|
+
function escapeRe(c) {
|
|
1523
|
+
return c.replace(REGEX_SPECIALS, "\\$&");
|
|
1524
|
+
}
|
|
1566
1525
|
function globToRegExp(pattern) {
|
|
1567
|
-
let re = "";
|
|
1568
1526
|
let i = 0;
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1527
|
+
function parseClass() {
|
|
1528
|
+
let j = i + 1;
|
|
1529
|
+
let negate = false;
|
|
1530
|
+
if (pattern.charAt(j) === "!") {
|
|
1531
|
+
negate = true;
|
|
1532
|
+
j += 1;
|
|
1533
|
+
}
|
|
1534
|
+
let body = "";
|
|
1535
|
+
if (pattern.charAt(j) === "]") {
|
|
1536
|
+
body += "\\]";
|
|
1537
|
+
j += 1;
|
|
1538
|
+
}
|
|
1539
|
+
while (j < pattern.length && pattern.charAt(j) !== "]") {
|
|
1540
|
+
const ch = pattern.charAt(j);
|
|
1541
|
+
body += ch === "\\" ? "\\\\" : ch;
|
|
1542
|
+
j += 1;
|
|
1543
|
+
}
|
|
1544
|
+
if (j < pattern.length) {
|
|
1545
|
+
i = j + 1;
|
|
1546
|
+
if (body.startsWith("^")) body = `\\${body}`;
|
|
1547
|
+
return `[${negate ? "^" : ""}${body}]`;
|
|
1548
|
+
}
|
|
1549
|
+
i += 1;
|
|
1550
|
+
return "\\[";
|
|
1551
|
+
}
|
|
1552
|
+
function parseSegment(depth) {
|
|
1553
|
+
let re2 = "";
|
|
1554
|
+
while (i < pattern.length) {
|
|
1555
|
+
const c = pattern.charAt(i);
|
|
1556
|
+
if (depth > 0 && (c === "," || c === "}")) {
|
|
1557
|
+
return { re: re2, end: c };
|
|
1558
|
+
}
|
|
1559
|
+
if (c === "*") {
|
|
1560
|
+
if (pattern.charAt(i + 1) === "*") {
|
|
1561
|
+
i += 2;
|
|
1562
|
+
if (pattern.charAt(i) === "/") {
|
|
1563
|
+
i += 1;
|
|
1564
|
+
re2 += "(?:[^/]+/)*";
|
|
1565
|
+
while (pattern.startsWith("**/", i)) {
|
|
1566
|
+
i += 3;
|
|
1567
|
+
}
|
|
1568
|
+
} else {
|
|
1569
|
+
re2 += ".*";
|
|
1570
|
+
}
|
|
1577
1571
|
} else {
|
|
1578
|
-
|
|
1572
|
+
re2 += "[^/]*";
|
|
1573
|
+
i += 1;
|
|
1579
1574
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (c === "?") {
|
|
1578
|
+
re2 += "[^/]";
|
|
1582
1579
|
i += 1;
|
|
1580
|
+
continue;
|
|
1583
1581
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1582
|
+
if (c === "[") {
|
|
1583
|
+
re2 += parseClass();
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
if (c === "{" && depth < 2) {
|
|
1587
|
+
const close = braceEnd(i);
|
|
1588
|
+
if (close === -1) {
|
|
1589
|
+
re2 += "\\{";
|
|
1590
|
+
i += 1;
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
i += 1;
|
|
1594
|
+
const alts = [];
|
|
1595
|
+
for (; ; ) {
|
|
1596
|
+
const part = parseSegment(depth + 1);
|
|
1597
|
+
alts.push(part.re);
|
|
1598
|
+
if (part.end !== ",") break;
|
|
1599
|
+
i += 1;
|
|
1600
|
+
}
|
|
1601
|
+
i += 1;
|
|
1602
|
+
re2 += `(?:${alts.join("|")})`;
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
re2 += escapeRe(c);
|
|
1589
1606
|
i += 1;
|
|
1590
1607
|
}
|
|
1608
|
+
return { re: re2, end: "" };
|
|
1609
|
+
}
|
|
1610
|
+
function braceEnd(from) {
|
|
1611
|
+
let depth = 0;
|
|
1612
|
+
for (let j = from; j < pattern.length; j++) {
|
|
1613
|
+
const ch = pattern.charAt(j);
|
|
1614
|
+
if (ch === "{") depth += 1;
|
|
1615
|
+
else if (ch === "}") {
|
|
1616
|
+
depth -= 1;
|
|
1617
|
+
if (depth === 0) return j;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return -1;
|
|
1591
1621
|
}
|
|
1622
|
+
const { re } = parseSegment(0);
|
|
1592
1623
|
return new RegExp(`^${re}$`);
|
|
1593
1624
|
}
|
|
1594
1625
|
function matchesGlob(pattern, relPath) {
|
|
@@ -1620,10 +1651,10 @@ function parseIgnorePatterns(body) {
|
|
|
1620
1651
|
return out;
|
|
1621
1652
|
}
|
|
1622
1653
|
function loadIgnorePatterns(dir) {
|
|
1623
|
-
const file =
|
|
1654
|
+
const file = path6.join(dir, IGNORE_FILE);
|
|
1624
1655
|
let mtimeMs;
|
|
1625
1656
|
try {
|
|
1626
|
-
mtimeMs =
|
|
1657
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
1627
1658
|
} catch {
|
|
1628
1659
|
return [];
|
|
1629
1660
|
}
|
|
@@ -1633,7 +1664,7 @@ function loadIgnorePatterns(dir) {
|
|
|
1633
1664
|
}
|
|
1634
1665
|
let patterns = [];
|
|
1635
1666
|
try {
|
|
1636
|
-
patterns = parseIgnorePatterns(
|
|
1667
|
+
patterns = parseIgnorePatterns(readFileSync(file, "utf8"));
|
|
1637
1668
|
} catch {
|
|
1638
1669
|
patterns = [];
|
|
1639
1670
|
}
|
|
@@ -1645,9 +1676,9 @@ function createIgnorePredicate(cwd) {
|
|
|
1645
1676
|
if (patterns.length === 0) {
|
|
1646
1677
|
return void 0;
|
|
1647
1678
|
}
|
|
1648
|
-
const root =
|
|
1679
|
+
const root = path6.resolve(cwd);
|
|
1649
1680
|
return (absPath, isDir) => {
|
|
1650
|
-
const rel =
|
|
1681
|
+
const rel = path6.relative(root, absPath).split(path6.sep).join("/");
|
|
1651
1682
|
if (!rel || rel.startsWith("..")) {
|
|
1652
1683
|
return false;
|
|
1653
1684
|
}
|
|
@@ -1662,27 +1693,26 @@ function createIgnorePredicate(cwd) {
|
|
|
1662
1693
|
return false;
|
|
1663
1694
|
};
|
|
1664
1695
|
}
|
|
1665
|
-
function isSensitivePath2(filePath) {
|
|
1666
|
-
const base = path8.basename(filePath);
|
|
1667
|
-
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
1668
|
-
return false;
|
|
1669
|
-
}
|
|
1670
|
-
if (base === ".env" || base.startsWith(".env.")) {
|
|
1671
|
-
return true;
|
|
1672
|
-
}
|
|
1673
|
-
if (base === "id_rsa" || base.endsWith(".pem")) {
|
|
1674
|
-
return true;
|
|
1675
|
-
}
|
|
1676
|
-
return false;
|
|
1677
|
-
}
|
|
1678
1696
|
|
|
1679
1697
|
// src/tools/fs/glob.ts
|
|
1680
1698
|
var MAX_RESULTS = 100;
|
|
1681
|
-
var
|
|
1682
|
-
|
|
1683
|
-
|
|
1699
|
+
var GLOB_MAGIC = /[*?[{]/;
|
|
1700
|
+
function literalDirPrefix(pattern) {
|
|
1701
|
+
const segments = pattern.replace(/\\/g, "/").split("/").slice(0, -1);
|
|
1702
|
+
const out = [];
|
|
1703
|
+
for (const segment of segments) {
|
|
1704
|
+
if (segment === "" || GLOB_MAGIC.test(segment)) {
|
|
1705
|
+
break;
|
|
1706
|
+
}
|
|
1707
|
+
out.push(segment);
|
|
1708
|
+
}
|
|
1709
|
+
return out;
|
|
1710
|
+
}
|
|
1711
|
+
var schema3 = z3.object({
|
|
1712
|
+
pattern: z3.string().describe(
|
|
1713
|
+
"Glob pattern, e.g. 'src/**/*.ts'. Supports **, *, ?, {a,b} braces and [abc] character classes. A bare pattern like '*.ts' matches basenames recursively."
|
|
1684
1714
|
),
|
|
1685
|
-
path:
|
|
1715
|
+
path: z3.string().optional().describe("Directory to search, defaults to the working directory")
|
|
1686
1716
|
});
|
|
1687
1717
|
var globTool = {
|
|
1688
1718
|
name: "glob",
|
|
@@ -1690,47 +1720,72 @@ var globTool = {
|
|
|
1690
1720
|
permission: "read",
|
|
1691
1721
|
parameters: schema3,
|
|
1692
1722
|
async execute(args, ctx) {
|
|
1693
|
-
const root =
|
|
1694
|
-
const
|
|
1695
|
-
const
|
|
1723
|
+
const root = path7.resolve(ctx.cwd, args.path ?? ".");
|
|
1724
|
+
const prefixSegments = literalDirPrefix(args.pattern);
|
|
1725
|
+
const walkRoot = prefixSegments.length > 0 ? path7.join(root, ...prefixSegments) : root;
|
|
1726
|
+
const prefix = prefixSegments.length > 0 ? `${prefixSegments.join("/")}/` : "";
|
|
1727
|
+
const files = await walkFiles(walkRoot, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1728
|
+
const matched = files.filter((f) => matchesGlob(args.pattern, `${prefix}${f.rel}`)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
1696
1729
|
if (matched.length === 0) {
|
|
1697
1730
|
return { content: `No files matched pattern: ${args.pattern}` };
|
|
1698
1731
|
}
|
|
1699
|
-
return { content: matched.map((f) => f.rel).join("\n") };
|
|
1732
|
+
return { content: matched.map((f) => `${prefix}${f.rel}`).join("\n") };
|
|
1700
1733
|
}
|
|
1701
1734
|
};
|
|
1702
1735
|
|
|
1703
1736
|
// src/tools/fs/grep.ts
|
|
1704
|
-
import { readFile as
|
|
1705
|
-
import
|
|
1706
|
-
import { z as
|
|
1737
|
+
import { readFile as readFile3, stat as stat4 } from "fs/promises";
|
|
1738
|
+
import path8 from "path";
|
|
1739
|
+
import { z as z4 } from "zod";
|
|
1707
1740
|
var MAX_MATCHES = 250;
|
|
1708
|
-
var
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1741
|
+
var MAX_LINE_CHARS = 500;
|
|
1742
|
+
var MAX_PATTERN_CHARS = 200;
|
|
1743
|
+
var MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
1744
|
+
function isTooComplexPattern(pattern) {
|
|
1745
|
+
return pattern.length > MAX_PATTERN_CHARS || /\)[+*]/.test(pattern) || /\)\{\d+(,\d*)?\}/.test(pattern);
|
|
1746
|
+
}
|
|
1747
|
+
var schema4 = z4.object({
|
|
1748
|
+
pattern: z4.string().describe("Regular expression to search for"),
|
|
1749
|
+
path: z4.string().optional().describe("File or directory to search, defaults to the working directory"),
|
|
1750
|
+
glob: z4.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
|
|
1751
|
+
ignoreCase: z4.boolean().optional().describe("Case-insensitive matching")
|
|
1713
1752
|
});
|
|
1714
1753
|
var grepTool = {
|
|
1715
1754
|
name: "grep",
|
|
1716
|
-
description: "Search file contents with a regular expression. Outputs 'file:line:content', up to 250 matches. Skips binary files, node_modules and .git.",
|
|
1755
|
+
description: "Search file contents with a regular expression. Outputs 'file:line:content', up to 250 matches. Skips binary files, files over 5MB, sensitive files, node_modules and .git.",
|
|
1717
1756
|
permission: "read",
|
|
1718
1757
|
parameters: schema4,
|
|
1719
1758
|
async execute(args, ctx) {
|
|
1759
|
+
if (isTooComplexPattern(args.pattern)) {
|
|
1760
|
+
return {
|
|
1761
|
+
content: `pattern too complex: keep it under ${MAX_PATTERN_CHARS} characters and avoid quantified groups like (a+)+`,
|
|
1762
|
+
isError: true
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1720
1765
|
let re;
|
|
1721
1766
|
try {
|
|
1722
1767
|
re = new RegExp(args.pattern, args.ignoreCase ? "i" : "");
|
|
1723
1768
|
} catch (err) {
|
|
1724
1769
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
1725
1770
|
}
|
|
1726
|
-
const root =
|
|
1771
|
+
const root = path8.resolve(ctx.cwd, args.path ?? ".");
|
|
1727
1772
|
let files;
|
|
1728
1773
|
try {
|
|
1729
|
-
const st = await
|
|
1774
|
+
const st = await stat4(root);
|
|
1730
1775
|
if (st.isFile()) {
|
|
1731
|
-
|
|
1776
|
+
if (isSensitivePath(root)) {
|
|
1777
|
+
return {
|
|
1778
|
+
content: `Refused to grep sensitive file: ${args.path ?? "."}`,
|
|
1779
|
+
isError: true
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
files = [{ abs: root, rel: path8.basename(root), mtimeMs: st.mtimeMs }];
|
|
1732
1783
|
} else {
|
|
1733
|
-
|
|
1784
|
+
const walked = await walkFiles(root, void 0, {
|
|
1785
|
+
ignore: createIgnorePredicate(ctx.cwd),
|
|
1786
|
+
withMtime: false
|
|
1787
|
+
});
|
|
1788
|
+
files = walked.filter((f) => !isSensitivePath(f.abs));
|
|
1734
1789
|
}
|
|
1735
1790
|
} catch {
|
|
1736
1791
|
return { content: `Path not found: ${args.path ?? "."}`, isError: true };
|
|
@@ -1741,10 +1796,19 @@ var grepTool = {
|
|
|
1741
1796
|
}
|
|
1742
1797
|
const out = [];
|
|
1743
1798
|
let truncated = false;
|
|
1799
|
+
let skippedLarge = 0;
|
|
1744
1800
|
outer: for (const file of files) {
|
|
1801
|
+
const st = await stat4(file.abs).catch(() => null);
|
|
1802
|
+
if (!st) {
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
if (st.size > MAX_FILE_BYTES) {
|
|
1806
|
+
skippedLarge += 1;
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1745
1809
|
let buf;
|
|
1746
1810
|
try {
|
|
1747
|
-
buf = await
|
|
1811
|
+
buf = await readFile3(file.abs);
|
|
1748
1812
|
} catch {
|
|
1749
1813
|
continue;
|
|
1750
1814
|
}
|
|
@@ -1755,7 +1819,8 @@ var grepTool = {
|
|
|
1755
1819
|
for (let i = 0; i < lines.length; i++) {
|
|
1756
1820
|
const line = lines[i] ?? "";
|
|
1757
1821
|
if (re.test(line)) {
|
|
1758
|
-
|
|
1822
|
+
const shown = line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
|
|
1823
|
+
out.push(`${file.rel}:${i + 1}:${shown}`);
|
|
1759
1824
|
if (out.length >= MAX_MATCHES) {
|
|
1760
1825
|
truncated = true;
|
|
1761
1826
|
break outer;
|
|
@@ -1763,26 +1828,55 @@ var grepTool = {
|
|
|
1763
1828
|
}
|
|
1764
1829
|
}
|
|
1765
1830
|
}
|
|
1831
|
+
const skippedNote = skippedLarge > 0 ? `(skipped ${skippedLarge} file(s) larger than 5MB)` : null;
|
|
1766
1832
|
if (out.length === 0) {
|
|
1767
|
-
return {
|
|
1833
|
+
return {
|
|
1834
|
+
content: `No matches for pattern: ${args.pattern}${skippedNote ? ` ${skippedNote}` : ""}`
|
|
1835
|
+
};
|
|
1768
1836
|
}
|
|
1769
1837
|
if (truncated) {
|
|
1770
1838
|
out.push(`... (truncated: more than ${MAX_MATCHES} matches)`);
|
|
1771
1839
|
}
|
|
1840
|
+
if (skippedNote) {
|
|
1841
|
+
out.push(`... ${skippedNote}`);
|
|
1842
|
+
}
|
|
1772
1843
|
return { content: out.join("\n") };
|
|
1773
1844
|
}
|
|
1774
1845
|
};
|
|
1775
1846
|
|
|
1776
1847
|
// src/tools/fs/read.ts
|
|
1777
|
-
import {
|
|
1778
|
-
import
|
|
1779
|
-
import { z as
|
|
1848
|
+
import { open, stat as stat5 } from "fs/promises";
|
|
1849
|
+
import path9 from "path";
|
|
1850
|
+
import { z as z5 } from "zod";
|
|
1780
1851
|
var DEFAULT_LIMIT = 2e3;
|
|
1781
1852
|
var MAX_CHARS = 100 * 1024;
|
|
1782
|
-
var
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1853
|
+
var MAX_READ_BYTES = 1024 * 1024;
|
|
1854
|
+
var BINARY_PROBE_BYTES = 8192;
|
|
1855
|
+
function decodeUtf16be(buf) {
|
|
1856
|
+
const len = buf.length - buf.length % 2;
|
|
1857
|
+
const swapped = Buffer.allocUnsafe(len);
|
|
1858
|
+
for (let k = 0; k < len; k += 2) {
|
|
1859
|
+
swapped[k] = buf[k + 1] ?? 0;
|
|
1860
|
+
swapped[k + 1] = buf[k] ?? 0;
|
|
1861
|
+
}
|
|
1862
|
+
return swapped.toString("utf16le");
|
|
1863
|
+
}
|
|
1864
|
+
function decodeTextBuffer(buf) {
|
|
1865
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) {
|
|
1866
|
+
return { text: buf.subarray(2).toString("utf16le") };
|
|
1867
|
+
}
|
|
1868
|
+
if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) {
|
|
1869
|
+
return { text: decodeUtf16be(buf.subarray(2)) };
|
|
1870
|
+
}
|
|
1871
|
+
if (buf.subarray(0, BINARY_PROBE_BYTES).includes(0)) {
|
|
1872
|
+
return { binary: true };
|
|
1873
|
+
}
|
|
1874
|
+
return { text: buf.toString("utf8") };
|
|
1875
|
+
}
|
|
1876
|
+
var schema5 = z5.object({
|
|
1877
|
+
path: z5.string().describe("File path, absolute or relative to the working directory"),
|
|
1878
|
+
offset: z5.number().int().positive().optional().describe("1-based line number to start reading from"),
|
|
1879
|
+
limit: z5.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
|
|
1786
1880
|
});
|
|
1787
1881
|
var readFileTool = {
|
|
1788
1882
|
name: "read_file",
|
|
@@ -1790,30 +1884,48 @@ var readFileTool = {
|
|
|
1790
1884
|
permission: "read",
|
|
1791
1885
|
parameters: schema5,
|
|
1792
1886
|
async execute(args, ctx) {
|
|
1793
|
-
const filePath =
|
|
1794
|
-
if (
|
|
1887
|
+
const filePath = path9.resolve(ctx.cwd, args.path);
|
|
1888
|
+
if (isSensitivePath(filePath)) {
|
|
1795
1889
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
1796
1890
|
}
|
|
1797
|
-
const st = await
|
|
1891
|
+
const st = await stat5(filePath).catch(() => null);
|
|
1798
1892
|
if (!st) {
|
|
1799
1893
|
return { content: `File not found: ${args.path}`, isError: true };
|
|
1800
1894
|
}
|
|
1801
1895
|
if (st.isDirectory()) {
|
|
1802
1896
|
return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
|
|
1803
1897
|
}
|
|
1804
|
-
const
|
|
1805
|
-
const
|
|
1898
|
+
const bytesToRead = Math.min(st.size, MAX_READ_BYTES);
|
|
1899
|
+
const fileCapped = st.size > MAX_READ_BYTES;
|
|
1900
|
+
const handle = await open(filePath, "r").catch(() => null);
|
|
1901
|
+
if (!handle) {
|
|
1902
|
+
return { content: `Failed to read ${args.path}`, isError: true };
|
|
1903
|
+
}
|
|
1904
|
+
let buf;
|
|
1905
|
+
try {
|
|
1906
|
+
const { bytesRead, buffer } = await handle.read(Buffer.alloc(bytesToRead), 0, bytesToRead, 0);
|
|
1907
|
+
buf = buffer.subarray(0, bytesRead);
|
|
1908
|
+
} finally {
|
|
1909
|
+
await handle.close().catch(() => {
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
const decoded = decodeTextBuffer(buf);
|
|
1913
|
+
if ("binary" in decoded) {
|
|
1914
|
+
return { content: `Cannot read binary file: ${args.path}`, isError: true };
|
|
1915
|
+
}
|
|
1916
|
+
const lines = decoded.text.split("\n");
|
|
1917
|
+
const totalDesc = fileCapped ? `at least ${lines.length}` : `${lines.length}`;
|
|
1806
1918
|
const start = args.offset ?? 1;
|
|
1807
1919
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
1920
|
+
if (start > lines.length) {
|
|
1921
|
+
return { content: `(offset ${start} beyond end of file; file has ${totalDesc} lines)` };
|
|
1922
|
+
}
|
|
1808
1923
|
const end = Math.min(lines.length, start - 1 + limit);
|
|
1809
1924
|
const out = [];
|
|
1810
1925
|
let size = 0;
|
|
1811
1926
|
let truncatedBySize = false;
|
|
1812
1927
|
for (let i = start - 1; i < end; i++) {
|
|
1813
|
-
const line = lines[i];
|
|
1814
|
-
if (line === void 0) {
|
|
1815
|
-
break;
|
|
1816
|
-
}
|
|
1928
|
+
const line = (lines[i] ?? "").replace(/\r$/, "");
|
|
1817
1929
|
const numbered = `${i + 1} ${line}`;
|
|
1818
1930
|
if (size + numbered.length > MAX_CHARS) {
|
|
1819
1931
|
truncatedBySize = true;
|
|
@@ -1825,22 +1937,22 @@ var readFileTool = {
|
|
|
1825
1937
|
const lastShown = start - 1 + out.length;
|
|
1826
1938
|
if (truncatedBySize) {
|
|
1827
1939
|
out.push(
|
|
1828
|
-
`... (truncated: output exceeds 100KB, showing lines ${start}-${lastShown} of ${
|
|
1940
|
+
`... (truncated: output exceeds 100KB, showing lines ${start}-${lastShown} of ${totalDesc})`
|
|
1829
1941
|
);
|
|
1830
|
-
} else if (end < lines.length) {
|
|
1831
|
-
out.push(`... (truncated: showing lines ${start}-${end} of ${
|
|
1942
|
+
} else if (end < lines.length || fileCapped) {
|
|
1943
|
+
out.push(`... (truncated: showing lines ${start}-${end} of ${totalDesc})`);
|
|
1832
1944
|
}
|
|
1833
1945
|
return { content: out.join("\n") };
|
|
1834
1946
|
}
|
|
1835
1947
|
};
|
|
1836
1948
|
|
|
1837
1949
|
// src/tools/fs/write.ts
|
|
1838
|
-
import { mkdir as mkdir3, writeFile as
|
|
1839
|
-
import
|
|
1840
|
-
import { z as
|
|
1841
|
-
var schema6 =
|
|
1842
|
-
path:
|
|
1843
|
-
content:
|
|
1950
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1951
|
+
import path10 from "path";
|
|
1952
|
+
import { z as z6 } from "zod";
|
|
1953
|
+
var schema6 = z6.object({
|
|
1954
|
+
path: z6.string().describe("File path, absolute or relative to the working directory"),
|
|
1955
|
+
content: z6.string().describe("Full content to write to the file")
|
|
1844
1956
|
});
|
|
1845
1957
|
var writeFileTool = {
|
|
1846
1958
|
name: "write_file",
|
|
@@ -1848,11 +1960,11 @@ var writeFileTool = {
|
|
|
1848
1960
|
permission: "write",
|
|
1849
1961
|
parameters: schema6,
|
|
1850
1962
|
async execute(args, ctx) {
|
|
1851
|
-
const filePath =
|
|
1852
|
-
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
1963
|
+
const filePath = path10.resolve(ctx.cwd, args.path);
|
|
1964
|
+
const snapshot = await captureSnapshot(filePath, "write_file", ctx.snapshotContext);
|
|
1853
1965
|
try {
|
|
1854
|
-
await mkdir3(
|
|
1855
|
-
await
|
|
1966
|
+
await mkdir3(path10.dirname(filePath), { recursive: true });
|
|
1967
|
+
await writeFile3(filePath, args.content, "utf8");
|
|
1856
1968
|
} catch (err) {
|
|
1857
1969
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1858
1970
|
}
|
|
@@ -1862,7 +1974,118 @@ var writeFileTool = {
|
|
|
1862
1974
|
};
|
|
1863
1975
|
|
|
1864
1976
|
// src/tools/tasks.ts
|
|
1865
|
-
import { z as
|
|
1977
|
+
import { z as z7 } from "zod";
|
|
1978
|
+
|
|
1979
|
+
// src/agent/agent-tasks.ts
|
|
1980
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1981
|
+
var MAX_FINISHED_RECORDS2 = 50;
|
|
1982
|
+
var AgentTaskManager = class extends EventEmitter2 {
|
|
1983
|
+
records = /* @__PURE__ */ new Map();
|
|
1984
|
+
counter = 0;
|
|
1985
|
+
start(run, meta) {
|
|
1986
|
+
const id = `agent-${++this.counter}`;
|
|
1987
|
+
const rec = {
|
|
1988
|
+
id,
|
|
1989
|
+
prompt: meta.prompt,
|
|
1990
|
+
description: meta.description,
|
|
1991
|
+
status: "running",
|
|
1992
|
+
startedAt: Date.now(),
|
|
1993
|
+
result: "",
|
|
1994
|
+
controller: new AbortController(),
|
|
1995
|
+
notified: false
|
|
1996
|
+
};
|
|
1997
|
+
this.records.set(id, rec);
|
|
1998
|
+
void run(rec.controller.signal).then((result) => this.finish(rec, "completed", result)).catch(
|
|
1999
|
+
(error) => this.finish(rec, "failed", error instanceof Error ? error.message : String(error))
|
|
2000
|
+
);
|
|
2001
|
+
this.emitUpdate(rec);
|
|
2002
|
+
return this.snapshot(rec);
|
|
2003
|
+
}
|
|
2004
|
+
get(id) {
|
|
2005
|
+
const rec = this.records.get(id);
|
|
2006
|
+
return rec ? this.snapshot(rec) : void 0;
|
|
2007
|
+
}
|
|
2008
|
+
list() {
|
|
2009
|
+
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
2010
|
+
}
|
|
2011
|
+
runningCount() {
|
|
2012
|
+
let count = 0;
|
|
2013
|
+
for (const rec of this.records.values()) {
|
|
2014
|
+
if (rec.status === "running") count += 1;
|
|
2015
|
+
}
|
|
2016
|
+
return count;
|
|
2017
|
+
}
|
|
2018
|
+
kill(id) {
|
|
2019
|
+
const rec = this.records.get(id);
|
|
2020
|
+
if (!rec || rec.status !== "running") return void 0;
|
|
2021
|
+
rec.controller.abort();
|
|
2022
|
+
this.finish(rec, "stopped", rec.result || "Stopped by user.");
|
|
2023
|
+
return this.snapshot(rec);
|
|
2024
|
+
}
|
|
2025
|
+
cleanup() {
|
|
2026
|
+
const killed = [];
|
|
2027
|
+
for (const rec of this.records.values()) {
|
|
2028
|
+
if (rec.status === "running") {
|
|
2029
|
+
rec.controller.abort();
|
|
2030
|
+
this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
|
|
2031
|
+
killed.push(this.snapshot(rec));
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return killed;
|
|
2035
|
+
}
|
|
2036
|
+
// Finished tasks whose result has not yet been handed to the parent loop.
|
|
2037
|
+
drainNotifications() {
|
|
2038
|
+
const drained = [];
|
|
2039
|
+
for (const rec of this.records.values()) {
|
|
2040
|
+
if (rec.status !== "running" && !rec.notified) {
|
|
2041
|
+
rec.notified = true;
|
|
2042
|
+
drained.push(this.snapshot(rec));
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
this.pruneFinished();
|
|
2046
|
+
return drained;
|
|
2047
|
+
}
|
|
2048
|
+
// Map iteration is insertion-ordered, so the first finished records met are
|
|
2049
|
+
// the oldest. Never drop an un-notified record: the parent has not seen its
|
|
2050
|
+
// result yet. Running records are untouched.
|
|
2051
|
+
pruneFinished() {
|
|
2052
|
+
let finished = 0;
|
|
2053
|
+
for (const rec of this.records.values()) {
|
|
2054
|
+
if (rec.status !== "running") finished += 1;
|
|
2055
|
+
}
|
|
2056
|
+
let excess = finished - MAX_FINISHED_RECORDS2;
|
|
2057
|
+
if (excess <= 0) return;
|
|
2058
|
+
for (const rec of this.records.values()) {
|
|
2059
|
+
if (excess <= 0) return;
|
|
2060
|
+
if (rec.status !== "running" && rec.notified) {
|
|
2061
|
+
this.records.delete(rec.id);
|
|
2062
|
+
excess -= 1;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
finish(rec, status, result) {
|
|
2067
|
+
if (rec.status !== "running") return;
|
|
2068
|
+
rec.status = status;
|
|
2069
|
+
rec.result = result;
|
|
2070
|
+
rec.endedAt = Date.now();
|
|
2071
|
+
this.emitUpdate(rec);
|
|
2072
|
+
}
|
|
2073
|
+
emitUpdate(rec) {
|
|
2074
|
+
this.emit("update", this.snapshot(rec));
|
|
2075
|
+
}
|
|
2076
|
+
snapshot(rec) {
|
|
2077
|
+
return {
|
|
2078
|
+
id: rec.id,
|
|
2079
|
+
prompt: rec.prompt,
|
|
2080
|
+
description: rec.description,
|
|
2081
|
+
status: rec.status,
|
|
2082
|
+
startedAt: rec.startedAt,
|
|
2083
|
+
endedAt: rec.endedAt,
|
|
2084
|
+
result: rec.result
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
var defaultAgentTasks = new AgentTaskManager();
|
|
1866
2089
|
|
|
1867
2090
|
// src/tasks/format.ts
|
|
1868
2091
|
function formatDuration(startedAt, endedAt) {
|
|
@@ -1908,9 +2131,14 @@ function formatTaskFinished(task) {
|
|
|
1908
2131
|
}
|
|
1909
2132
|
|
|
1910
2133
|
// src/tools/tasks.ts
|
|
1911
|
-
var idSchema =
|
|
1912
|
-
id:
|
|
2134
|
+
var idSchema = z7.object({
|
|
2135
|
+
id: z7.string().describe("Task id, e.g. task-1 or agent-1")
|
|
1913
2136
|
});
|
|
2137
|
+
function truncateOutput(s) {
|
|
2138
|
+
if (s.length <= MAX_OUTPUT) return s;
|
|
2139
|
+
return `[... earlier output truncated; showing the last ${MAX_OUTPUT} characters ...]
|
|
2140
|
+
${s.slice(-MAX_OUTPUT)}`;
|
|
2141
|
+
}
|
|
1914
2142
|
function formatAgentTaskLine(task) {
|
|
1915
2143
|
const duration = formatDuration(task.startedAt, task.endedAt);
|
|
1916
2144
|
const label = task.description ?? task.prompt.slice(0, 60);
|
|
@@ -1921,7 +2149,7 @@ var taskListTool = {
|
|
|
1921
2149
|
name: "task_list",
|
|
1922
2150
|
description: "List background tasks (shell commands and subagents) with status and runtime.",
|
|
1923
2151
|
permission: "read",
|
|
1924
|
-
parameters:
|
|
2152
|
+
parameters: z7.object({}),
|
|
1925
2153
|
execute() {
|
|
1926
2154
|
const shell = formatTaskList(defaultTaskManager.list());
|
|
1927
2155
|
const agents = defaultAgentTasks.list();
|
|
@@ -1939,13 +2167,13 @@ var taskOutputTool = {
|
|
|
1939
2167
|
execute(args) {
|
|
1940
2168
|
const shell = defaultTaskManager.get(args.id);
|
|
1941
2169
|
if (shell) {
|
|
1942
|
-
const body = shell.output.replace(/\s+$/, "") || "(no output yet)";
|
|
2170
|
+
const body = truncateOutput(shell.output.replace(/\s+$/, "") || "(no output yet)");
|
|
1943
2171
|
return Promise.resolve({ content: `${formatTaskLine(shell)}
|
|
1944
2172
|
${body}` });
|
|
1945
2173
|
}
|
|
1946
2174
|
const agent = defaultAgentTasks.get(args.id);
|
|
1947
2175
|
if (agent) {
|
|
1948
|
-
const body = agent.status === "running" ? "(still running)" : agent.result.replace(/\s+$/, "") || "(no report)";
|
|
2176
|
+
const body = agent.status === "running" ? "(still running)" : truncateOutput(agent.result.replace(/\s+$/, "") || "(no report)");
|
|
1949
2177
|
return Promise.resolve({ content: `${formatAgentTaskLine(agent)}
|
|
1950
2178
|
${body}` });
|
|
1951
2179
|
}
|
|
@@ -1975,21 +2203,154 @@ var taskKillTool = {
|
|
|
1975
2203
|
} else {
|
|
1976
2204
|
defaultAgentTasks.kill(args.id);
|
|
1977
2205
|
}
|
|
1978
|
-
return Promise.resolve({ content: `Task ${args.id} stopped.` });
|
|
2206
|
+
return Promise.resolve({ content: `Task ${args.id} stopped.` });
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
|
|
2210
|
+
// src/tools/todo.ts
|
|
2211
|
+
import { mkdir as mkdir4, readFile as readFile4, rename, writeFile as writeFile4 } from "fs/promises";
|
|
2212
|
+
import path11 from "path";
|
|
2213
|
+
import { z as z8 } from "zod";
|
|
2214
|
+
var todoItemSchema = z8.object({
|
|
2215
|
+
id: z8.number().int(),
|
|
2216
|
+
title: z8.string(),
|
|
2217
|
+
status: z8.enum(["pending", "in_progress", "done"])
|
|
2218
|
+
});
|
|
2219
|
+
var writeSchema = z8.object({
|
|
2220
|
+
todos: z8.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
2221
|
+
});
|
|
2222
|
+
var readSchema = z8.object({});
|
|
2223
|
+
function fileFor(cwd) {
|
|
2224
|
+
return path11.join(cwd, ".star", "todos.json");
|
|
2225
|
+
}
|
|
2226
|
+
var persistGuard = () => true;
|
|
2227
|
+
function setTodoPersistGuard(guard) {
|
|
2228
|
+
persistGuard = guard;
|
|
2229
|
+
}
|
|
2230
|
+
var TodoStore = class {
|
|
2231
|
+
items = /* @__PURE__ */ new Map();
|
|
2232
|
+
list() {
|
|
2233
|
+
return [...this.items.values()];
|
|
2234
|
+
}
|
|
2235
|
+
replace(items) {
|
|
2236
|
+
this.items.clear();
|
|
2237
|
+
for (const item of items) {
|
|
2238
|
+
this.items.set(item.id, item);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
async load(cwd) {
|
|
2242
|
+
let raw;
|
|
2243
|
+
try {
|
|
2244
|
+
raw = await readFile4(fileFor(cwd), "utf8");
|
|
2245
|
+
} catch {
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
try {
|
|
2249
|
+
const parsed = JSON.parse(raw);
|
|
2250
|
+
if (Array.isArray(parsed)) {
|
|
2251
|
+
this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
|
|
2252
|
+
}
|
|
2253
|
+
} catch {
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
// Atomic write (tmp + rename): a crash or failed save can never leave a
|
|
2257
|
+
// truncated todos.json behind.
|
|
2258
|
+
async save(cwd) {
|
|
2259
|
+
if (!persistGuard()) {
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
await mkdir4(path11.join(cwd, ".star"), { recursive: true });
|
|
2263
|
+
const file = fileFor(cwd);
|
|
2264
|
+
const tmp = `${file}.tmp`;
|
|
2265
|
+
await writeFile4(tmp, `${JSON.stringify(this.list(), null, 2)}
|
|
2266
|
+
`);
|
|
2267
|
+
await rename(tmp, file);
|
|
1979
2268
|
}
|
|
1980
2269
|
};
|
|
2270
|
+
var SYMBOLS = {
|
|
2271
|
+
pending: "[ ]",
|
|
2272
|
+
in_progress: "[~]",
|
|
2273
|
+
done: "[x]"
|
|
2274
|
+
};
|
|
2275
|
+
function formatTodos(items) {
|
|
2276
|
+
if (items.length === 0) {
|
|
2277
|
+
return "No todos.";
|
|
2278
|
+
}
|
|
2279
|
+
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
2280
|
+
const width = Math.max(...lines.map((l) => l.length));
|
|
2281
|
+
return lines.map((line, i) => {
|
|
2282
|
+
const status = items[i]?.status;
|
|
2283
|
+
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
2284
|
+
}).join("\n");
|
|
2285
|
+
}
|
|
2286
|
+
var defaultStore = new TodoStore();
|
|
2287
|
+
function parseTodoArgs(args) {
|
|
2288
|
+
const parsed = writeSchema.safeParse(args);
|
|
2289
|
+
return parsed.success ? parsed.data.todos : null;
|
|
2290
|
+
}
|
|
2291
|
+
function pendingTodoTitles(args) {
|
|
2292
|
+
return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
|
|
2293
|
+
}
|
|
2294
|
+
async function loadTodos(cwd) {
|
|
2295
|
+
await defaultStore.load(cwd);
|
|
2296
|
+
return defaultStore.list();
|
|
2297
|
+
}
|
|
2298
|
+
function resetTodos() {
|
|
2299
|
+
defaultStore.replace([]);
|
|
2300
|
+
}
|
|
2301
|
+
function createTodoTools(store = defaultStore) {
|
|
2302
|
+
const todoWrite = {
|
|
2303
|
+
name: "todo_write",
|
|
2304
|
+
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
2305
|
+
permission: "read",
|
|
2306
|
+
parameters: writeSchema,
|
|
2307
|
+
async execute(args, ctx) {
|
|
2308
|
+
const parsed = writeSchema.safeParse(args);
|
|
2309
|
+
if (!parsed.success) {
|
|
2310
|
+
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
2311
|
+
}
|
|
2312
|
+
const previous = store.list();
|
|
2313
|
+
store.replace(parsed.data.todos);
|
|
2314
|
+
try {
|
|
2315
|
+
await store.save(ctx.cwd);
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
store.replace(previous);
|
|
2318
|
+
return { content: `Failed to save todos: ${err.message}`, isError: true };
|
|
2319
|
+
}
|
|
2320
|
+
const items = store.list();
|
|
2321
|
+
const done = items.filter((it) => it.status === "done").length;
|
|
2322
|
+
return { content: `Todo list updated (${done}/${items.length} done).` };
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
const todoRead = {
|
|
2326
|
+
name: "todo_read",
|
|
2327
|
+
description: "Read the current todo list.",
|
|
2328
|
+
permission: "read",
|
|
2329
|
+
parameters: readSchema,
|
|
2330
|
+
async execute() {
|
|
2331
|
+
return { content: formatTodos(store.list()) };
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
return [todoWrite, todoRead];
|
|
2335
|
+
}
|
|
1981
2336
|
|
|
1982
2337
|
// src/tools/web/fetch.ts
|
|
1983
|
-
import { z as
|
|
2338
|
+
import { z as z9 } from "zod";
|
|
1984
2339
|
var DEFAULT_MAX_CHARS = 2e4;
|
|
1985
2340
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
1986
2341
|
var TIMEOUT_MS = 3e4;
|
|
1987
|
-
var schema7 =
|
|
1988
|
-
url:
|
|
1989
|
-
maxChars:
|
|
2342
|
+
var schema7 = z9.object({
|
|
2343
|
+
url: z9.string().describe("The http(s) URL to fetch"),
|
|
2344
|
+
maxChars: z9.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
|
|
1990
2345
|
});
|
|
2346
|
+
function safeCodePoint(n, raw) {
|
|
2347
|
+
if (!Number.isInteger(n) || n < 0 || n > 1114111) {
|
|
2348
|
+
return raw;
|
|
2349
|
+
}
|
|
2350
|
+
return String.fromCodePoint(n);
|
|
2351
|
+
}
|
|
1991
2352
|
function decodeEntities(s) {
|
|
1992
|
-
return s.replace(/&#(\d+);/g, (
|
|
2353
|
+
return s.replace(/&#(\d+);/g, (m, n) => safeCodePoint(Number(n), m)).replace(/&#x([0-9a-fA-F]+);/gi, (m, n) => safeCodePoint(Number.parseInt(n, 16), m)).replace(/&(quot|lt|gt|nbsp);/g, (m) => {
|
|
1993
2354
|
switch (m) {
|
|
1994
2355
|
case """:
|
|
1995
2356
|
return '"';
|
|
@@ -2049,7 +2410,7 @@ function truncate2(s, maxChars) {
|
|
|
2049
2410
|
}
|
|
2050
2411
|
var webFetchTool = {
|
|
2051
2412
|
name: "web_fetch",
|
|
2052
|
-
description: "Fetch a URL over http(s) and return its contents as readable text. HTML pages are converted to plain text (tags, scripts and styles removed). Output is truncated to maxChars (default 20000).",
|
|
2413
|
+
description: "Fetch a URL over http(s) and return its contents as readable text. HTML pages are converted to plain text (tags, scripts and styles removed). Output is truncated to maxChars (default 20000). Can reach internal/intranet addresses, so do not use it for untrusted URLs.",
|
|
2053
2414
|
permission: "read",
|
|
2054
2415
|
parameters: schema7,
|
|
2055
2416
|
async execute(args, ctx) {
|
|
@@ -2114,15 +2475,15 @@ var webFetchTool = {
|
|
|
2114
2475
|
};
|
|
2115
2476
|
|
|
2116
2477
|
// src/tools/web/search.ts
|
|
2117
|
-
import { z as
|
|
2478
|
+
import { z as z10 } from "zod";
|
|
2118
2479
|
var TIMEOUT_MS2 = 15e3;
|
|
2119
2480
|
var DEFAULT_MAX_RESULTS = 5;
|
|
2120
2481
|
var MAX_RESULTS_LIMIT = 10;
|
|
2121
2482
|
var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
|
|
2122
2483
|
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
2123
|
-
var schema8 =
|
|
2124
|
-
query:
|
|
2125
|
-
maxResults:
|
|
2484
|
+
var schema8 = z10.object({
|
|
2485
|
+
query: z10.string().min(1).describe("The search query"),
|
|
2486
|
+
maxResults: z10.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
|
|
2126
2487
|
`Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
|
|
2127
2488
|
)
|
|
2128
2489
|
});
|
|
@@ -2231,7 +2592,9 @@ var webSearchTool = {
|
|
|
2231
2592
|
// src/tools/registry.ts
|
|
2232
2593
|
var ToolRegistry = class {
|
|
2233
2594
|
tools = /* @__PURE__ */ new Map();
|
|
2234
|
-
|
|
2595
|
+
// todoStore defaults to the module-level shared store; subagent loops pass
|
|
2596
|
+
// their own instance so a child's todo_write cannot clobber the parent list.
|
|
2597
|
+
constructor(todoStore) {
|
|
2235
2598
|
for (const tool of [
|
|
2236
2599
|
readFileTool,
|
|
2237
2600
|
writeFileTool,
|
|
@@ -2244,7 +2607,7 @@ var ToolRegistry = class {
|
|
|
2244
2607
|
taskListTool,
|
|
2245
2608
|
taskOutputTool,
|
|
2246
2609
|
taskKillTool,
|
|
2247
|
-
...createTodoTools()
|
|
2610
|
+
...createTodoTools(todoStore)
|
|
2248
2611
|
]) {
|
|
2249
2612
|
this.register(tool);
|
|
2250
2613
|
}
|
|
@@ -2263,13 +2626,165 @@ var ToolRegistry = class {
|
|
|
2263
2626
|
}
|
|
2264
2627
|
};
|
|
2265
2628
|
|
|
2266
|
-
// src/
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2629
|
+
// src/agent/project-memory.ts
|
|
2630
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
2631
|
+
import path12 from "path";
|
|
2632
|
+
var PROJECT_MEMORY_FILE = "AGENTS.md";
|
|
2633
|
+
var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
|
|
2634
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2635
|
+
function readProjectMemory(cwd) {
|
|
2636
|
+
const file = path12.join(cwd, PROJECT_MEMORY_FILE);
|
|
2637
|
+
let mtimeMs;
|
|
2638
|
+
try {
|
|
2639
|
+
mtimeMs = statSync2(file).mtimeMs;
|
|
2640
|
+
} catch {
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2643
|
+
const hit = cache.get(file);
|
|
2644
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.block;
|
|
2645
|
+
let block = null;
|
|
2646
|
+
try {
|
|
2647
|
+
let body = readFileSync2(file, "utf8").trim();
|
|
2648
|
+
if (body) {
|
|
2649
|
+
if (body.length > PROJECT_MEMORY_MAX_CHARS) {
|
|
2650
|
+
body = `${body.slice(0, PROJECT_MEMORY_MAX_CHARS)}
|
|
2651
|
+
|
|
2652
|
+
[AGENTS.md truncated to ${PROJECT_MEMORY_MAX_CHARS} characters]`;
|
|
2653
|
+
}
|
|
2654
|
+
block = `# Project instructions (AGENTS.md)
|
|
2655
|
+
${body}`;
|
|
2656
|
+
}
|
|
2657
|
+
} catch {
|
|
2658
|
+
block = null;
|
|
2659
|
+
}
|
|
2660
|
+
cache.set(file, { mtimeMs, block });
|
|
2661
|
+
return block;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// src/agent/skills.ts
|
|
2665
|
+
import { readFileSync as readFileSync3, readdirSync, statSync as statSync3 } from "fs";
|
|
2666
|
+
import path13 from "path";
|
|
2667
|
+
import { z as z11 } from "zod";
|
|
2668
|
+
var SKILL_FILE = "SKILL.md";
|
|
2669
|
+
var SKILL_MAX_CHARS = 32 * 1024;
|
|
2670
|
+
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
2671
|
+
function parseFrontmatter(raw) {
|
|
2672
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
2673
|
+
if (!match) return null;
|
|
2674
|
+
const meta = {};
|
|
2675
|
+
for (const line of (match[1] ?? "").split(/\r?\n/)) {
|
|
2676
|
+
const kv = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
2677
|
+
if (kv?.[1]) meta[kv[1]] = (kv[2] ?? "").trim();
|
|
2678
|
+
}
|
|
2679
|
+
return { meta, body: raw.slice(match[0].length).trim() };
|
|
2680
|
+
}
|
|
2681
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
2682
|
+
function parseSkillFile(filePath, dirName, scope) {
|
|
2683
|
+
let mtimeMs;
|
|
2684
|
+
try {
|
|
2685
|
+
mtimeMs = statSync3(filePath).mtimeMs;
|
|
2686
|
+
} catch {
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2689
|
+
const hit = cache2.get(filePath);
|
|
2690
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.skill;
|
|
2691
|
+
let skill = null;
|
|
2692
|
+
try {
|
|
2693
|
+
const parsed = parseFrontmatter(readFileSync3(filePath, "utf8"));
|
|
2694
|
+
const name = parsed?.meta.name || dirName;
|
|
2695
|
+
const description = parsed?.meta.description;
|
|
2696
|
+
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
2697
|
+
skill = { name, description, filePath, dir: path13.dirname(filePath), scope };
|
|
2698
|
+
}
|
|
2699
|
+
} catch {
|
|
2700
|
+
skill = null;
|
|
2701
|
+
}
|
|
2702
|
+
cache2.set(filePath, { mtimeMs, skill });
|
|
2703
|
+
return skill;
|
|
2704
|
+
}
|
|
2705
|
+
function scanDir(base, scope, into) {
|
|
2706
|
+
let entries;
|
|
2707
|
+
try {
|
|
2708
|
+
entries = readdirSync(base);
|
|
2709
|
+
} catch {
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
for (const entry of entries.sort()) {
|
|
2713
|
+
if (!NAME_PATTERN.test(entry)) continue;
|
|
2714
|
+
const parsed = parseSkillFile(path13.join(base, entry, SKILL_FILE), entry, scope);
|
|
2715
|
+
if (parsed) into.set(parsed.name, parsed);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function discoverSkills(cwd, home = starHome()) {
|
|
2719
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2720
|
+
scanDir(path13.join(home, "skills"), "user", byName);
|
|
2721
|
+
scanDir(path13.join(cwd, ".star", "skills"), "project", byName);
|
|
2722
|
+
return [...byName.values()];
|
|
2723
|
+
}
|
|
2724
|
+
function formatSkillsBlock(skills) {
|
|
2725
|
+
const lines = skills.map((s) => `- ${s.name}: ${s.description}`);
|
|
2726
|
+
return [
|
|
2727
|
+
"# Available skills",
|
|
2728
|
+
"When the user's request matches one of these skills, call the skill tool with its name to load the full instructions before proceeding:",
|
|
2729
|
+
...lines
|
|
2730
|
+
].join("\n");
|
|
2731
|
+
}
|
|
2732
|
+
function loadSkillBody(skill) {
|
|
2733
|
+
let parsed = null;
|
|
2734
|
+
try {
|
|
2735
|
+
parsed = parseFrontmatter(readFileSync3(skill.filePath, "utf8"));
|
|
2736
|
+
} catch {
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
2739
|
+
if (!parsed?.body) return null;
|
|
2740
|
+
if (parsed.body.length > SKILL_MAX_CHARS) {
|
|
2741
|
+
return `${parsed.body.slice(0, SKILL_MAX_CHARS)}
|
|
2742
|
+
|
|
2743
|
+
[skill truncated to ${SKILL_MAX_CHARS} characters]`;
|
|
2271
2744
|
}
|
|
2272
|
-
return
|
|
2745
|
+
return parsed.body;
|
|
2746
|
+
}
|
|
2747
|
+
function createSkillTool(deps) {
|
|
2748
|
+
return {
|
|
2749
|
+
name: "skill",
|
|
2750
|
+
description: "Load the full instructions of a skill by name. Available skills are listed in the system prompt when present; call this before starting work that matches one of them.",
|
|
2751
|
+
permission: "read",
|
|
2752
|
+
parameters: z11.object({
|
|
2753
|
+
name: z11.string().describe("Name of the skill to load, as listed in the system prompt.")
|
|
2754
|
+
}),
|
|
2755
|
+
async execute(args) {
|
|
2756
|
+
const skills = discoverSkills(deps.cwd, deps.home);
|
|
2757
|
+
const skill = skills.find((s) => s.name === args.name);
|
|
2758
|
+
if (!skill) {
|
|
2759
|
+
const available = skills.map((s) => s.name).join(", ") || "none";
|
|
2760
|
+
return {
|
|
2761
|
+
content: `Unknown skill: "${args.name}". Available skills: ${available}`,
|
|
2762
|
+
isError: true
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
const body = loadSkillBody(skill);
|
|
2766
|
+
if (!body) {
|
|
2767
|
+
return { content: `Skill "${skill.name}" is empty or unreadable.`, isError: true };
|
|
2768
|
+
}
|
|
2769
|
+
return {
|
|
2770
|
+
content: [
|
|
2771
|
+
`# Skill: ${skill.name}`,
|
|
2772
|
+
"",
|
|
2773
|
+
body,
|
|
2774
|
+
"",
|
|
2775
|
+
`Base directory for relative file references in this skill: ${skill.dir}`
|
|
2776
|
+
].join("\n")
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// src/agent/subagent.ts
|
|
2783
|
+
import { z as z12 } from "zod";
|
|
2784
|
+
|
|
2785
|
+
// src/tools/index.ts
|
|
2786
|
+
function createDefaultRegistry(todoStore) {
|
|
2787
|
+
return new ToolRegistry(todoStore);
|
|
2273
2788
|
}
|
|
2274
2789
|
|
|
2275
2790
|
// src/agent/subagent.ts
|
|
@@ -2277,7 +2792,7 @@ var MAX_SUBAGENT_DEPTH = 1;
|
|
|
2277
2792
|
var MAX_REPORT_CHARS = 2e4;
|
|
2278
2793
|
var SUBAGENT_PROMPT = "You are a subagent spawned to handle a focused subtask on behalf of the main agent. Work autonomously with the tools available to you, then finish with a concise report of what you found or did. Your final text is returned to the main agent as the tool result \u2014 make it self-contained.";
|
|
2279
2794
|
async function runSubagent(deps, args, cwd, signal) {
|
|
2280
|
-
const { AgentLoop: AgentLoop2 } = await import("./loop-
|
|
2795
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-TQ7GFTTE.js");
|
|
2281
2796
|
const child = new AgentLoop2({
|
|
2282
2797
|
model: deps.model,
|
|
2283
2798
|
registry: createDefaultRegistry(),
|
|
@@ -2304,6 +2819,9 @@ ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
|
|
|
2304
2819
|
} catch (e) {
|
|
2305
2820
|
error = e instanceof Error ? e.message : String(e);
|
|
2306
2821
|
}
|
|
2822
|
+
if (signal.aborted) {
|
|
2823
|
+
throw new Error("Tool execution aborted.");
|
|
2824
|
+
}
|
|
2307
2825
|
let report = "";
|
|
2308
2826
|
const messages = child.getMessages();
|
|
2309
2827
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -2362,13 +2880,13 @@ function createSubagentTool(deps) {
|
|
|
2362
2880
|
|
|
2363
2881
|
// src/agent/user-memory.ts
|
|
2364
2882
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
2365
|
-
import
|
|
2883
|
+
import path14 from "path";
|
|
2366
2884
|
import { z as z13 } from "zod";
|
|
2367
2885
|
var USER_MEMORY_MAX_CHARS = 32 * 1024;
|
|
2368
2886
|
var REMEMBER_TEXT_MAX_CHARS = 500;
|
|
2369
2887
|
var cache3 = /* @__PURE__ */ new Map();
|
|
2370
2888
|
function readUserMemory(home = starHome()) {
|
|
2371
|
-
const file =
|
|
2889
|
+
const file = path14.join(home, "MEMORY.md");
|
|
2372
2890
|
let mtimeMs;
|
|
2373
2891
|
try {
|
|
2374
2892
|
mtimeMs = statSync4(file).mtimeMs;
|
|
@@ -2400,7 +2918,7 @@ function readUserMemory(home = starHome()) {
|
|
|
2400
2918
|
}
|
|
2401
2919
|
function appendUserMemory(text, home = starHome()) {
|
|
2402
2920
|
mkdirSync(home, { recursive: true });
|
|
2403
|
-
appendFileSync(
|
|
2921
|
+
appendFileSync(path14.join(home, "MEMORY.md"), `- ${text}
|
|
2404
2922
|
`, "utf8");
|
|
2405
2923
|
}
|
|
2406
2924
|
function createRememberTool(deps = {}) {
|
|
@@ -2467,6 +2985,24 @@ ${finalReply.slice(0, 2e3)}
|
|
|
2467
2985
|
}
|
|
2468
2986
|
}
|
|
2469
2987
|
var STREAM_RETRY_BASE_DELAY_MS = 1e3;
|
|
2988
|
+
var REDO_LABEL_MAX = 40;
|
|
2989
|
+
function messageText(message) {
|
|
2990
|
+
if (!message) return "";
|
|
2991
|
+
const content = message.content;
|
|
2992
|
+
if (typeof content === "string") return content;
|
|
2993
|
+
if (Array.isArray(content)) {
|
|
2994
|
+
return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
|
|
2995
|
+
}
|
|
2996
|
+
return "";
|
|
2997
|
+
}
|
|
2998
|
+
function stableStringify(value) {
|
|
2999
|
+
if (value === void 0) return "undefined";
|
|
3000
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
3001
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
3002
|
+
const record = value;
|
|
3003
|
+
const entries = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
|
|
3004
|
+
return `{${entries.join(",")}}`;
|
|
3005
|
+
}
|
|
2470
3006
|
function sleep(ms, signal) {
|
|
2471
3007
|
if (signal.aborted) return Promise.resolve(false);
|
|
2472
3008
|
return new Promise((resolve) => {
|
|
@@ -2485,11 +3021,32 @@ var AgentLoop = class {
|
|
|
2485
3021
|
messages = [];
|
|
2486
3022
|
opts;
|
|
2487
3023
|
// Seq + user-message index of each turn started via stream(); lets /undo
|
|
2488
|
-
// match a retracted turn to the file snapshots it produced.
|
|
2489
|
-
//
|
|
2490
|
-
//
|
|
3024
|
+
// match a retracted turn to the file snapshots it produced. `tree` is the
|
|
3025
|
+
// whole-tree git snapshot captured at the turn's start (root loop only,
|
|
3026
|
+
// config.gitSnapshots) — when present, /undo restores it instead of
|
|
3027
|
+
// replaying per-file snapshots, which also covers bash-made changes.
|
|
3028
|
+
// Cleared when history is replaced wholesale (resume/compact), because
|
|
3029
|
+
// indices no longer line up — in that case /undo only retracts messages,
|
|
3030
|
+
// never wrong files.
|
|
2491
3031
|
turnMarkers = [];
|
|
3032
|
+
// Pre-undo tree snapshots, latest last: a git-path /undo pushes the
|
|
3033
|
+
// pre-undo working tree here so /redo can restore it exactly. Only the git
|
|
3034
|
+
// path feeds this — per-file snapshot undos stay non-redoable.
|
|
3035
|
+
redoStack = [];
|
|
3036
|
+
// Start of the most recently retracted turn's user message; undoLastTurn
|
|
3037
|
+
// consumes it as the redo label right after retractLastTurn sets it.
|
|
3038
|
+
lastUndoneLabel = "";
|
|
3039
|
+
// Doom-loop guard: signature of the most recent tool call and how many
|
|
3040
|
+
// times in a row it has repeated. Any different call resets the count.
|
|
3041
|
+
lastToolSignature = null;
|
|
3042
|
+
repeatedToolCalls = 0;
|
|
3043
|
+
// Turn context of the in-flight stream() turn, stamped onto every tool
|
|
3044
|
+
// execution so file snapshots attribute root changes to a turn and mark
|
|
3045
|
+
// subagent changes as excluded from turn-level retraction.
|
|
3046
|
+
activeTurnSeq = 0;
|
|
3047
|
+
activeTurnUserIndex = -1;
|
|
2492
3048
|
titleScheduled = false;
|
|
3049
|
+
titleInput;
|
|
2493
3050
|
confirmHandler;
|
|
2494
3051
|
// Hook failures never block the turn (except an explicit PreToolUse block);
|
|
2495
3052
|
// their stderr surfaces through this callback (REPL system message / stderr
|
|
@@ -2497,6 +3054,15 @@ var AgentLoop = class {
|
|
|
2497
3054
|
onHookWarning;
|
|
2498
3055
|
constructor(opts) {
|
|
2499
3056
|
this.opts = opts;
|
|
3057
|
+
const depth = opts.subagentDepth ?? 0;
|
|
3058
|
+
if (depth > 0) {
|
|
3059
|
+
opts.registry = new ToolRegistry(new TodoStore());
|
|
3060
|
+
} else {
|
|
3061
|
+
setTodoPersistGuard(() => {
|
|
3062
|
+
const mode = this.opts.config.permissionMode;
|
|
3063
|
+
return mode !== "readonly" && mode !== "plan";
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
2500
3066
|
if (opts.system) {
|
|
2501
3067
|
this.messages.push({ role: "system", content: opts.system });
|
|
2502
3068
|
}
|
|
@@ -2505,7 +3071,6 @@ var AgentLoop = class {
|
|
|
2505
3071
|
}
|
|
2506
3072
|
opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
|
|
2507
3073
|
opts.registry?.register(createRememberTool());
|
|
2508
|
-
const depth = opts.subagentDepth ?? 0;
|
|
2509
3074
|
if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
|
|
2510
3075
|
opts.registry.register(
|
|
2511
3076
|
createSubagentTool({
|
|
@@ -2524,10 +3089,14 @@ var AgentLoop = class {
|
|
|
2524
3089
|
}
|
|
2525
3090
|
// Swaps the persistence target (/new starts a fresh session mid-REPL).
|
|
2526
3091
|
// Title generation is re-armed so the new session gets one after its first
|
|
2527
|
-
// turn, and snapshot checkpoints now flow to the new store.
|
|
3092
|
+
// turn, and snapshot checkpoints now flow to the new store. The redo stack
|
|
3093
|
+
// dies with the old session context: its trees describe working-tree states
|
|
3094
|
+
// that no longer line up with the new session's conversation.
|
|
2528
3095
|
setSessionStore(store) {
|
|
2529
3096
|
this.opts.sessionStore = store;
|
|
2530
3097
|
this.titleScheduled = false;
|
|
3098
|
+
this.titleInput = void 0;
|
|
3099
|
+
this.redoStack = [];
|
|
2531
3100
|
if (store) this.bindSnapshotHooks(store);
|
|
2532
3101
|
}
|
|
2533
3102
|
bindSnapshotHooks(store) {
|
|
@@ -2540,7 +3109,9 @@ var AgentLoop = class {
|
|
|
2540
3109
|
existed: snapshot.existed,
|
|
2541
3110
|
toolName: snapshot.toolName,
|
|
2542
3111
|
turn: snapshot.turn,
|
|
2543
|
-
messageIndex: snapshot.messageIndex
|
|
3112
|
+
messageIndex: snapshot.messageIndex,
|
|
3113
|
+
owner: snapshot.owner,
|
|
3114
|
+
contentTooLarge: snapshot.contentTooLarge
|
|
2544
3115
|
},
|
|
2545
3116
|
snapshot.content
|
|
2546
3117
|
),
|
|
@@ -2550,37 +3121,92 @@ var AgentLoop = class {
|
|
|
2550
3121
|
async loadMessages(messages) {
|
|
2551
3122
|
this.messages = reconcileToolCalls(messages);
|
|
2552
3123
|
this.turnMarkers = [];
|
|
3124
|
+
this.redoStack = [];
|
|
2553
3125
|
}
|
|
2554
3126
|
// Read-only counterpart of retractLastTurn: how many messages /undo would
|
|
2555
|
-
// drop
|
|
2556
|
-
// no verified turn, so no file snapshots may be reverted)
|
|
2557
|
-
//
|
|
3127
|
+
// drop, which snapshot turn's file changes it would revert (undefined =
|
|
3128
|
+
// no verified turn, so no file snapshots may be reverted), and the turn's
|
|
3129
|
+
// whole-tree snapshot when one was captured. Mutates nothing, so the REPL
|
|
3130
|
+
// can show the preview before the user confirms.
|
|
2558
3131
|
previewLastTurnRetraction() {
|
|
2559
3132
|
const result = retractLastTurn([...this.messages]);
|
|
2560
3133
|
if (result.removed === 0) return { removed: 0 };
|
|
2561
3134
|
const userIndex = result.messages.length;
|
|
2562
3135
|
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
2563
|
-
const
|
|
2564
|
-
return {
|
|
3136
|
+
const verified = last && last.userIndex === userIndex;
|
|
3137
|
+
return {
|
|
3138
|
+
removed: result.removed,
|
|
3139
|
+
turn: verified ? last.seq : void 0,
|
|
3140
|
+
tree: verified ? last.tree : void 0
|
|
3141
|
+
};
|
|
2565
3142
|
}
|
|
2566
3143
|
// Drops the final user message and everything after it, and persists the
|
|
2567
3144
|
// trimmed history so a later /resume does not bring the turn back.
|
|
2568
3145
|
// `turn` is the retracted turn's snapshot seq when it could be verified
|
|
2569
3146
|
// against the marker recorded at turn start — undefined means the caller
|
|
2570
|
-
// must not revert any file snapshots.
|
|
3147
|
+
// must not revert any file snapshots. `tree` is the turn's whole-tree
|
|
3148
|
+
// snapshot when git snapshots tracked it.
|
|
2571
3149
|
async retractLastTurn() {
|
|
2572
3150
|
const result = retractLastTurn([...this.messages]);
|
|
2573
3151
|
if (result.removed === 0) return { removed: 0 };
|
|
2574
3152
|
const userIndex = result.messages.length;
|
|
3153
|
+
this.lastUndoneLabel = messageText(this.messages[userIndex]).slice(0, REDO_LABEL_MAX);
|
|
2575
3154
|
this.messages = result.messages;
|
|
2576
3155
|
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
2577
3156
|
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
2578
3157
|
let turn;
|
|
3158
|
+
let tree;
|
|
2579
3159
|
if (last && last.userIndex === userIndex) {
|
|
2580
3160
|
turn = last.seq;
|
|
3161
|
+
tree = last.tree;
|
|
2581
3162
|
this.turnMarkers.pop();
|
|
2582
3163
|
}
|
|
2583
|
-
return { removed: result.removed, turn };
|
|
3164
|
+
return { removed: result.removed, turn, tree };
|
|
3165
|
+
}
|
|
3166
|
+
// The confirmed /undo: retract the last turn, then revert its file changes.
|
|
3167
|
+
// With a whole-tree snapshot the working tree is restored from git — which
|
|
3168
|
+
// also reverts changes no tool snapshot saw (bash edits, deleted files) —
|
|
3169
|
+
// and the turn's per-file snapshots are dropped instead of replayed (they
|
|
3170
|
+
// would double-restore). The pre-undo tree feeds the redo stack, so /redo
|
|
3171
|
+
// returns to the exact state before the undo. Any git failure falls back
|
|
3172
|
+
// to the per-file snapshot revert, which never pushes the redo stack.
|
|
3173
|
+
async undoLastTurn() {
|
|
3174
|
+
const { removed, turn, tree } = await this.retractLastTurn();
|
|
3175
|
+
if (removed === 0) return { removed, reverted: [] };
|
|
3176
|
+
if (turn !== void 0 && tree !== void 0) {
|
|
3177
|
+
const preUndoTree = await trackTree(this.opts.cwd);
|
|
3178
|
+
if (preUndoTree !== null && await restoreTree(this.opts.cwd, tree)) {
|
|
3179
|
+
this.redoStack.push({ tree: preUndoTree, label: this.lastUndoneLabel });
|
|
3180
|
+
const dropped = await dropTurnSnapshots(turn, true);
|
|
3181
|
+
return {
|
|
3182
|
+
removed,
|
|
3183
|
+
reverted: [
|
|
3184
|
+
`Restored the working tree from the git snapshot taken at the turn's start (${tree.slice(0, 8)}; covers changes from any tool, bash included; ${dropped} per-file snapshot(s) discarded).`
|
|
3185
|
+
],
|
|
3186
|
+
tree
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
const reverted = turn !== void 0 ? await undoTurnSnapshots(turn) : [];
|
|
3191
|
+
return { removed, reverted };
|
|
3192
|
+
}
|
|
3193
|
+
// The redo entry /redo would restore, without popping it (for the confirm
|
|
3194
|
+
// prompt); null when there is nothing to redo.
|
|
3195
|
+
peekRedo() {
|
|
3196
|
+
return this.redoStack[this.redoStack.length - 1] ?? null;
|
|
3197
|
+
}
|
|
3198
|
+
// Restores the working tree to its state just before the last git-path
|
|
3199
|
+
// /undo. Conversation messages are not re-added — redo is file-level only.
|
|
3200
|
+
// The entry stays on the stack when the restore fails, so it can be retried.
|
|
3201
|
+
async redoLastUndo() {
|
|
3202
|
+
const entry = this.redoStack[this.redoStack.length - 1];
|
|
3203
|
+
if (!entry) return null;
|
|
3204
|
+
const names = await diffTreeNames(this.opts.cwd, entry.tree);
|
|
3205
|
+
if (!await restoreTree(this.opts.cwd, entry.tree)) {
|
|
3206
|
+
return { ok: false, tree: entry.tree, label: entry.label, files: null };
|
|
3207
|
+
}
|
|
3208
|
+
this.redoStack.pop();
|
|
3209
|
+
return { ok: true, tree: entry.tree, label: entry.label, files: names?.length ?? null };
|
|
2584
3210
|
}
|
|
2585
3211
|
// Cut point for a rewind: messages[index] should be the user message that
|
|
2586
3212
|
// started the rewound turn, but compaction may have shifted indices, so
|
|
@@ -2626,13 +3252,18 @@ var AgentLoop = class {
|
|
|
2626
3252
|
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
2627
3253
|
return removed;
|
|
2628
3254
|
}
|
|
2629
|
-
//
|
|
2630
|
-
//
|
|
2631
|
-
//
|
|
2632
|
-
|
|
3255
|
+
// Background title generation runs once per loop, fed by the first real
|
|
3256
|
+
// user message of the session. It must NOT latch on the first assistant
|
|
3257
|
+
// step's text: that text is empty for a tool-call-only first step (the
|
|
3258
|
+
// common agentic case), which used to burn the one-shot flag on an empty
|
|
3259
|
+
// input and left every such session untitled. The store-level title check
|
|
3260
|
+
// makes resumed sessions that already have a title a no-op.
|
|
3261
|
+
maybeScheduleTitle() {
|
|
2633
3262
|
if (this.titleScheduled) return;
|
|
2634
3263
|
const store = this.opts.sessionStore;
|
|
2635
3264
|
if (!store) return;
|
|
3265
|
+
const input = this.titleInput;
|
|
3266
|
+
if (!input) return;
|
|
2636
3267
|
this.titleScheduled = true;
|
|
2637
3268
|
scheduleSessionTitle(store, input, this.opts.model);
|
|
2638
3269
|
}
|
|
@@ -2681,6 +3312,9 @@ var AgentLoop = class {
|
|
|
2681
3312
|
this.syncSystemMessage();
|
|
2682
3313
|
const inputText = typeof input === "string" ? input : input.text;
|
|
2683
3314
|
const images = typeof input === "string" ? [] : input.images;
|
|
3315
|
+
if (this.titleInput === void 0 && inputText.trim().length > 0) {
|
|
3316
|
+
this.titleInput = inputText;
|
|
3317
|
+
}
|
|
2684
3318
|
const userMessage = images.length > 0 ? {
|
|
2685
3319
|
role: "user",
|
|
2686
3320
|
content: [
|
|
@@ -2692,12 +3326,17 @@ var AgentLoop = class {
|
|
|
2692
3326
|
{ type: "text", text: inputText }
|
|
2693
3327
|
]
|
|
2694
3328
|
} : { role: "user", content: inputText };
|
|
3329
|
+
const isRoot = (this.opts.subagentDepth ?? 0) === 0;
|
|
3330
|
+
const tree = isRoot && this.opts.config.gitSnapshots ? await trackTree(this.opts.cwd) ?? void 0 : void 0;
|
|
2695
3331
|
this.messages.push(userMessage);
|
|
2696
|
-
const seq =
|
|
2697
|
-
this.turnMarkers.push({ seq, userIndex: this.messages.length - 1 });
|
|
3332
|
+
const seq = isRoot ? beginTurn(this.messages.length - 1) : currentTurnSeq();
|
|
3333
|
+
this.turnMarkers.push({ seq, userIndex: this.messages.length - 1, tree });
|
|
3334
|
+
this.activeTurnSeq = seq;
|
|
3335
|
+
this.activeTurnUserIndex = this.messages.length - 1;
|
|
2698
3336
|
await this.persist(
|
|
2699
3337
|
opts?.persistAs !== void 0 ? { role: "user", content: opts.persistAs } : userMessage
|
|
2700
3338
|
);
|
|
3339
|
+
this.maybeScheduleTitle();
|
|
2701
3340
|
const { config, registry, cwd } = this.opts;
|
|
2702
3341
|
const aiTools = this.buildAiTools();
|
|
2703
3342
|
let autoContinues = 0;
|
|
@@ -2720,7 +3359,8 @@ ${finished.result}`
|
|
|
2720
3359
|
const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
|
|
2721
3360
|
const compacted = compactMessages([...this.messages], maxTokens);
|
|
2722
3361
|
if (compacted.compacted) {
|
|
2723
|
-
this.messages = await this.applyCompactionSummary(compacted);
|
|
3362
|
+
this.messages = await this.applyCompactionSummary(compacted, signal);
|
|
3363
|
+
this.turnMarkers = [];
|
|
2724
3364
|
}
|
|
2725
3365
|
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
2726
3366
|
const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
|
|
@@ -2793,7 +3433,10 @@ ${finished.result}`
|
|
|
2793
3433
|
delayMs,
|
|
2794
3434
|
reason: failure ? summarizeStreamError(failure) : `empty response (finish: ${lastFinishReason ?? "none"})`
|
|
2795
3435
|
};
|
|
2796
|
-
if (!await sleep(delayMs, signal))
|
|
3436
|
+
if (!await sleep(delayMs, signal)) {
|
|
3437
|
+
yield* this.persistInterrupted(text, toolCalls);
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
2797
3440
|
}
|
|
2798
3441
|
if (failure) {
|
|
2799
3442
|
yield { type: "error", error: failure };
|
|
@@ -2835,7 +3478,6 @@ ${finished.result}`
|
|
|
2835
3478
|
};
|
|
2836
3479
|
this.messages.push(assistantMessage);
|
|
2837
3480
|
await this.persist(assistantMessage);
|
|
2838
|
-
this.maybeScheduleTitle(text);
|
|
2839
3481
|
if (toolCalls.length === 0) {
|
|
2840
3482
|
let reason = null;
|
|
2841
3483
|
let nudgeText = AUTO_CONTINUE_NUDGE;
|
|
@@ -2883,7 +3525,11 @@ ${finished.result}`
|
|
|
2883
3525
|
try {
|
|
2884
3526
|
for (const call of toolCalls) {
|
|
2885
3527
|
if (signal.aborted) break;
|
|
2886
|
-
const
|
|
3528
|
+
const refusal = this.doomLoopRefusal(call);
|
|
3529
|
+
if (refusal !== null) {
|
|
3530
|
+
yield { type: "notice", message: refusal };
|
|
3531
|
+
}
|
|
3532
|
+
const result = refusal !== null ? { content: refusal, isError: true } : await this.executeTool(call, signal);
|
|
2887
3533
|
const toolMessage = {
|
|
2888
3534
|
role: "tool",
|
|
2889
3535
|
content: [
|
|
@@ -2960,7 +3606,7 @@ ${finished.result}`
|
|
|
2960
3606
|
if (userMemory) parts.push(userMemory);
|
|
2961
3607
|
const skills = discoverSkills(this.opts.cwd);
|
|
2962
3608
|
if (skills.length > 0) parts.push(formatSkillsBlock(skills));
|
|
2963
|
-
const git =
|
|
3609
|
+
const git = getGitSummaryCached(this.opts.cwd);
|
|
2964
3610
|
if (git) parts.push(formatGitSummary(git));
|
|
2965
3611
|
if (parts.length === 0) return;
|
|
2966
3612
|
const content = parts.join("\n\n");
|
|
@@ -2971,7 +3617,7 @@ ${finished.result}`
|
|
|
2971
3617
|
this.messages.unshift({ role: "system", content });
|
|
2972
3618
|
}
|
|
2973
3619
|
}
|
|
2974
|
-
async applyCompactionSummary(compacted) {
|
|
3620
|
+
async applyCompactionSummary(compacted, signal) {
|
|
2975
3621
|
const { config, model } = this.opts;
|
|
2976
3622
|
if (config.contextCompaction !== "summary" || !model) {
|
|
2977
3623
|
return compacted.messages;
|
|
@@ -2979,7 +3625,7 @@ ${finished.result}`
|
|
|
2979
3625
|
const headCount = compacted.messages[0]?.role === "system" ? 1 : 0;
|
|
2980
3626
|
const dropped = this.messages.slice(headCount, headCount + compacted.droppedCount);
|
|
2981
3627
|
try {
|
|
2982
|
-
const summary = await summarizeMessages(dropped, model);
|
|
3628
|
+
const summary = await summarizeMessages(dropped, model, signal);
|
|
2983
3629
|
const messages = compacted.messages.slice();
|
|
2984
3630
|
messages[headCount] = {
|
|
2985
3631
|
role: "user",
|
|
@@ -2992,15 +3638,23 @@ ${summary}`
|
|
|
2992
3638
|
}
|
|
2993
3639
|
}
|
|
2994
3640
|
async *streamOnce(aiTools, signal, timeoutScale = 1) {
|
|
2995
|
-
const { streamChat } = await import("./stream-
|
|
2996
|
-
|
|
3641
|
+
const { streamChat } = await import("./stream-H4BYBVAZ.js");
|
|
3642
|
+
for await (const event of streamChat({
|
|
2997
3643
|
model: this.opts.model,
|
|
2998
3644
|
messages: this.messages,
|
|
2999
3645
|
tools: aiTools,
|
|
3000
3646
|
abortSignal: signal,
|
|
3001
3647
|
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3 * timeoutScale,
|
|
3002
3648
|
firstPartTimeoutMs: this.opts.config.streamFirstChunkTimeoutSec * 1e3 * timeoutScale
|
|
3003
|
-
})
|
|
3649
|
+
})) {
|
|
3650
|
+
if (event.type === "finish" && event.truncated) {
|
|
3651
|
+
yield {
|
|
3652
|
+
type: "notice",
|
|
3653
|
+
message: "Response was cut short by the stream idle timeout \u2014 the reply may be incomplete."
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
yield event;
|
|
3657
|
+
}
|
|
3004
3658
|
}
|
|
3005
3659
|
buildAiTools() {
|
|
3006
3660
|
const plan = this.opts.config.permissionMode === "plan";
|
|
@@ -3030,6 +3684,23 @@ ${summary}`
|
|
|
3030
3684
|
return empty;
|
|
3031
3685
|
}
|
|
3032
3686
|
}
|
|
3687
|
+
// opencode's doom_loop guard: returns a refusal once the identical tool
|
|
3688
|
+
// call (name + stable-serialized args) has repeated config.doomLoopThreshold
|
|
3689
|
+
// times in a row; 0 disables. Any different signature resets the streak.
|
|
3690
|
+
// Subagents are separate loop instances, so each counts its own streak.
|
|
3691
|
+
doomLoopRefusal(call) {
|
|
3692
|
+
const threshold = this.opts.config.doomLoopThreshold;
|
|
3693
|
+
if (threshold <= 0) return null;
|
|
3694
|
+
const signature = `${call.name} ${stableStringify(call.args)}`;
|
|
3695
|
+
if (signature === this.lastToolSignature) {
|
|
3696
|
+
this.repeatedToolCalls += 1;
|
|
3697
|
+
} else {
|
|
3698
|
+
this.lastToolSignature = signature;
|
|
3699
|
+
this.repeatedToolCalls = 1;
|
|
3700
|
+
}
|
|
3701
|
+
if (this.repeatedToolCalls < threshold) return null;
|
|
3702
|
+
return `Refused: tool "${call.name}" was called ${this.repeatedToolCalls} times in a row with identical arguments. Stop retrying it \u2014 change your approach or report that you are blocked.`;
|
|
3703
|
+
}
|
|
3033
3704
|
async executeTool(call, signal) {
|
|
3034
3705
|
const { registry, config, cwd } = this.opts;
|
|
3035
3706
|
const tool = registry.get(call.name);
|
|
@@ -3074,7 +3745,15 @@ ${summary}`
|
|
|
3074
3745
|
}
|
|
3075
3746
|
let result;
|
|
3076
3747
|
try {
|
|
3077
|
-
result = await tool.execute(parsed.data, {
|
|
3748
|
+
result = await tool.execute(parsed.data, {
|
|
3749
|
+
cwd,
|
|
3750
|
+
abortSignal: signal,
|
|
3751
|
+
snapshotContext: {
|
|
3752
|
+
owner: (this.opts.subagentDepth ?? 0) > 0 ? "subagent" : "root",
|
|
3753
|
+
turn: this.activeTurnSeq,
|
|
3754
|
+
messageIndex: this.activeTurnUserIndex
|
|
3755
|
+
}
|
|
3756
|
+
});
|
|
3078
3757
|
} catch (error) {
|
|
3079
3758
|
if (signal.aborted) {
|
|
3080
3759
|
return { content: "Tool execution aborted.", isError: true };
|
|
@@ -3105,34 +3784,34 @@ export {
|
|
|
3105
3784
|
MAX_IMAGE_DIMENSION,
|
|
3106
3785
|
probeImageDimensions,
|
|
3107
3786
|
reconcileToolCalls,
|
|
3787
|
+
isSensitivePath,
|
|
3108
3788
|
isAllowedByRules,
|
|
3109
3789
|
buildAllowRule,
|
|
3110
3790
|
isDangerousCommand,
|
|
3791
|
+
diffTreeNames,
|
|
3111
3792
|
listSnapshots,
|
|
3112
3793
|
clearSnapshots,
|
|
3113
3794
|
hydrateSnapshots,
|
|
3114
3795
|
listTurnSnapshots,
|
|
3115
3796
|
resolveSnapshotContent,
|
|
3116
|
-
undoTurnSnapshots,
|
|
3117
3797
|
rewindToSnapshot,
|
|
3118
|
-
TodoStore,
|
|
3119
|
-
formatTodos,
|
|
3120
|
-
parseTodoArgs,
|
|
3121
|
-
loadTodos,
|
|
3122
|
-
resetTodos,
|
|
3123
|
-
discoverSkills,
|
|
3124
3798
|
defaultTaskManager,
|
|
3125
3799
|
resolveShell,
|
|
3126
3800
|
bashTool,
|
|
3127
3801
|
SKIP_DIRS,
|
|
3128
3802
|
createIgnorePredicate,
|
|
3129
|
-
isSensitivePath2 as isSensitivePath,
|
|
3130
3803
|
formatTaskList,
|
|
3131
3804
|
formatTaskStarted,
|
|
3132
3805
|
formatTaskFinished,
|
|
3806
|
+
TodoStore,
|
|
3807
|
+
formatTodos,
|
|
3808
|
+
parseTodoArgs,
|
|
3809
|
+
loadTodos,
|
|
3810
|
+
resetTodos,
|
|
3811
|
+
discoverSkills,
|
|
3133
3812
|
createDefaultRegistry,
|
|
3134
3813
|
appendUserMemory,
|
|
3135
3814
|
PLAN_MODE_PROMPT,
|
|
3136
3815
|
AgentLoop
|
|
3137
3816
|
};
|
|
3138
|
-
//# sourceMappingURL=chunk-
|
|
3817
|
+
//# sourceMappingURL=chunk-3L6QKJM4.js.map
|