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/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
@@ -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
- voiceIO?.stop();
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 = ["resources", mounted[0].name];
9809
+ a = [picked, mounted[0].name];
9646
9810
  } else {
9647
- 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 }))] });
9648
9812
  if (rv === null) return;
9649
- a = rv ? ["resources", rv] : ["resources"];
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 slug = await reflectOnRun({ ai, model: agent.options.model, fs: agent.options.fs, dir: primaryMemDir(agent.options.memoryDir, `${_fsBase}/.agent/memory`), result: res });
10172
- 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}
10173
10345
  `));
10174
10346
  }
10175
10347
  await closeMcp(mounted);