agent.libx.js 0.93.28 → 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/dist/cli.js CHANGED
@@ -26,7 +26,10 @@ var init_redact = __esm({
26
26
  });
27
27
 
28
28
  // src/tools.structured.ts
29
- async function walkFiles(fs, dir, out = []) {
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 slug = String(name ?? "").replace(/^\//, "");
2006
- const c = commands.find((x) => x.name === slug);
2007
- if (!c) return `Error: no command named '${slug}'. Available: ${commands.map((x) => x.name).join(", ")}`;
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 slug = l.match(/\]\(([^)]+)\.md\)/)?.[1];
2043
- if (slug && !seenSlugs.has(slug)) {
2044
- seenSlugs.add(slug);
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, slug, body, opts) {
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}/${slug}.md`, content.endsWith("\n") ? content : content + "\n");
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 = `- [${slug}](${slug}.md) \u2014 ${summary}`;
2077
+ const line = `- [${slug2}](${slug2}.md) \u2014 ${summary}`;
2072
2078
  const lines = idx.split("\n");
2073
- const at = lines.findIndex((l) => l.includes(`(${slug}.md)`));
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, slug) {
2125
- const path = `${dir}/${slug}.md`;
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, slug) {
2140
+ async function loadFactMulti(fs, dirs, slug2) {
2135
2141
  for (const d of dirs) {
2136
- const r = await loadFact(fs, d, slug);
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 (slug) targets = [cleanSlug(slug)];
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 slug of slugs) {
2216
- const body = await loadFactMulti(fs, dirs, slug);
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(slug || body.split("\n")[0]);
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 slug = ("lesson-" + slugify(lesson)).slice(0, 56);
3634
+ const slug2 = ("lesson-" + slugify(lesson)).slice(0, 56);
3535
3635
  try {
3536
- await writeFact(o.fs, o.dir, slug, lesson);
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 ${slug}`);
3542
- return slug;
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
- ...isCursor ? { providerOptions: { cwd, ...toCursorMcp(o.mcpServers) ?? {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } } } : {},
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 extra = [o.appendSystemPrompt, envNote, cwdNote, shellNote, toneNote, mountNote].filter(Boolean).join("\n\n");
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
- const base = toolsByName([...o.tools ?? DEFAULT_TOOLS, ...autoWebTools()]);
5604
- if (!realShell.length) return [...base, ...o.extraTools ?? []];
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, ...o.extraTools ?? []];
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
@@ -7993,14 +8124,26 @@ function makeHost(format = "text", opts) {
7993
8124
  const flushText = () => {
7994
8125
  if (md && md.pending()) process.stdout.write(md.flush());
7995
8126
  };
8127
+ let openReasonLine = false;
8128
+ const closeReasonLine = () => {
8129
+ if (openReasonLine) {
8130
+ process.stderr.write("\n");
8131
+ openReasonLine = false;
8132
+ }
8133
+ };
7996
8134
  return {
7997
8135
  flushText,
7998
8136
  notify(e) {
7999
8137
  spinner.stop();
8000
8138
  if (e.kind === "text_delta") {
8001
8139
  if (streamJson) process.stdout.write(JSON.stringify({ type: "text", text: e.message }) + "\n");
8002
- else if (md) process.stdout.write(md.feed(e.message));
8003
- else (cleanStdout ? process.stderr : process.stdout).write(e.message);
8140
+ else if (md) {
8141
+ closeReasonLine();
8142
+ process.stdout.write(md.feed(e.message));
8143
+ } else {
8144
+ if (!cleanStdout) closeReasonLine();
8145
+ (cleanStdout ? process.stderr : process.stdout).write(e.message);
8146
+ }
8004
8147
  return;
8005
8148
  }
8006
8149
  if (e.kind === "thinking_delta") {
@@ -8008,12 +8151,14 @@ function makeHost(format = "text", opts) {
8008
8151
  else if (!cleanStdout) {
8009
8152
  if (md && md.pending()) process.stdout.write(md.flush() + "\n");
8010
8153
  process.stderr.write(dim(e.message));
8154
+ openReasonLine = true;
8011
8155
  }
8012
8156
  return;
8013
8157
  }
8158
+ if (!("message" in e)) return;
8159
+ closeReasonLine();
8014
8160
  if (md && md.pending()) process.stdout.write(md.flush() + "\n");
8015
- const notice = "message" in e ? e.message : e.kind;
8016
- err(dim(` \xB7 ${notice}
8161
+ err(dim(` \xB7 ${e.message}
8017
8162
  `));
8018
8163
  },
8019
8164
  async confirm(prompt) {
@@ -8342,6 +8487,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
8342
8487
  seed: args.seed,
8343
8488
  realShell: args.shell,
8344
8489
  // undefined → core.ts defaults (on for disk, off for sandbox/boddb)
8490
+ scratch: args.scratch,
8345
8491
  appendSystemPrompt: args.appendSystemPrompt,
8346
8492
  addDirs: args.addDirs,
8347
8493
  stream: args.stream,
@@ -8705,7 +8851,7 @@ async function repl(args, ai, cfg, cwd) {
8705
8851
  return { decision: "deny" };
8706
8852
  };
8707
8853
  if (duplex) {
8708
- const { host: _host, stream: _stream, signal: _signal, ...wo } = agent.options;
8854
+ const { host: _host, stream: _stream, signal: _signal, providerOptions: _po, ...wo } = agent.options;
8709
8855
  workerOptions = wo;
8710
8856
  if (workerOptions.permissions)
8711
8857
  workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: void 0, ask: duplexAsk });
@@ -8785,6 +8931,7 @@ async function repl(args, ai, cfg, cwd) {
8785
8931
  ...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
8786
8932
  actModel: agent.options.model,
8787
8933
  actOptions: workerOptions,
8934
+ providerOptionsFor: (m) => cursorProviderOptions(m, cwd, cfg.mcpServers),
8788
8935
  ...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
8789
8936
  host,
8790
8937
  ...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true } : {},
@@ -8887,15 +9034,30 @@ async function repl(args, ai, cfg, cwd) {
8887
9034
  const img = grabClipboardImage(dir, String(Date.now()));
8888
9035
  return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
8889
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
+ };
8890
9049
  process.on("SIGINT", () => {
8891
9050
  if (activeTurn) {
9051
+ if (aborting) {
9052
+ err(red("\n \u23FB force-quit\n"));
9053
+ forceQuit();
9054
+ return;
9055
+ }
8892
9056
  activeTurn.abort();
8893
9057
  voiceIO?.interrupt();
8894
9058
  return;
8895
9059
  }
8896
- voiceIO?.stop();
8897
- void closeMcp(mounted);
8898
- process.exit(130);
9060
+ forceQuit();
8899
9061
  });
8900
9062
  installCancelGuards(mounted);
8901
9063
  const store = new SessionStore(cwd);
@@ -9478,7 +9640,7 @@ ${extra}` : body);
9478
9640
  commands: { desc: "pick a custom slash command to run (./.agent/commands)", run: () => pickAndRun("command") },
9479
9641
  skills: { desc: "pick a skill to run (./.agent/skills)", run: () => pickAndRun("skill") },
9480
9642
  mcp: {
9481
- 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]]",
9482
9644
  run: async (a) => {
9483
9645
  const sub = a[0]?.toLowerCase();
9484
9646
  if (sub === "login") {
@@ -9554,6 +9716,22 @@ ${extra}` : body);
9554
9716
  `));
9555
9717
  return;
9556
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
+ }
9557
9735
  if (sub === "resources") {
9558
9736
  const filter = a[1];
9559
9737
  const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
@@ -9588,16 +9766,16 @@ ${extra}` : body);
9588
9766
  }
9589
9767
  for (const m of mounted) {
9590
9768
  const ver = m.serverInfo?.name ? dim(` \xB7 ${m.serverInfo.name}${m.serverInfo.version ? " v" + m.serverInfo.version : ""}`) : "";
9591
- err(` ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}
9769
+ err(` ${green("\u2713")} ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}
9592
9770
  `);
9593
- for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, "")}${t.description ? " \u2014 " + t.description.split("\n")[0] : ""}
9594
- `));
9595
9771
  }
9772
+ err(dim(" /mcp tools [name] to list tools \xB7 /mcp resources [name] for resources\n"));
9596
9773
  };
9597
9774
  const items = [
9598
9775
  { label: "list", value: "list", desc: `show mounted servers (${mounted.length})` },
9599
9776
  { label: "add", value: "add", desc: "mount a new MCP server" },
9600
9777
  ...mounted.length ? [
9778
+ { label: "tools", value: "tools", desc: "list a server's tools" },
9601
9779
  { label: "remove", value: "remove", desc: "unmount an MCP server" },
9602
9780
  { label: "resources", value: "resources", desc: "list server resources" }
9603
9781
  ] : []
@@ -9626,13 +9804,13 @@ ${extra}` : body);
9626
9804
  const rv = await selectMenu(process.stderr, { title: "remove server", items: mounted.map((m) => ({ label: m.name, value: m.name })) });
9627
9805
  if (!rv) return;
9628
9806
  a = ["remove", rv];
9629
- } else if (picked === "resources") {
9807
+ } else if (picked === "tools" || picked === "resources") {
9630
9808
  if (mounted.length === 1) {
9631
- a = ["resources", mounted[0].name];
9809
+ a = [picked, mounted[0].name];
9632
9810
  } else {
9633
- const rv = await selectMenu(process.stderr, { title: "server resources", items: [{ label: "(all)", value: "" }, ...mounted.map((m) => ({ label: m.name, value: m.name }))] });
9811
+ const rv = await selectMenu(process.stderr, { title: `server ${picked}`, items: [{ label: "(all)", value: "" }, ...mounted.map((m) => ({ label: m.name, value: m.name }))] });
9634
9812
  if (rv === null) return;
9635
- a = rv ? ["resources", rv] : ["resources"];
9813
+ a = rv ? [picked, rv] : [picked];
9636
9814
  }
9637
9815
  }
9638
9816
  return builtins.mcp.run(a);
@@ -9772,7 +9950,13 @@ ${extra}` : body);
9772
9950
  aborting = true;
9773
9951
  activeTurn.abort();
9774
9952
  voiceIO?.interrupt();
9775
- 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();
9776
9960
  } else if (k === "escape" && !pendingRewind) {
9777
9961
  pendingRewind = true;
9778
9962
  err(dim(" \u238B\u238B jumping back to edit\u2026\n"));
@@ -10070,6 +10254,7 @@ ${extra}` : body);
10070
10254
  if (forced) {
10071
10255
  voiceIO?.stop();
10072
10256
  releaseStdin();
10257
+ disposeCursorSessions();
10073
10258
  await closeMcp(mounted);
10074
10259
  process.exit(130);
10075
10260
  }
@@ -10078,6 +10263,7 @@ ${extra}` : body);
10078
10263
  }
10079
10264
  }
10080
10265
  releaseStdin();
10266
+ disposeCursorSessions();
10081
10267
  await closeMcp(mounted);
10082
10268
  }
10083
10269
  function readAllStdin() {
@@ -10154,8 +10340,8 @@ async function main() {
10154
10340
  const { ok, res } = await runTurn(agent, store, session, args.task, void 0, cwd);
10155
10341
  if (cfg.reflectOnFailure && !ok && res && agent.options.memoryDir) {
10156
10342
  const _fsBase = agent.options.fs.getCwd() === "/" ? "" : agent.options.fs.getCwd();
10157
- const slug = await reflectOnRun({ ai, model: agent.options.model, fs: agent.options.fs, dir: primaryMemDir(agent.options.memoryDir, `${_fsBase}/.agent/memory`), result: res });
10158
- if (slug) err(dim(` \u270E learned a lesson \u2192 ${slug}
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}
10159
10345
  `));
10160
10346
  }
10161
10347
  await closeMcp(mounted);