agent.libx.js 0.94.7 → 0.94.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -106,7 +106,8 @@ agentx --resume <id> "…" # resume a specific session
106
106
  - **Filesystem + Shell** — by default the CLI has **full real-filesystem access like Claude Code** (root `/` is the machine root, the launch dir is the working dir, absolute host paths and above-cwd reach both work) with a **real `/bin/sh`** (`Shell` tool) so the agent can run git, bun, node, curl, and any installed binary. Secrets (`.env`, `.ssh`, keys, `.git`) stay hidden by the jail; env secrets are scrubbed from the child shell. `--sandbox` instead operates over an in-memory copy of the working dir with a VFS-only `bash` — the real disk is never touched. `--boddb <dir>` runs over a **persistent database workspace** (a bod-db store at `<dir>` — `meta.db` tree + `files/` bytes) that survives across runs while the real disk stays untouched; DB-native by default, or add `--seed` to hydrate it from cwd on the first run. `--no-shell` forces the VFS bash in disk mode. (`/sandbox` shows the active mode.)
107
107
  - **Sessions** — every conversation persists to `./.agent/sessions/<id>.json`; `--continue`/`--resume` (and `/sessions`, `/resume`) pick it back up, *with memory across turns* — a REPL turn sees the previous one. A global symlink index at `~/.agent/sessions/` enables cross-project lookup: `--resume 090715-myproject` resolves from any directory, and `/sessions all` lists every project's sessions in one picker.
108
108
  - **Diffs** — every `Edit`/`Write`/`MultiEdit` renders a colorized `+/-` diff (TTY-gated; plain when piped).
109
- - **Slash commands** — `/help /tools /model /compact /clear /sessions /resume /commands /init`; user-defined `./.agent/commands/<name>.md` are invokable directly as `/<name>` (the same registry the model's `SlashCommand` tool uses).
109
+ - **Slash commands** — `/help /tools /model /compact /memory /clear /sessions /resume /commands /init`; `/compact <focus>` preserves matching lines from the folded span; `/memory` opens the memory index in `$EDITOR`; user-defined `./.agent/commands/<name>.md` are invokable directly as `/<name>` (the same registry the model's `SlashCommand` tool uses).
110
+ - **Live chrome** — the thinking spinner shows elapsed seconds + `esc to interrupt`; the terminal tab title tracks the session topic; a bell rings when a long (>10s) turn finishes in a backgrounded tab; the footer warns at 80%/90% context pressure and auto-trims announce themselves.
110
111
  - **Project instructions** — `./AGENTS.md` (or `CLAUDE.md`) auto-loads into every run; `/init` scaffolds one.
111
112
  - **Any provider** — set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `GROQ_API_KEY`; choose with `-m provider/model`.
112
113
  - **@-file mentions & headless JSON** — reference files inline in a prompt with `@path` (e.g. `explain @src/Agent.ts`); script with `-p --output-format json` to get one machine-readable result object on stdout (activity stays on stderr).
@@ -311,6 +311,7 @@ declare class Agent {
311
311
  options: AgentOptions;
312
312
  transcript: Message[];
313
313
  private ctx;
314
+ private lastTrimNotified;
314
315
  private activeTools;
315
316
  private activeHooks?;
316
317
  private prepared;
@@ -362,8 +363,10 @@ declare class Agent {
362
363
  * Fold the conversation in place (manual `/compact`): keep the system message + the
363
364
  * most-recent window, summarizing the dropped middle (deterministic, no LLM call).
364
365
  * No-op when the transcript already fits. Returns the number of messages removed.
366
+ * `focus` (e.g. from `/compact keep the API details`) preserves matching lines from the
367
+ * dropped span verbatim in the summary, instead of losing them to the generic recap.
365
368
  */
366
- compactNow(maxMessages?: number): number;
369
+ compactNow(maxMessages?: number, focus?: string): number;
367
370
  private runLoop;
368
371
  /**
369
372
  * Drain a streamed chat() response: emit each text delta to the host
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { h as RunResult, R as ReasoningEffort } from './Agent-uWtu_WFY.js';
2
+ import { h as RunResult, R as ReasoningEffort } from './Agent-COa80xYy.js';
3
3
  import { IFilesystem } from '@livx.cc/wcli/core';
4
4
  import { M as Message, c as ContentPart } from './tools-GPWp7oXq.js';
5
5
 
package/dist/cli.js CHANGED
@@ -2769,6 +2769,8 @@ var Agent = class _Agent {
2769
2769
  transcript = [];
2770
2770
  ctx;
2771
2771
  // built in the ctor when `fs` is provided, else lazily in ensureFs()
2772
+ lastTrimNotified = 0;
2773
+ // last auto-trim drop count surfaced via host.notify (dedup)
2772
2774
  activeTools = [];
2773
2775
  activeHooks;
2774
2776
  // composed: user hooks + plan-mode + permissions
@@ -2932,13 +2934,15 @@ var Agent = class _Agent {
2932
2934
  * Fold the conversation in place (manual `/compact`): keep the system message + the
2933
2935
  * most-recent window, summarizing the dropped middle (deterministic, no LLM call).
2934
2936
  * No-op when the transcript already fits. Returns the number of messages removed.
2937
+ * `focus` (e.g. from `/compact keep the API details`) preserves matching lines from the
2938
+ * dropped span verbatim in the summary, instead of losing them to the generic recap.
2935
2939
  */
2936
- compactNow(maxMessages = 12) {
2940
+ compactNow(maxMessages = 12, focus) {
2937
2941
  const max = Math.max(2, maxMessages);
2938
2942
  if (this.transcript.length <= max) return 0;
2939
2943
  void this.activeHooks?.onPreCompact?.(this.transcript);
2940
2944
  const before = this.transcript.length;
2941
- this.transcript = compact(this.transcript, max);
2945
+ this.transcript = compact(this.transcript, max, focus);
2942
2946
  return before - this.transcript.length;
2943
2947
  }
2944
2948
  async runLoop() {
@@ -3168,7 +3172,15 @@ ${out}`;
3168
3172
  if (o.compaction?.maxMessages && m.length > o.compaction.maxMessages) out = compact(m, o.compaction.maxMessages);
3169
3173
  else if (o.maxContextMessages && m.length > o.maxContextMessages) out = dropOldest(m, o.maxContextMessages);
3170
3174
  if (o.keepToolOutputs) out = stubOldToolResults(out ?? m, o.keepToolOutputs);
3171
- if (o.maxContextTokens) out = fitTokenBudget(out ?? m, o.maxContextTokens);
3175
+ if (o.maxContextTokens) {
3176
+ const pre = (out ?? m).length;
3177
+ out = fitTokenBudget(out ?? m, o.maxContextTokens);
3178
+ const dropped = pre - out.length;
3179
+ if (dropped > 0 && dropped !== this.lastTrimNotified) {
3180
+ this.lastTrimNotified = dropped;
3181
+ o.host?.notify?.({ kind: "compaction", message: `context full \u2014 auto-trimmed ${dropped} oldest message(s) to fit ~${Math.round(o.maxContextTokens / 1e3)}k tokens` });
3182
+ }
3183
+ }
3172
3184
  return dropOrphanToolResults(out ?? m);
3173
3185
  }
3174
3186
  };
@@ -3228,16 +3240,16 @@ function fitTokenBudget(messages, maxTokens) {
3228
3240
  log3.warn(`context ~${estimateTokens([...head, ...body])} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
3229
3241
  return [...head, ...body];
3230
3242
  }
3231
- function compact(m, max) {
3243
+ function compact(m, max, focus) {
3232
3244
  const hasSystem = m[0]?.role === "system";
3233
3245
  const head = hasSystem ? [m[0]] : [];
3234
3246
  const tailCount = Math.max(1, max - head.length - 1);
3235
3247
  const tail = m.slice(head.length).slice(-tailCount);
3236
3248
  const dropped = m.slice(head.length, m.length - tail.length);
3237
3249
  if (dropped.length === 0) return [...head, ...tail];
3238
- return [...head, { role: "system", content: summarize(dropped) }, ...tail];
3250
+ return [...head, { role: "system", content: summarize(dropped, focus) }, ...tail];
3239
3251
  }
3240
- function summarize(messages) {
3252
+ function summarize(messages, focus) {
3241
3253
  const tools = /* @__PURE__ */ new Set();
3242
3254
  const files = /* @__PURE__ */ new Set();
3243
3255
  let lastAssistant = "";
@@ -3256,6 +3268,21 @@ function summarize(messages) {
3256
3268
  if (tools.size) lines.push(`tools used: ${[...tools].join(", ")}`);
3257
3269
  if (files.size) lines.push(`files touched: ${[...files].join(", ")}`);
3258
3270
  if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
3271
+ if (focus?.trim()) {
3272
+ const words = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
3273
+ const kept = [];
3274
+ outer: for (const msg of messages) {
3275
+ if (msg.role === "tool") continue;
3276
+ for (const line of contentText(msg.content).split("\n")) {
3277
+ const l = line.toLowerCase();
3278
+ if (line.trim() && words.some((w) => l.includes(w))) {
3279
+ kept.push(line.trim().slice(0, 200));
3280
+ if (kept.length >= 12) break outer;
3281
+ }
3282
+ }
3283
+ }
3284
+ if (kept.length) lines.push(`preserved (re: ${focus.trim()}):`, ...kept.map((l) => ` ${l}`));
3285
+ }
3259
3286
  return lines.join("\n");
3260
3287
  }
3261
3288
  function lastAssistantText(messages) {
@@ -5605,7 +5632,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
5605
5632
  const init = await client.connect();
5606
5633
  const specs = await client.listTools();
5607
5634
  const tools = mcpToolsToAgentTools(specs, (tool, a) => client.callTool(tool, a), `mcp__${name}__`);
5608
- return { name, client, tools, specs, serverInfo: init?.serverInfo };
5635
+ return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
5609
5636
  })(), mountTimeoutMs, name);
5610
5637
  } catch (e) {
5611
5638
  await client.close().catch((err2) => log11.debug(`close after failed mount of "${name}": ${err2}`));
@@ -7386,7 +7413,24 @@ var EditorState = class _EditorState {
7386
7413
  this.menuOpen = r.hits.length > 0 && !(r.hits.length === 1 && r.hits[0] === r.token);
7387
7414
  if (this.sel >= r.hits.length) this.sel = 0;
7388
7415
  }
7416
+ // ── Editor undo (Ctrl-_): snapshot before every mutation, pop to restore ──
7417
+ undoStack = [];
7418
+ snapshot() {
7419
+ const top = this.undoStack[this.undoStack.length - 1];
7420
+ if (top?.buf === this.buf) return;
7421
+ this.undoStack.push({ buf: this.buf, cursor: this.cursor });
7422
+ if (this.undoStack.length > 200) this.undoStack.shift();
7423
+ }
7424
+ undo() {
7425
+ const p = this.undoStack.pop();
7426
+ if (!p) return;
7427
+ this.buf = p.buf;
7428
+ this.cursor = Math.min(p.cursor, p.buf.length);
7429
+ this.histIdx = -1;
7430
+ this.refresh();
7431
+ }
7389
7432
  insert(s) {
7433
+ this.snapshot();
7390
7434
  this.buf = this.buf.slice(0, this.cursor) + s + this.buf.slice(this.cursor);
7391
7435
  this.cursor += s.length;
7392
7436
  this.histIdx = -1;
@@ -7394,6 +7438,7 @@ var EditorState = class _EditorState {
7394
7438
  }
7395
7439
  backspace() {
7396
7440
  if (this.cursor === 0) return;
7441
+ this.snapshot();
7397
7442
  this.buf = this.buf.slice(0, this.cursor - 1) + this.buf.slice(this.cursor);
7398
7443
  this.cursor--;
7399
7444
  this.histIdx = -1;
@@ -7401,6 +7446,7 @@ var EditorState = class _EditorState {
7401
7446
  }
7402
7447
  del() {
7403
7448
  if (this.cursor >= this.buf.length) return;
7449
+ this.snapshot();
7404
7450
  this.buf = this.buf.slice(0, this.cursor) + this.buf.slice(this.cursor + 1);
7405
7451
  this.refresh();
7406
7452
  }
@@ -7437,6 +7483,7 @@ var EditorState = class _EditorState {
7437
7483
  return j;
7438
7484
  }
7439
7485
  cut(from, to) {
7486
+ this.snapshot();
7440
7487
  this.killed = this.buf.slice(from, to);
7441
7488
  this.buf = this.buf.slice(0, from) + this.buf.slice(to);
7442
7489
  this.cursor = from;
@@ -7748,6 +7795,7 @@ function applyKey(s, key, str) {
7748
7795
  if (key?.ctrl && k === "u") return s.killToStart(), "none";
7749
7796
  if (key?.ctrl && k === "k") return s.killToEnd(), "none";
7750
7797
  if (key?.ctrl && k === "y") return s.yank(), "none";
7798
+ if (str === "") return s.undo(), "none";
7751
7799
  if (key?.meta && k === "b") return s.wordLeft(), "none";
7752
7800
  if (key?.meta && k === "f") return s.wordRight(), "none";
7753
7801
  if (key?.meta && k === "d") return s.killWordForward(), "none";
@@ -8408,10 +8456,17 @@ var spinner = /* @__PURE__ */ (() => {
8408
8456
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
8409
8457
  let timer;
8410
8458
  let i = 0;
8459
+ let t0 = 0;
8411
8460
  return {
8461
+ /** Anchor for the elapsed counter; runTurn sets it once per turn so tool-call stop/start cycles don't reset it. */
8462
+ turnStart: 0,
8412
8463
  start(label = "thinking\u2026") {
8413
8464
  if (!tty || timer) return;
8414
- timer = setInterval(() => err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label}`)), 90);
8465
+ t0 = this.turnStart || Date.now();
8466
+ timer = setInterval(() => {
8467
+ const secs = Math.round((Date.now() - t0) / 1e3);
8468
+ err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs ? `${secs}s \xB7 ` : ""}esc to interrupt`));
8469
+ }, 90);
8415
8470
  },
8416
8471
  stop() {
8417
8472
  if (timer) {
@@ -8422,6 +8477,9 @@ var spinner = /* @__PURE__ */ (() => {
8422
8477
  }
8423
8478
  };
8424
8479
  })();
8480
+ var setTermTitle = (t) => {
8481
+ if (tty) err(`\x1B]0;${t.replace(/[\x00-\x1f]/g, " ").slice(0, 80)}\x07`);
8482
+ };
8425
8483
  var activeTurn = null;
8426
8484
  var exitRequested = false;
8427
8485
  var inputStash = [];
@@ -8571,12 +8629,12 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
8571
8629
  Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
8572
8630
 
8573
8631
  REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \xB7 @path inlines a file
8574
- REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit (duplex: /act /think /tasks /voice /voice-model /think-model)
8632
+ REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /memory /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit (duplex: /act /think /tasks /voice /voice-model /think-model)
8575
8633
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
8576
8634
  REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to continue. Esc cancels a running turn / clears the input line; double-Esc jumps back to edit a previous message.
8577
8635
  REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 Ctrl+O toggles verbose tool output \xB7 \u2192 or Tab accepts the dim history ghost-suggestion \xB7 Alt+S/Ctrl+S stash/unstash.
8578
8636
  REPL stash: type while a turn is running \u2192 Enter queues it (auto-submits when the turn finishes). Alt+S (or Ctrl+S) with text stashes it; on an empty prompt pops the next entry for editing.
8579
- REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
8637
+ REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Ctrl-_ undo \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
8580
8638
  REPL paste: large/multi-line pastes collapse to a [Pasted text +N lines] preview (expands on send); a pasted image/file path attaches as [Image]/[File]; /paste grabs a clipboard image (macOS).`;
8581
8639
  function newestModel() {
8582
8640
  return listModels().slice().sort((a, b) => (getModelInfo(b)?.releaseDate ?? "").localeCompare(getModelInfo(a)?.releaseDate ?? ""))[0] ?? "";
@@ -8635,6 +8693,16 @@ function makeHost(format = "text", opts) {
8635
8693
  return {
8636
8694
  flushText,
8637
8695
  notify(e) {
8696
+ if (e.kind === "turn_start") {
8697
+ if (e.message !== "step 1") {
8698
+ spinner.stop();
8699
+ closeReasonLine();
8700
+ err(dim(` \xB7 ${e.message}
8701
+ `));
8702
+ spinner.start();
8703
+ }
8704
+ return;
8705
+ }
8638
8706
  spinner.stop();
8639
8707
  if (e.kind === "text_delta") {
8640
8708
  if (streamJson) process.stdout.write(JSON.stringify({ type: "text", text: e.message }) + "\n");
@@ -9161,6 +9229,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9161
9229
  agent.options.signal = ctrl.signal;
9162
9230
  const content = images.length ? [{ type: "text", text }, ...images] : text;
9163
9231
  let res;
9232
+ spinner.turnStart = t0;
9164
9233
  spinner.start(sendFn ? "voice\u2026" : void 0);
9165
9234
  try {
9166
9235
  res = await (sendFn ? sendFn(content) : agent.send(content));
@@ -9177,6 +9246,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9177
9246
  return { ok: false };
9178
9247
  } finally {
9179
9248
  spinner.stop();
9249
+ spinner.turnStart = 0;
9180
9250
  activeTurn = null;
9181
9251
  agent.options.signal = void 0;
9182
9252
  }
@@ -9193,6 +9263,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9193
9263
  if (!silentAbort)
9194
9264
  err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
9195
9265
  `));
9266
+ if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
9196
9267
  if (res.finishReason === "error" && res.error) {
9197
9268
  const e = res.error;
9198
9269
  err(red(` ${e?.message ?? e}`) + (e?.statusCode ? dim(` (${e.statusCode}${e.code ? " " + e.code : ""})`) : "") + "\n");
@@ -9208,7 +9279,10 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9208
9279
  const ev = { ts: t0, durationMs: Date.now() - t0, model: agent.options.model, finishReason: res.finishReason, steps: res.steps, tools, tokens: res.usage?.totalTokens, costUsd: cost, estimated: res.usageEstimated };
9209
9280
  if (res.finishReason === "error" && res.error) ev.error = errInfo(res.error);
9210
9281
  (session.meta.events ??= []).push(ev);
9211
- if (!session.meta.title) session.meta.title = titleOf(agent.transcript);
9282
+ if (!session.meta.title) {
9283
+ session.meta.title = titleOf(agent.transcript);
9284
+ if (session.meta.title) setTermTitle(`agentx \xB7 ${session.meta.title}`);
9285
+ }
9212
9286
  try {
9213
9287
  store.save(session);
9214
9288
  } catch (e) {
@@ -9651,6 +9725,7 @@ async function repl(args, ai, cfg, cwd) {
9651
9725
  installCancelGuards(mounted);
9652
9726
  const store = new SessionStore(cwd);
9653
9727
  let session = startSession(args, store, face, cwd);
9728
+ setTermTitle(`agentx \xB7 ${session.meta.title || cwd.split("/").pop() || "session"}`);
9654
9729
  if (session.meta.scheduledJobs?.length) {
9655
9730
  scheduler.restore(session.meta.scheduledJobs);
9656
9731
  err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
@@ -10210,19 +10285,60 @@ ${extra}` : body);
10210
10285
  store.save(session);
10211
10286
  } catch {
10212
10287
  }
10288
+ setTermTitle(`agentx \xB7 ${t}`);
10213
10289
  err(dim(" renamed \u2192 " + t + "\n"));
10214
10290
  }
10215
10291
  },
10216
10292
  compact: {
10217
- desc: "summarize older context to free up the window",
10218
- run: () => {
10219
- const n = face.compactNow();
10293
+ desc: "summarize older context to free up the window \u2014 /compact [what to preserve]",
10294
+ run: (a) => {
10295
+ const focus = a.join(" ").trim() || void 0;
10296
+ const n = face.compactNow(12, focus);
10220
10297
  session.messages = face.transcript;
10221
10298
  try {
10222
10299
  store.save(session);
10223
10300
  } catch {
10224
10301
  }
10225
- err(dim(` compacted \u2014 folded ${n} message(s)
10302
+ err(dim(` compacted \u2014 folded ${n} message(s)${focus && n ? ` (preserving: ${focus})` : ""}
10303
+ `));
10304
+ }
10305
+ },
10306
+ memory: {
10307
+ desc: "open the memory index in $EDITOR (.agent/memory/MEMORY.md)",
10308
+ run: async () => {
10309
+ const dir = primaryMemDir(face.options.memoryDir, adot("memory"));
10310
+ const idx = `${dir}/MEMORY.md`;
10311
+ const fs2 = face.options.fs;
10312
+ if (args.vfs || args.boddb) {
10313
+ try {
10314
+ err(dim(await fs2.readFile(idx)) + "\n");
10315
+ } catch {
10316
+ err(dim(" (no memory yet \u2014 save one with `#<note>`)\n"));
10317
+ }
10318
+ return;
10319
+ }
10320
+ try {
10321
+ await fs2.readFile(idx);
10322
+ } catch {
10323
+ try {
10324
+ await mkdirp(fs2, dir);
10325
+ await fs2.writeFile(idx, "# Memory\n\n");
10326
+ } catch (e) {
10327
+ err(red(` can't create ${idx}: ${e?.message ?? e}
10328
+ `));
10329
+ return;
10330
+ }
10331
+ }
10332
+ const ed = process.env.VISUAL || process.env.EDITOR || "vi";
10333
+ const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
10334
+ if (wasRaw) process.stdin.setRawMode(false);
10335
+ try {
10336
+ const { spawnSync: spawnSync3 } = await import("child_process");
10337
+ spawnSync3(ed, [idx], { stdio: "inherit" });
10338
+ } finally {
10339
+ if (wasRaw) process.stdin.setRawMode(true);
10340
+ }
10341
+ err(dim(` \u270E ${idx}
10226
10342
  `));
10227
10343
  }
10228
10344
  },
@@ -10289,7 +10405,7 @@ ${extra}` : body);
10289
10405
  commands: { desc: "pick a custom slash command to run (./.agent/commands)", run: () => pickAndRun("command") },
10290
10406
  skills: { desc: "pick a skill to run (./.agent/skills)", run: () => pickAndRun("skill") },
10291
10407
  mcp: {
10292
- desc: "manage MCP servers \u2014 /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [tools [name]] [resources [name]]",
10408
+ desc: "manage MCP servers \u2014 /mcp [add <name> <cmd|url>] [login <name>] [reconnect <name>] [remove <name>] [tools [name]] [resources [name]]",
10293
10409
  run: async (a) => {
10294
10410
  const sub = a[0]?.toLowerCase();
10295
10411
  if (sub === "login") {
@@ -10342,6 +10458,36 @@ ${extra}` : body);
10342
10458
  `));
10343
10459
  } catch (e) {
10344
10460
  err(red(` failed to mount "${name}": ${e?.message ?? e}
10461
+ `));
10462
+ }
10463
+ return;
10464
+ }
10465
+ if (sub === "reconnect") {
10466
+ const name = a[1];
10467
+ if (!name) {
10468
+ err(yellow(" usage: /mcp reconnect <name>\n"));
10469
+ return;
10470
+ }
10471
+ const idx = mounted.findIndex((m) => m.name === name);
10472
+ const conf = idx >= 0 ? mounted[idx].config : cfg.mcpServers?.[name];
10473
+ if (!conf) {
10474
+ err(yellow(` MCP "${name}" not found (not mounted and not in config)
10475
+ `));
10476
+ return;
10477
+ }
10478
+ if (idx >= 0) {
10479
+ const old = mounted.splice(idx, 1)[0];
10480
+ removeWorkTools(old.tools.map((t) => t.name));
10481
+ await old.client.close().catch((e) => log17.debug("mcp close failed", e));
10482
+ }
10483
+ try {
10484
+ const m = await mountMcpServer(name, conf);
10485
+ mounted.push(m);
10486
+ addWorkTools(m.tools);
10487
+ err(green(` \u2713 reconnected "${name}"`) + dim(` \u2014 ${m.tools.length} tool(s)
10488
+ `));
10489
+ } catch (e) {
10490
+ err(red(` reconnect failed: ${e?.message ?? e}
10345
10491
  `));
10346
10492
  }
10347
10493
  return;
@@ -10425,6 +10571,7 @@ ${extra}` : body);
10425
10571
  { label: "add", value: "add", desc: "mount a new MCP server" },
10426
10572
  ...mounted.length ? [
10427
10573
  { label: "tools", value: "tools", desc: "list a server's tools" },
10574
+ { label: "reconnect", value: "reconnect", desc: "remount a server (hung/restarted)" },
10428
10575
  { label: "remove", value: "remove", desc: "unmount an MCP server" },
10429
10576
  { label: "resources", value: "resources", desc: "list server resources" }
10430
10577
  ] : []
@@ -10449,10 +10596,10 @@ ${extra}` : body);
10449
10596
  } finally {
10450
10597
  io.close();
10451
10598
  }
10452
- } else if (picked === "remove") {
10453
- const rv = await selectMenu(process.stderr, { title: "remove server", items: mounted.map((m) => ({ label: m.name, value: m.name })) });
10599
+ } else if (picked === "remove" || picked === "reconnect") {
10600
+ const rv = await selectMenu(process.stderr, { title: `${picked} server`, items: mounted.map((m) => ({ label: m.name, value: m.name })) });
10454
10601
  if (!rv) return;
10455
- a = ["remove", rv];
10602
+ a = [picked, rv];
10456
10603
  } else if (picked === "tools" || picked === "resources") {
10457
10604
  if (mounted.length === 1) {
10458
10605
  a = [picked, mounted[0].name];
@@ -10797,6 +10944,7 @@ ${extra}` : body);
10797
10944
  editorRef?.redrawNow();
10798
10945
  };
10799
10946
  if (args.voice && duplex && process.stdin.isTTY) await startVoice(true);
10947
+ let ctxWarned = 0;
10800
10948
  while (true) {
10801
10949
  if (pendingRewind) {
10802
10950
  pendingRewind = false;
@@ -10811,6 +10959,14 @@ ${extra}` : body);
10811
10959
  prefill = void 0;
10812
10960
  const ctxTok = estimateTranscriptTokens(face.transcript);
10813
10961
  const ctxCap = face.options.maxTokens || 2e5;
10962
+ {
10963
+ const pct = Math.round(ctxTok / ctxCap * 100);
10964
+ const step = pct >= 90 ? 90 : pct >= 80 ? 80 : 0;
10965
+ if (step > ctxWarned) {
10966
+ ctxWarned = step;
10967
+ err(yellow(` \u26A0 context ${pct}% full`) + dim(" \u2014 /compact folds older messages (oldest are auto-trimmed at the cap)\n"));
10968
+ } else if (!step) ctxWarned = 0;
10969
+ }
10814
10970
  const usd = session.meta.costUsd ?? 0;
10815
10971
  const computeFooter = () => {
10816
10972
  const parts = [];