agent.libx.js 0.93.29 → 0.93.30
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/cli/cli.ts +56 -17
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +227 -55
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +22 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -26,7 +26,10 @@ var init_redact = __esm({
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
// src/tools.structured.ts
|
|
29
|
-
|
|
29
|
+
function ckAbort(signal) {
|
|
30
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
31
|
+
}
|
|
32
|
+
async function walkFiles(fs, dir, signal, out = []) {
|
|
30
33
|
let entries;
|
|
31
34
|
try {
|
|
32
35
|
entries = await fs.readDir(dir);
|
|
@@ -34,8 +37,9 @@ async function walkFiles(fs, dir, out = []) {
|
|
|
34
37
|
return out;
|
|
35
38
|
}
|
|
36
39
|
for (const name of entries.sort()) {
|
|
40
|
+
ckAbort(signal);
|
|
37
41
|
const p = dir === "/" ? `/${name}` : `${dir}/${name}`;
|
|
38
|
-
if (await fs.isDirectory(p)) await walkFiles(fs, p, out);
|
|
42
|
+
if (await fs.isDirectory(p)) await walkFiles(fs, p, signal, out);
|
|
39
43
|
else out.push(p);
|
|
40
44
|
}
|
|
41
45
|
return out;
|
|
@@ -92,13 +96,14 @@ function signaturesOf(content, cap = 40) {
|
|
|
92
96
|
}
|
|
93
97
|
return out;
|
|
94
98
|
}
|
|
95
|
-
async function repoIndex(fs, glob, mode = "code") {
|
|
99
|
+
async function repoIndex(fs, glob, mode = "code", signal) {
|
|
96
100
|
const scope = glob ? anchoredGlob(fs, String(glob)) : null;
|
|
97
101
|
const filter = mode === "code" ? isCode : mode === "docs" ? isDoc : (p) => isCode(p) || isDoc(p);
|
|
98
|
-
const files = (await walkFiles(fs, fsCwd(fs))).filter((p) => scope ? scope.test(p) : filter(p));
|
|
102
|
+
const files = (await walkFiles(fs, fsCwd(fs), signal)).filter((p) => scope ? scope.test(p) : filter(p));
|
|
99
103
|
const blocks = [];
|
|
100
104
|
let shown = 0;
|
|
101
105
|
for (const path of files) {
|
|
106
|
+
ckAbort(signal);
|
|
102
107
|
let content;
|
|
103
108
|
try {
|
|
104
109
|
content = await fs.readFile(path);
|
|
@@ -218,7 +223,7 @@ var init_tools_structured = __esm({
|
|
|
218
223
|
const include = pats.filter((p) => !p.startsWith("!")).map((p) => anchoredGlob(ctx.fs, p));
|
|
219
224
|
const exclude = pats.filter((p) => p.startsWith("!")).map((p) => anchoredGlob(ctx.fs, p.slice(1)));
|
|
220
225
|
const includes = include.length ? include : [anchoredGlob(ctx.fs, "**")];
|
|
221
|
-
const hits = (await walkFiles(ctx.fs, fsCwd(ctx.fs))).filter(
|
|
226
|
+
const hits = (await walkFiles(ctx.fs, fsCwd(ctx.fs), ctx.signal)).filter(
|
|
222
227
|
(p) => includes.some((re) => re.test(p)) && !exclude.some((re) => re.test(p))
|
|
223
228
|
);
|
|
224
229
|
return hits.length ? hits.join("\n") : "(no matches)";
|
|
@@ -245,11 +250,12 @@ var init_tools_structured = __esm({
|
|
|
245
250
|
throw new Error(`invalid regex: ${String(e)}`);
|
|
246
251
|
}
|
|
247
252
|
const scope = glob ? anchoredGlob(ctx.fs, String(glob)) : null;
|
|
248
|
-
const files = (await walkFiles(ctx.fs, fsCwd(ctx.fs))).filter((p) => !scope || scope.test(p));
|
|
253
|
+
const files = (await walkFiles(ctx.fs, fsCwd(ctx.fs), ctx.signal)).filter((p) => !scope || scope.test(p));
|
|
249
254
|
const ctxN = Math.max(0, Number(context ?? 0));
|
|
250
255
|
const out = [];
|
|
251
256
|
const matched = [];
|
|
252
257
|
for (const path of files) {
|
|
258
|
+
ckAbort(ctx.signal);
|
|
253
259
|
let content;
|
|
254
260
|
try {
|
|
255
261
|
content = await ctx.fs.readFile(path);
|
|
@@ -285,7 +291,7 @@ var init_tools_structured = __esm({
|
|
|
285
291
|
scope: { type: "string", enum: ["code", "docs", "all"], description: 'what to map: "code" (default), "docs", or "all"' }
|
|
286
292
|
}
|
|
287
293
|
},
|
|
288
|
-
run: ({ glob, scope }, ctx) => repoIndex(ctx.fs, glob, scope || "code")
|
|
294
|
+
run: ({ glob, scope }, ctx) => repoIndex(ctx.fs, glob, scope || "code", ctx.signal)
|
|
289
295
|
};
|
|
290
296
|
writeTool = {
|
|
291
297
|
name: "Write",
|
|
@@ -1671,7 +1677,7 @@ close access f`;
|
|
|
1671
1677
|
|
|
1672
1678
|
// cli/cli.ts
|
|
1673
1679
|
import { join as join9, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
|
|
1674
|
-
import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported } from "ai.libx.js";
|
|
1680
|
+
import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions } from "ai.libx.js";
|
|
1675
1681
|
|
|
1676
1682
|
// src/llm.ts
|
|
1677
1683
|
function contentText(content) {
|
|
@@ -2002,9 +2008,9 @@ async function loadCommands(fs, dir, opts = {}) {
|
|
|
2002
2008
|
properties: { name: { type: "string" }, args: { type: "string", description: "arguments to fill the template" } }
|
|
2003
2009
|
},
|
|
2004
2010
|
async run({ name, args }, ctx) {
|
|
2005
|
-
const
|
|
2006
|
-
const c = commands.find((x) => x.name ===
|
|
2007
|
-
if (!c) return `Error: no command named '${
|
|
2011
|
+
const slug2 = String(name ?? "").replace(/^\//, "");
|
|
2012
|
+
const c = commands.find((x) => x.name === slug2);
|
|
2013
|
+
if (!c) return `Error: no command named '${slug2}'. Available: ${commands.map((x) => x.name).join(", ")}`;
|
|
2008
2014
|
return expandCommand(ctx.fs, c, String(args ?? ""));
|
|
2009
2015
|
}
|
|
2010
2016
|
};
|
|
@@ -2039,9 +2045,9 @@ async function loadMemory(fs, dir, opts = {}) {
|
|
|
2039
2045
|
const lines = md.split("\n");
|
|
2040
2046
|
if (!header) header = lines.filter((l) => !/^\s*-\s*\[.+\]\(.+\.md\)/.test(l)).join("\n").trim();
|
|
2041
2047
|
for (const l of lines.filter((l2) => /^\s*-\s*\[.+\]\(.+\.md\)/.test(l2))) {
|
|
2042
|
-
const
|
|
2043
|
-
if (
|
|
2044
|
-
seenSlugs.add(
|
|
2048
|
+
const slug2 = l.match(/\]\(([^)]+)\.md\)/)?.[1];
|
|
2049
|
+
if (slug2 && !seenSlugs.has(slug2)) {
|
|
2050
|
+
seenSlugs.add(slug2);
|
|
2045
2051
|
allPointers.push(l);
|
|
2046
2052
|
}
|
|
2047
2053
|
}
|
|
@@ -2057,20 +2063,20 @@ function slugify(s, fallback = "note") {
|
|
|
2057
2063
|
const base = String(s ?? "").trim().toLowerCase().replace(/\.md$/i, "").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 48);
|
|
2058
2064
|
return base || fallback;
|
|
2059
2065
|
}
|
|
2060
|
-
async function writeFact(fs, dir,
|
|
2066
|
+
async function writeFact(fs, dir, slug2, body, opts) {
|
|
2061
2067
|
await mkdirp(fs, dir);
|
|
2062
2068
|
const content = opts?.type ? `---
|
|
2063
2069
|
type: ${opts.type}
|
|
2064
2070
|
---
|
|
2065
2071
|
|
|
2066
2072
|
${body}` : body;
|
|
2067
|
-
await fs.writeFile(`${dir}/${
|
|
2073
|
+
await fs.writeFile(`${dir}/${slug2}.md`, content.endsWith("\n") ? content : content + "\n");
|
|
2068
2074
|
const indexPath = `${dir}/MEMORY.md`;
|
|
2069
2075
|
const idx = await fs.exists(indexPath) ? await fs.readFile(indexPath) : "# Memory Index\n";
|
|
2070
2076
|
const summary = opts?.description || body.split("\n")[0].slice(0, 80);
|
|
2071
|
-
const line = `- [${
|
|
2077
|
+
const line = `- [${slug2}](${slug2}.md) \u2014 ${summary}`;
|
|
2072
2078
|
const lines = idx.split("\n");
|
|
2073
|
-
const at = lines.findIndex((l) => l.includes(`(${
|
|
2079
|
+
const at = lines.findIndex((l) => l.includes(`(${slug2}.md)`));
|
|
2074
2080
|
if (at >= 0) {
|
|
2075
2081
|
if (lines[at] !== line) {
|
|
2076
2082
|
lines[at] = line;
|
|
@@ -2121,8 +2127,8 @@ async function listSlugs(fs, dir, prefix = "") {
|
|
|
2121
2127
|
}
|
|
2122
2128
|
return out;
|
|
2123
2129
|
}
|
|
2124
|
-
async function loadFact(fs, dir,
|
|
2125
|
-
const path = `${dir}/${
|
|
2130
|
+
async function loadFact(fs, dir, slug2) {
|
|
2131
|
+
const path = `${dir}/${slug2}.md`;
|
|
2126
2132
|
try {
|
|
2127
2133
|
const raw = await fs.readFile(path);
|
|
2128
2134
|
const { body } = splitFrontmatter(raw);
|
|
@@ -2131,9 +2137,9 @@ async function loadFact(fs, dir, slug) {
|
|
|
2131
2137
|
return null;
|
|
2132
2138
|
}
|
|
2133
2139
|
}
|
|
2134
|
-
async function loadFactMulti(fs, dirs,
|
|
2140
|
+
async function loadFactMulti(fs, dirs, slug2) {
|
|
2135
2141
|
for (const d of dirs) {
|
|
2136
|
-
const r = await loadFact(fs, d,
|
|
2142
|
+
const r = await loadFact(fs, d, slug2);
|
|
2137
2143
|
if (r != null) return r;
|
|
2138
2144
|
}
|
|
2139
2145
|
return null;
|
|
@@ -2159,9 +2165,9 @@ function recallTool(fs, dirs) {
|
|
|
2159
2165
|
pattern: { type: "string", description: 'glob pattern to match against slugs (e.g. "auth*", "*database*")' }
|
|
2160
2166
|
}
|
|
2161
2167
|
},
|
|
2162
|
-
async run({ slug, slugs, pattern }, ctx) {
|
|
2168
|
+
async run({ slug: slug2, slugs, pattern }, ctx) {
|
|
2163
2169
|
let targets = [];
|
|
2164
|
-
if (
|
|
2170
|
+
if (slug2) targets = [cleanSlug(slug2)];
|
|
2165
2171
|
else if (Array.isArray(slugs)) targets = slugs.map(cleanSlug).filter(Boolean);
|
|
2166
2172
|
else if (pattern) {
|
|
2167
2173
|
const escaped = String(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
@@ -2212,19 +2218,19 @@ function memorySearchTool(fs, dirs) {
|
|
|
2212
2218
|
matcher = (l) => l.toLowerCase().includes(lower);
|
|
2213
2219
|
}
|
|
2214
2220
|
const loaded = [];
|
|
2215
|
-
for (const
|
|
2216
|
-
const body = await loadFactMulti(fs, dirs,
|
|
2217
|
-
if (body) loaded.push({ slug, body });
|
|
2221
|
+
for (const slug2 of slugs) {
|
|
2222
|
+
const body = await loadFactMulti(fs, dirs, slug2);
|
|
2223
|
+
if (body) loaded.push({ slug: slug2, body });
|
|
2218
2224
|
}
|
|
2219
2225
|
const idf = idfWeights(loaded.map((l) => l.body));
|
|
2220
2226
|
const qTokens = tokenize(q2);
|
|
2221
2227
|
const hits = [];
|
|
2222
|
-
for (const { slug, body } of loaded) {
|
|
2228
|
+
for (const { slug: slug2, body } of loaded) {
|
|
2223
2229
|
const lines = body.split("\n");
|
|
2224
2230
|
const matchLine = lines.find(matcher);
|
|
2225
2231
|
if (!matchLine && !relevanceScore(body, qTokens, idf)) continue;
|
|
2226
2232
|
const snippet = matchLine?.trim().slice(0, 120) || lines.find((l) => l.trim())?.trim().slice(0, 120) || "";
|
|
2227
|
-
hits.push({ slug, snippet, score: relevanceScore(body, qTokens, idf) });
|
|
2233
|
+
hits.push({ slug: slug2, snippet, score: relevanceScore(body, qTokens, idf) });
|
|
2228
2234
|
}
|
|
2229
2235
|
if (!hits.length) return "(no matches)";
|
|
2230
2236
|
hits.sort((a, b) => b.score - a.score);
|
|
@@ -2248,11 +2254,11 @@ function rememberTool(fs, dir, memOpts = {}) {
|
|
|
2248
2254
|
description: { type: "string", description: "one-line summary for the memory index (\u226480 chars)" }
|
|
2249
2255
|
}
|
|
2250
2256
|
},
|
|
2251
|
-
async run({ fact, slug, type, description }, ctx) {
|
|
2257
|
+
async run({ fact, slug: slug2, type, description }, ctx) {
|
|
2252
2258
|
const body = String(fact ?? "").trim();
|
|
2253
2259
|
if (!body) return `Error: nothing to remember (empty fact).`;
|
|
2254
2260
|
if (++writes > maxWrites) return `Rate limit: too many memories this session (${maxWrites}). Only persist genuinely durable facts.`;
|
|
2255
|
-
const name = slugify(
|
|
2261
|
+
const name = slugify(slug2 || body.split("\n")[0]);
|
|
2256
2262
|
const isGlobal = (type === "user" || type === "feedback") && memOpts.userDir;
|
|
2257
2263
|
const targetDir = isGlobal ? memOpts.userDir : dir;
|
|
2258
2264
|
await writeFact(fs, targetDir, name, body, { type, description });
|
|
@@ -3008,6 +3014,7 @@ var Agent = class _Agent {
|
|
|
3008
3014
|
toolCallsTotal += toolCalls.length;
|
|
3009
3015
|
if (o.maxToolCalls && toolCallsTotal > o.maxToolCalls) return kill("max_tool_calls");
|
|
3010
3016
|
for (const tc of toolCalls) {
|
|
3017
|
+
if (o.signal?.aborted) return kill("aborted");
|
|
3011
3018
|
const raw = await this.dispatch(tc);
|
|
3012
3019
|
let content;
|
|
3013
3020
|
if (typeof raw === "string") {
|
|
@@ -3473,6 +3480,99 @@ init_tools();
|
|
|
3473
3480
|
init_tools_structured();
|
|
3474
3481
|
init_logging();
|
|
3475
3482
|
var log4 = forComponent("scratch");
|
|
3483
|
+
var SCRATCH_DIR = "/scratch";
|
|
3484
|
+
function shortArgs(args) {
|
|
3485
|
+
try {
|
|
3486
|
+
const s = JSON.stringify(args ?? {});
|
|
3487
|
+
return s.length > 50 ? s.slice(0, 47) + "\u2026" : s;
|
|
3488
|
+
} catch {
|
|
3489
|
+
return "";
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
var slug = (s) => s.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase().slice(0, 32) || "out";
|
|
3493
|
+
var Scratch = class {
|
|
3494
|
+
constructor(fs, options = {}) {
|
|
3495
|
+
this.fs = fs;
|
|
3496
|
+
this.options = { threshold: 1500, dir: SCRATCH_DIR, previewChars: 320, ...options };
|
|
3497
|
+
}
|
|
3498
|
+
fs;
|
|
3499
|
+
options;
|
|
3500
|
+
seq = 0;
|
|
3501
|
+
dirReady;
|
|
3502
|
+
/** Number of captures so far. */
|
|
3503
|
+
get count() {
|
|
3504
|
+
return this.seq;
|
|
3505
|
+
}
|
|
3506
|
+
/** Wrap a tool: oversized STRING results spill to a scratch file; everything else passes through. */
|
|
3507
|
+
capture(tool) {
|
|
3508
|
+
const { threshold, dir, previewChars } = this.options;
|
|
3509
|
+
return {
|
|
3510
|
+
...tool,
|
|
3511
|
+
run: async (args, ctx) => {
|
|
3512
|
+
const raw = await tool.run(args, ctx);
|
|
3513
|
+
if (typeof raw !== "string" || raw.length <= threshold) return raw;
|
|
3514
|
+
const id = "a" + ++this.seq;
|
|
3515
|
+
const path = `${dir}/${id}-${slug(tool.name)}.txt`;
|
|
3516
|
+
const header = `# ${tool.name}(${shortArgs(args)}) \u2014 ${raw.length} bytes
|
|
3517
|
+
`;
|
|
3518
|
+
try {
|
|
3519
|
+
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
3520
|
+
await this.fs.writeFile(path, header + raw);
|
|
3521
|
+
} catch (e) {
|
|
3522
|
+
log4.debug("scratch write failed; returning raw", e);
|
|
3523
|
+
return raw;
|
|
3524
|
+
}
|
|
3525
|
+
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
3526
|
+
return `[scratch ${path} \xB7 ${tool.name} \xB7 ${raw.length} bytes \u2014 full output saved out of context to keep it clean]
|
|
3527
|
+
preview: ${preview}\u2026
|
|
3528
|
+
To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026", over: "${path}" }). Do NOT guess at what the preview cuts off.`;
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
/** Wrap many tools at once. */
|
|
3533
|
+
captureAll(tools) {
|
|
3534
|
+
return tools.map((t) => this.capture(t));
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
var ASK_PROMPT = "You are a retrieval-extraction step with Read, Grep and Glob over a scratch filesystem holding raw outputs from earlier tools. Find the information that answers the question and return it concisely, quoting values/facts verbatim. Do NOT add analysis or anything not grounded in the files. If the answer is not present, say so plainly.";
|
|
3538
|
+
function makeAskTool(o) {
|
|
3539
|
+
const dir = o.dir ?? SCRATCH_DIR;
|
|
3540
|
+
return {
|
|
3541
|
+
name: "Ask",
|
|
3542
|
+
description: "Answer a question by peeking into the scratch files \u2014 large earlier outputs (web search, big reads, subagent reports) kept out of your context. Pass `over` with a scratch path for a targeted lookup, or omit it to search the scratch dir. Returns only the extracted answer; the full data never enters your context.",
|
|
3543
|
+
parameters: {
|
|
3544
|
+
type: "object",
|
|
3545
|
+
required: ["question"],
|
|
3546
|
+
properties: {
|
|
3547
|
+
question: { type: "string", description: "what you need from the scratch files" },
|
|
3548
|
+
over: { type: "string", description: 'scratch path to read, e.g. "/scratch/a3-websearch.txt"; omit to search' }
|
|
3549
|
+
}
|
|
3550
|
+
},
|
|
3551
|
+
async run({ question, over }) {
|
|
3552
|
+
const q2 = String(question ?? "").trim();
|
|
3553
|
+
if (!q2) return "Error: empty question";
|
|
3554
|
+
const child = new Agent({
|
|
3555
|
+
ai: o.ai,
|
|
3556
|
+
model: o.model,
|
|
3557
|
+
fs: o.fs,
|
|
3558
|
+
tools: toolsByName(["Read", "Grep", "Glob"]),
|
|
3559
|
+
maxSteps: o.maxSteps ?? 6,
|
|
3560
|
+
systemPrompt: ASK_PROMPT
|
|
3561
|
+
});
|
|
3562
|
+
const hint = over ? `Start by reading: ${over}.` : `Grep/Glob ${dir} to find the relevant file(s) first.`;
|
|
3563
|
+
try {
|
|
3564
|
+
const res = await child.run(`${hint}
|
|
3565
|
+
|
|
3566
|
+
Question: ${q2}`);
|
|
3567
|
+
const answer = (res.text ?? "").trim();
|
|
3568
|
+
return answer || "(no answer found in scratch)";
|
|
3569
|
+
} catch (e) {
|
|
3570
|
+
log4.debug("Ask peek failed", e);
|
|
3571
|
+
return `Error querying scratch: ${e?.message ?? e}`;
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3476
3576
|
|
|
3477
3577
|
// src/lessons.ts
|
|
3478
3578
|
init_logging();
|
|
@@ -3531,15 +3631,15 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
3531
3631
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
3532
3632
|
const lesson = m?.[1]?.trim().slice(0, 200) ?? "";
|
|
3533
3633
|
if (!lesson || /^none\b/i.test(lesson)) return null;
|
|
3534
|
-
const
|
|
3634
|
+
const slug2 = ("lesson-" + slugify(lesson)).slice(0, 56);
|
|
3535
3635
|
try {
|
|
3536
|
-
await writeFact(o.fs, o.dir,
|
|
3636
|
+
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
3537
3637
|
} catch (e) {
|
|
3538
3638
|
log6.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
3539
3639
|
return null;
|
|
3540
3640
|
}
|
|
3541
|
-
log6.debug(`reflection persisted ${
|
|
3542
|
-
return
|
|
3641
|
+
log6.debug(`reflection persisted ${slug2}`);
|
|
3642
|
+
return slug2;
|
|
3543
3643
|
}
|
|
3544
3644
|
function digestRun(messages, maxChars) {
|
|
3545
3645
|
const lines = [];
|
|
@@ -3570,6 +3670,11 @@ var DuplexAgentOptions = class {
|
|
|
3570
3670
|
actModel = "anthropic/claude-sonnet-4-6";
|
|
3571
3671
|
/** Premium reasoning model. Set to `false` to disable the Think tier entirely. */
|
|
3572
3672
|
thinkModel = "anthropic/claude-opus-4-8";
|
|
3673
|
+
/** Per-worker providerOptions, derived from the worker's actual model at spawn time (IoC — keeps duplex
|
|
3674
|
+
* provider-agnostic). Workers override the reflex/main model, so provider-specific options (e.g. cursor's
|
|
3675
|
+
* cwd/cursorSession) must be recomputed for the worker's model, never inherited from the main template —
|
|
3676
|
+
* leaking cursor options to an anthropic worker is a hard 400. Returns undefined → no providerOptions. */
|
|
3677
|
+
providerOptionsFor;
|
|
3573
3678
|
/** Escape hatches merged over the derived per-agent options. */
|
|
3574
3679
|
reflexOptions;
|
|
3575
3680
|
actOptions;
|
|
@@ -3832,6 +3937,9 @@ ${recent}` : brief) + verify;
|
|
|
3832
3937
|
model: tierModel,
|
|
3833
3938
|
...tier === "think" ? { reasoning: tierOpts?.reasoning ?? "high" } : {},
|
|
3834
3939
|
...tierOpts,
|
|
3940
|
+
// Recompute providerOptions for THIS worker's model (after tierOpts so it wins over any inherited
|
|
3941
|
+
// main-template value) — prevents cursor-only cwd/cursorSession leaking onto an anthropic worker.
|
|
3942
|
+
providerOptions: o.providerOptionsFor?.(tierModel),
|
|
3835
3943
|
...workerHost ? { host: workerHost } : {},
|
|
3836
3944
|
...hooks ? { hooks } : {},
|
|
3837
3945
|
signal: controller.signal
|
|
@@ -5475,6 +5583,10 @@ function toCursorMcp(servers) {
|
|
|
5475
5583
|
}
|
|
5476
5584
|
return Object.keys(out).length ? { mcpServers: out } : void 0;
|
|
5477
5585
|
}
|
|
5586
|
+
function cursorProviderOptions(model, cwd, mcpServers) {
|
|
5587
|
+
if (!(model ?? "").startsWith("cursor/")) return void 0;
|
|
5588
|
+
return { cwd, ...toCursorMcp(mcpServers) ?? {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } };
|
|
5589
|
+
}
|
|
5478
5590
|
async function buildAgent(o) {
|
|
5479
5591
|
if (typeof o.sandbox !== "boolean")
|
|
5480
5592
|
throw new Error(
|
|
@@ -5566,6 +5678,8 @@ Reference files in them by their mount path (the left side).`;
|
|
|
5566
5678
|
const jobs = new ShellJobRegistry({ cwd, killOnExit: true });
|
|
5567
5679
|
realShell = [makeRealShellTool({ cwd, registry: jobs }), ...makeShellJobTools(jobs)];
|
|
5568
5680
|
}
|
|
5681
|
+
const scratchDir = o.scratch ? o.scratchDir ?? `${cwd}/.agent/scratch` : void 0;
|
|
5682
|
+
const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
|
|
5569
5683
|
return new Agent({
|
|
5570
5684
|
ai: o.ai,
|
|
5571
5685
|
fs,
|
|
@@ -5576,7 +5690,10 @@ Reference files in them by their mount path (the left side).`;
|
|
|
5576
5690
|
// would corrupt those calls.
|
|
5577
5691
|
// Warm cursor session (default on; CURSOR_WARM=0 to disable): one long-lived agent reused across
|
|
5578
5692
|
// turns, amortising the ~2s Agent.create + h2 handshake. Keyed per agentx launch.
|
|
5579
|
-
...
|
|
5693
|
+
...(() => {
|
|
5694
|
+
const po = cursorProviderOptions(o.model, cwd, o.mcpServers);
|
|
5695
|
+
return po ? { providerOptions: po } : {};
|
|
5696
|
+
})(),
|
|
5580
5697
|
...(() => {
|
|
5581
5698
|
const now5 = /* @__PURE__ */ new Date();
|
|
5582
5699
|
const platformNames = { darwin: "macOS", linux: "Linux", win32: "Windows" };
|
|
@@ -5590,7 +5707,13 @@ SANDBOX: you operate on an in-memory COPY of this directory \u2014 the real disk
|
|
|
5590
5707
|
The filesystem root '/' is the real machine root \u2014 you have full filesystem access, like a normal shell. Use paths relative to the working directory, or absolute paths. Secret files (.env, .ssh, keys, .git) are hidden for safety.`;
|
|
5591
5708
|
const shellNote = useRealShell && !virtual ? "The `Shell` tool runs /bin/sh \u2014 you can run any installed binary (git, bun, node, curl, etc). Use `Shell` for all shell commands." : void 0;
|
|
5592
5709
|
const toneNote = "Be concise. Do not use emojis unless the user asks. Do not explain what you are about to do \u2014 just do it.";
|
|
5593
|
-
const
|
|
5710
|
+
const mcpNote = (() => {
|
|
5711
|
+
const names = new Set(Object.keys(o.mcpServers ?? {}));
|
|
5712
|
+
for (const t of o.extraTools ?? []) if (t.name.startsWith("mcp__")) names.add(t.name.slice(5).split("__")[0]);
|
|
5713
|
+
const list = [...names];
|
|
5714
|
+
return `You are agentx (the agent.libx.js runtime) \u2014 NOT Claude Code or Claude Desktop, which are separate agents with their own config. Your MCP servers are configured in .agent/settings.json or .agent/config.* (this project's .agent/ or ~/.agent/), and their tools are already in your tool list as \`mcp__<server>__<tool>\`.${list.length ? ` Configured MCP servers: ${list.join(", ")}.` : " No MCP servers are currently configured."} When asked which MCPs/tools you have, answer from your own tool list \u2014 never read Claude Code/Desktop config (e.g. ~/.claude.json, claude_desktop_config.json) to answer; those belong to a different agent.`;
|
|
5715
|
+
})();
|
|
5716
|
+
const extra = [o.appendSystemPrompt, envNote, cwdNote, shellNote, toneNote, mountNote, mcpNote].filter(Boolean).join("\n\n");
|
|
5594
5717
|
const basePrompt = (() => {
|
|
5595
5718
|
let p = new AgentOptions().systemPrompt;
|
|
5596
5719
|
if (!virtual) p = p.replace("operating on a virtual filesystem", "operating on the host filesystem");
|
|
@@ -5600,10 +5723,16 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
|
|
|
5600
5723
|
return { systemPrompt: basePrompt + "\n\n" + extra };
|
|
5601
5724
|
})(),
|
|
5602
5725
|
tools: (() => {
|
|
5603
|
-
|
|
5604
|
-
|
|
5726
|
+
let base = toolsByName([...o.tools ?? DEFAULT_TOOLS, ...autoWebTools()]);
|
|
5727
|
+
const tail = [...o.extraTools ?? []];
|
|
5728
|
+
if (scratch) {
|
|
5729
|
+
const CAPTURE2 = /* @__PURE__ */ new Set(["WebSearch", "WebFetch"]);
|
|
5730
|
+
base = base.map((t) => CAPTURE2.has(t.name) ? scratch.capture(t) : t);
|
|
5731
|
+
tail.push(makeAskTool({ fs, ai: o.ai, model: o.scratchAskModel ?? o.model ?? "anthropic/claude-sonnet-4-6", dir: scratchDir }));
|
|
5732
|
+
}
|
|
5733
|
+
if (!realShell.length) return [...base, ...tail];
|
|
5605
5734
|
const filtered = base.filter((t) => t.name !== "bash");
|
|
5606
|
-
return [...filtered, ...realShell, ...
|
|
5735
|
+
return [...filtered, ...realShell, ...tail];
|
|
5607
5736
|
})(),
|
|
5608
5737
|
maxSteps: o.maxSteps ?? 30,
|
|
5609
5738
|
...o.reasoning != null ? { reasoning: o.reasoning } : {},
|
|
@@ -7837,6 +7966,7 @@ function parseArgs(argv) {
|
|
|
7837
7966
|
else if (x === "--ask") a.ask = true;
|
|
7838
7967
|
else if (x === "--yes" || x === "-y") a.yes = true;
|
|
7839
7968
|
else if (x === "--vfs" || x === "--sandbox") a.vfs = true;
|
|
7969
|
+
else if (x === "--scratch") a.scratch = true;
|
|
7840
7970
|
else if (x === "--boddb") a.boddb = val(++i, x);
|
|
7841
7971
|
else if (x === "--seed") a.seed = true;
|
|
7842
7972
|
else if (x === "--shell") a.shell = true;
|
|
@@ -7890,6 +8020,7 @@ Flags:
|
|
|
7890
8020
|
--no-stream disable token streaming
|
|
7891
8021
|
(default: disk mode \u2014 full real filesystem access, like Claude Code)
|
|
7892
8022
|
--vfs, --sandbox sandbox mode: work over an in-memory copy of cwd \u2014 real disk is NEVER modified
|
|
8023
|
+
--scratch spill big web outputs to scratch files (kept out of context; peek via Grep/Ask)
|
|
7893
8024
|
--boddb <dir> database-backed workspace: files live in a persistent bod-db store at <dir>,
|
|
7894
8025
|
surviving across runs \u2014 real disk is NEVER modified (DB-native; add --seed below)
|
|
7895
8026
|
--seed with --boddb: hydrate the store from cwd on the first run (empty DB) only
|
|
@@ -8356,6 +8487,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
|
|
|
8356
8487
|
seed: args.seed,
|
|
8357
8488
|
realShell: args.shell,
|
|
8358
8489
|
// undefined → core.ts defaults (on for disk, off for sandbox/boddb)
|
|
8490
|
+
scratch: args.scratch,
|
|
8359
8491
|
appendSystemPrompt: args.appendSystemPrompt,
|
|
8360
8492
|
addDirs: args.addDirs,
|
|
8361
8493
|
stream: args.stream,
|
|
@@ -8719,7 +8851,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8719
8851
|
return { decision: "deny" };
|
|
8720
8852
|
};
|
|
8721
8853
|
if (duplex) {
|
|
8722
|
-
const { host: _host, stream: _stream, signal: _signal, ...wo } = agent.options;
|
|
8854
|
+
const { host: _host, stream: _stream, signal: _signal, providerOptions: _po, ...wo } = agent.options;
|
|
8723
8855
|
workerOptions = wo;
|
|
8724
8856
|
if (workerOptions.permissions)
|
|
8725
8857
|
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: void 0, ask: duplexAsk });
|
|
@@ -8799,6 +8931,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8799
8931
|
...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
|
|
8800
8932
|
actModel: agent.options.model,
|
|
8801
8933
|
actOptions: workerOptions,
|
|
8934
|
+
providerOptionsFor: (m) => cursorProviderOptions(m, cwd, cfg.mcpServers),
|
|
8802
8935
|
...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
|
|
8803
8936
|
host,
|
|
8804
8937
|
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true } : {},
|
|
@@ -8901,15 +9034,30 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8901
9034
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
8902
9035
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
8903
9036
|
};
|
|
9037
|
+
const forceQuit = (code = 130) => {
|
|
9038
|
+
try {
|
|
9039
|
+
voiceIO?.stop();
|
|
9040
|
+
} catch {
|
|
9041
|
+
}
|
|
9042
|
+
try {
|
|
9043
|
+
disposeCursorSessions();
|
|
9044
|
+
} catch {
|
|
9045
|
+
}
|
|
9046
|
+
void closeMcp(mounted);
|
|
9047
|
+
process.exit(code);
|
|
9048
|
+
};
|
|
8904
9049
|
process.on("SIGINT", () => {
|
|
8905
9050
|
if (activeTurn) {
|
|
9051
|
+
if (aborting) {
|
|
9052
|
+
err(red("\n \u23FB force-quit\n"));
|
|
9053
|
+
forceQuit();
|
|
9054
|
+
return;
|
|
9055
|
+
}
|
|
8906
9056
|
activeTurn.abort();
|
|
8907
9057
|
voiceIO?.interrupt();
|
|
8908
9058
|
return;
|
|
8909
9059
|
}
|
|
8910
|
-
|
|
8911
|
-
void closeMcp(mounted);
|
|
8912
|
-
process.exit(130);
|
|
9060
|
+
forceQuit();
|
|
8913
9061
|
});
|
|
8914
9062
|
installCancelGuards(mounted);
|
|
8915
9063
|
const store = new SessionStore(cwd);
|
|
@@ -9492,7 +9640,7 @@ ${extra}` : body);
|
|
|
9492
9640
|
commands: { desc: "pick a custom slash command to run (./.agent/commands)", run: () => pickAndRun("command") },
|
|
9493
9641
|
skills: { desc: "pick a skill to run (./.agent/skills)", run: () => pickAndRun("skill") },
|
|
9494
9642
|
mcp: {
|
|
9495
|
-
desc: "manage MCP servers \u2014 /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [resources [name]]",
|
|
9643
|
+
desc: "manage MCP servers \u2014 /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [tools [name]] [resources [name]]",
|
|
9496
9644
|
run: async (a) => {
|
|
9497
9645
|
const sub = a[0]?.toLowerCase();
|
|
9498
9646
|
if (sub === "login") {
|
|
@@ -9568,6 +9716,22 @@ ${extra}` : body);
|
|
|
9568
9716
|
`));
|
|
9569
9717
|
return;
|
|
9570
9718
|
}
|
|
9719
|
+
if (sub === "tools") {
|
|
9720
|
+
const filter = a[1];
|
|
9721
|
+
const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
|
|
9722
|
+
if (!targets.length) {
|
|
9723
|
+
err(dim(` (no ${filter ? `MCP server "${filter}"` : "MCP servers"} found)
|
|
9724
|
+
`));
|
|
9725
|
+
return;
|
|
9726
|
+
}
|
|
9727
|
+
for (const m of targets) {
|
|
9728
|
+
err(` ${cyan(m.name)} ${dim(`(${m.tools.length} tools)`)}
|
|
9729
|
+
`);
|
|
9730
|
+
for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, "")}${t.description ? " \u2014 " + t.description.split("\n")[0] : ""}
|
|
9731
|
+
`));
|
|
9732
|
+
}
|
|
9733
|
+
return;
|
|
9734
|
+
}
|
|
9571
9735
|
if (sub === "resources") {
|
|
9572
9736
|
const filter = a[1];
|
|
9573
9737
|
const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
|
|
@@ -9602,16 +9766,16 @@ ${extra}` : body);
|
|
|
9602
9766
|
}
|
|
9603
9767
|
for (const m of mounted) {
|
|
9604
9768
|
const ver = m.serverInfo?.name ? dim(` \xB7 ${m.serverInfo.name}${m.serverInfo.version ? " v" + m.serverInfo.version : ""}`) : "";
|
|
9605
|
-
err(` ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}
|
|
9769
|
+
err(` ${green("\u2713")} ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}
|
|
9606
9770
|
`);
|
|
9607
|
-
for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, "")}${t.description ? " \u2014 " + t.description.split("\n")[0] : ""}
|
|
9608
|
-
`));
|
|
9609
9771
|
}
|
|
9772
|
+
err(dim(" /mcp tools [name] to list tools \xB7 /mcp resources [name] for resources\n"));
|
|
9610
9773
|
};
|
|
9611
9774
|
const items = [
|
|
9612
9775
|
{ label: "list", value: "list", desc: `show mounted servers (${mounted.length})` },
|
|
9613
9776
|
{ label: "add", value: "add", desc: "mount a new MCP server" },
|
|
9614
9777
|
...mounted.length ? [
|
|
9778
|
+
{ label: "tools", value: "tools", desc: "list a server's tools" },
|
|
9615
9779
|
{ label: "remove", value: "remove", desc: "unmount an MCP server" },
|
|
9616
9780
|
{ label: "resources", value: "resources", desc: "list server resources" }
|
|
9617
9781
|
] : []
|
|
@@ -9640,13 +9804,13 @@ ${extra}` : body);
|
|
|
9640
9804
|
const rv = await selectMenu(process.stderr, { title: "remove server", items: mounted.map((m) => ({ label: m.name, value: m.name })) });
|
|
9641
9805
|
if (!rv) return;
|
|
9642
9806
|
a = ["remove", rv];
|
|
9643
|
-
} else if (picked === "resources") {
|
|
9807
|
+
} else if (picked === "tools" || picked === "resources") {
|
|
9644
9808
|
if (mounted.length === 1) {
|
|
9645
|
-
a = [
|
|
9809
|
+
a = [picked, mounted[0].name];
|
|
9646
9810
|
} else {
|
|
9647
|
-
const rv = await selectMenu(process.stderr, { title:
|
|
9811
|
+
const rv = await selectMenu(process.stderr, { title: `server ${picked}`, items: [{ label: "(all)", value: "" }, ...mounted.map((m) => ({ label: m.name, value: m.name }))] });
|
|
9648
9812
|
if (rv === null) return;
|
|
9649
|
-
a = rv ? [
|
|
9813
|
+
a = rv ? [picked, rv] : [picked];
|
|
9650
9814
|
}
|
|
9651
9815
|
}
|
|
9652
9816
|
return builtins.mcp.run(a);
|
|
@@ -9786,7 +9950,13 @@ ${extra}` : body);
|
|
|
9786
9950
|
aborting = true;
|
|
9787
9951
|
activeTurn.abort();
|
|
9788
9952
|
voiceIO?.interrupt();
|
|
9789
|
-
err(yellow("\n \u238B cancelling\u2026\n"));
|
|
9953
|
+
err(yellow("\n \u238B cancelling\u2026") + dim(" (Ctrl-C again to force-quit)\n"));
|
|
9954
|
+
setTimeout(() => {
|
|
9955
|
+
if (activeTurn) err(red(" \u26A0 still cancelling \u2014 press Ctrl-C to force-quit\n"));
|
|
9956
|
+
}, 4e3).unref?.();
|
|
9957
|
+
} else if (key?.ctrl && k === "c") {
|
|
9958
|
+
err(red("\n \u23FB force-quit\n"));
|
|
9959
|
+
forceQuit();
|
|
9790
9960
|
} else if (k === "escape" && !pendingRewind) {
|
|
9791
9961
|
pendingRewind = true;
|
|
9792
9962
|
err(dim(" \u238B\u238B jumping back to edit\u2026\n"));
|
|
@@ -10084,6 +10254,7 @@ ${extra}` : body);
|
|
|
10084
10254
|
if (forced) {
|
|
10085
10255
|
voiceIO?.stop();
|
|
10086
10256
|
releaseStdin();
|
|
10257
|
+
disposeCursorSessions();
|
|
10087
10258
|
await closeMcp(mounted);
|
|
10088
10259
|
process.exit(130);
|
|
10089
10260
|
}
|
|
@@ -10092,6 +10263,7 @@ ${extra}` : body);
|
|
|
10092
10263
|
}
|
|
10093
10264
|
}
|
|
10094
10265
|
releaseStdin();
|
|
10266
|
+
disposeCursorSessions();
|
|
10095
10267
|
await closeMcp(mounted);
|
|
10096
10268
|
}
|
|
10097
10269
|
function readAllStdin() {
|
|
@@ -10168,8 +10340,8 @@ async function main() {
|
|
|
10168
10340
|
const { ok, res } = await runTurn(agent, store, session, args.task, void 0, cwd);
|
|
10169
10341
|
if (cfg.reflectOnFailure && !ok && res && agent.options.memoryDir) {
|
|
10170
10342
|
const _fsBase = agent.options.fs.getCwd() === "/" ? "" : agent.options.fs.getCwd();
|
|
10171
|
-
const
|
|
10172
|
-
if (
|
|
10343
|
+
const slug2 = await reflectOnRun({ ai, model: agent.options.model, fs: agent.options.fs, dir: primaryMemDir(agent.options.memoryDir, `${_fsBase}/.agent/memory`), result: res });
|
|
10344
|
+
if (slug2) err(dim(` \u270E learned a lesson \u2192 ${slug2}
|
|
10173
10345
|
`));
|
|
10174
10346
|
}
|
|
10175
10347
|
await closeMcp(mounted);
|