agent.libx.js 0.94.7 → 0.94.9

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}`));
@@ -7383,10 +7410,28 @@ var EditorState = class _EditorState {
7383
7410
  this.hits = r.hits;
7384
7411
  this.token = r.token;
7385
7412
  this.describe = r.describe;
7386
- this.menuOpen = r.hits.length > 0 && !(r.hits.length === 1 && r.hits[0] === r.token);
7413
+ const exact = r.hits.length === 1 && r.hits[0] === r.token;
7414
+ this.menuOpen = r.hits.length > 0 && (!exact || r.token.startsWith("/"));
7387
7415
  if (this.sel >= r.hits.length) this.sel = 0;
7388
7416
  }
7417
+ // ── Editor undo (Ctrl-_): snapshot before every mutation, pop to restore ──
7418
+ undoStack = [];
7419
+ snapshot() {
7420
+ const top = this.undoStack[this.undoStack.length - 1];
7421
+ if (top?.buf === this.buf) return;
7422
+ this.undoStack.push({ buf: this.buf, cursor: this.cursor });
7423
+ if (this.undoStack.length > 200) this.undoStack.shift();
7424
+ }
7425
+ undo() {
7426
+ const p = this.undoStack.pop();
7427
+ if (!p) return;
7428
+ this.buf = p.buf;
7429
+ this.cursor = Math.min(p.cursor, p.buf.length);
7430
+ this.histIdx = -1;
7431
+ this.refresh();
7432
+ }
7389
7433
  insert(s) {
7434
+ this.snapshot();
7390
7435
  this.buf = this.buf.slice(0, this.cursor) + s + this.buf.slice(this.cursor);
7391
7436
  this.cursor += s.length;
7392
7437
  this.histIdx = -1;
@@ -7394,6 +7439,7 @@ var EditorState = class _EditorState {
7394
7439
  }
7395
7440
  backspace() {
7396
7441
  if (this.cursor === 0) return;
7442
+ this.snapshot();
7397
7443
  this.buf = this.buf.slice(0, this.cursor - 1) + this.buf.slice(this.cursor);
7398
7444
  this.cursor--;
7399
7445
  this.histIdx = -1;
@@ -7401,6 +7447,7 @@ var EditorState = class _EditorState {
7401
7447
  }
7402
7448
  del() {
7403
7449
  if (this.cursor >= this.buf.length) return;
7450
+ this.snapshot();
7404
7451
  this.buf = this.buf.slice(0, this.cursor) + this.buf.slice(this.cursor + 1);
7405
7452
  this.refresh();
7406
7453
  }
@@ -7437,6 +7484,7 @@ var EditorState = class _EditorState {
7437
7484
  return j;
7438
7485
  }
7439
7486
  cut(from, to) {
7487
+ this.snapshot();
7440
7488
  this.killed = this.buf.slice(from, to);
7441
7489
  this.buf = this.buf.slice(0, from) + this.buf.slice(to);
7442
7490
  this.cursor = from;
@@ -7748,6 +7796,7 @@ function applyKey(s, key, str) {
7748
7796
  if (key?.ctrl && k === "u") return s.killToStart(), "none";
7749
7797
  if (key?.ctrl && k === "k") return s.killToEnd(), "none";
7750
7798
  if (key?.ctrl && k === "y") return s.yank(), "none";
7799
+ if (str === "") return s.undo(), "none";
7751
7800
  if (key?.meta && k === "b") return s.wordLeft(), "none";
7752
7801
  if (key?.meta && k === "f") return s.wordRight(), "none";
7753
7802
  if (key?.meta && k === "d") return s.killWordForward(), "none";
@@ -8408,10 +8457,17 @@ var spinner = /* @__PURE__ */ (() => {
8408
8457
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
8409
8458
  let timer;
8410
8459
  let i = 0;
8460
+ let t0 = 0;
8411
8461
  return {
8462
+ /** Anchor for the elapsed counter; runTurn sets it once per turn so tool-call stop/start cycles don't reset it. */
8463
+ turnStart: 0,
8412
8464
  start(label = "thinking\u2026") {
8413
8465
  if (!tty || timer) return;
8414
- timer = setInterval(() => err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label}`)), 90);
8466
+ t0 = this.turnStart || Date.now();
8467
+ timer = setInterval(() => {
8468
+ const secs = Math.round((Date.now() - t0) / 1e3);
8469
+ err("\r\x1B[2K" + dim(` ${frames[i = (i + 1) % frames.length]} ${label} ${secs ? `${secs}s \xB7 ` : ""}esc to interrupt`));
8470
+ }, 90);
8415
8471
  },
8416
8472
  stop() {
8417
8473
  if (timer) {
@@ -8422,6 +8478,9 @@ var spinner = /* @__PURE__ */ (() => {
8422
8478
  }
8423
8479
  };
8424
8480
  })();
8481
+ var setTermTitle = (t) => {
8482
+ if (tty) err(`\x1B]0;${t.replace(/[\x00-\x1f]/g, " ").slice(0, 80)}\x07`);
8483
+ };
8425
8484
  var activeTurn = null;
8426
8485
  var exitRequested = false;
8427
8486
  var inputStash = [];
@@ -8571,12 +8630,12 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
8571
8630
  Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
8572
8631
 
8573
8632
  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)
8633
+ 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
8634
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
8576
8635
  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
8636
  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
8637
  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.
8638
+ 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
8639
  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
8640
  function newestModel() {
8582
8641
  return listModels().slice().sort((a, b) => (getModelInfo(b)?.releaseDate ?? "").localeCompare(getModelInfo(a)?.releaseDate ?? ""))[0] ?? "";
@@ -8635,6 +8694,16 @@ function makeHost(format = "text", opts) {
8635
8694
  return {
8636
8695
  flushText,
8637
8696
  notify(e) {
8697
+ if (e.kind === "turn_start") {
8698
+ if (e.message !== "step 1") {
8699
+ spinner.stop();
8700
+ closeReasonLine();
8701
+ err(dim(` \xB7 ${e.message}
8702
+ `));
8703
+ spinner.start();
8704
+ }
8705
+ return;
8706
+ }
8638
8707
  spinner.stop();
8639
8708
  if (e.kind === "text_delta") {
8640
8709
  if (streamJson) process.stdout.write(JSON.stringify({ type: "text", text: e.message }) + "\n");
@@ -9161,6 +9230,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9161
9230
  agent.options.signal = ctrl.signal;
9162
9231
  const content = images.length ? [{ type: "text", text }, ...images] : text;
9163
9232
  let res;
9233
+ spinner.turnStart = t0;
9164
9234
  spinner.start(sendFn ? "voice\u2026" : void 0);
9165
9235
  try {
9166
9236
  res = await (sendFn ? sendFn(content) : agent.send(content));
@@ -9177,6 +9247,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9177
9247
  return { ok: false };
9178
9248
  } finally {
9179
9249
  spinner.stop();
9250
+ spinner.turnStart = 0;
9180
9251
  activeTurn = null;
9181
9252
  agent.options.signal = void 0;
9182
9253
  }
@@ -9193,6 +9264,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9193
9264
  if (!silentAbort)
9194
9265
  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
9266
  `));
9267
+ if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
9196
9268
  if (res.finishReason === "error" && res.error) {
9197
9269
  const e = res.error;
9198
9270
  err(red(` ${e?.message ?? e}`) + (e?.statusCode ? dim(` (${e.statusCode}${e.code ? " " + e.code : ""})`) : "") + "\n");
@@ -9208,7 +9280,10 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9208
9280
  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
9281
  if (res.finishReason === "error" && res.error) ev.error = errInfo(res.error);
9210
9282
  (session.meta.events ??= []).push(ev);
9211
- if (!session.meta.title) session.meta.title = titleOf(agent.transcript);
9283
+ if (!session.meta.title) {
9284
+ session.meta.title = titleOf(agent.transcript);
9285
+ if (session.meta.title) setTermTitle(`agentx \xB7 ${session.meta.title}`);
9286
+ }
9212
9287
  try {
9213
9288
  store.save(session);
9214
9289
  } catch (e) {
@@ -9651,6 +9726,7 @@ async function repl(args, ai, cfg, cwd) {
9651
9726
  installCancelGuards(mounted);
9652
9727
  const store = new SessionStore(cwd);
9653
9728
  let session = startSession(args, store, face, cwd);
9729
+ setTermTitle(`agentx \xB7 ${session.meta.title || cwd.split("/").pop() || "session"}`);
9654
9730
  if (session.meta.scheduledJobs?.length) {
9655
9731
  scheduler.restore(session.meta.scheduledJobs);
9656
9732
  err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
@@ -10210,19 +10286,60 @@ ${extra}` : body);
10210
10286
  store.save(session);
10211
10287
  } catch {
10212
10288
  }
10289
+ setTermTitle(`agentx \xB7 ${t}`);
10213
10290
  err(dim(" renamed \u2192 " + t + "\n"));
10214
10291
  }
10215
10292
  },
10216
10293
  compact: {
10217
- desc: "summarize older context to free up the window",
10218
- run: () => {
10219
- const n = face.compactNow();
10294
+ desc: "summarize older context to free up the window \u2014 /compact [what to preserve]",
10295
+ run: (a) => {
10296
+ const focus = a.join(" ").trim() || void 0;
10297
+ const n = face.compactNow(12, focus);
10220
10298
  session.messages = face.transcript;
10221
10299
  try {
10222
10300
  store.save(session);
10223
10301
  } catch {
10224
10302
  }
10225
- err(dim(` compacted \u2014 folded ${n} message(s)
10303
+ err(dim(` compacted \u2014 folded ${n} message(s)${focus && n ? ` (preserving: ${focus})` : ""}
10304
+ `));
10305
+ }
10306
+ },
10307
+ memory: {
10308
+ desc: "open the memory index in $EDITOR (.agent/memory/MEMORY.md)",
10309
+ run: async () => {
10310
+ const dir = primaryMemDir(face.options.memoryDir, adot("memory"));
10311
+ const idx = `${dir}/MEMORY.md`;
10312
+ const fs2 = face.options.fs;
10313
+ if (args.vfs || args.boddb) {
10314
+ try {
10315
+ err(dim(await fs2.readFile(idx)) + "\n");
10316
+ } catch {
10317
+ err(dim(" (no memory yet \u2014 save one with `#<note>`)\n"));
10318
+ }
10319
+ return;
10320
+ }
10321
+ try {
10322
+ await fs2.readFile(idx);
10323
+ } catch {
10324
+ try {
10325
+ await mkdirp(fs2, dir);
10326
+ await fs2.writeFile(idx, "# Memory\n\n");
10327
+ } catch (e) {
10328
+ err(red(` can't create ${idx}: ${e?.message ?? e}
10329
+ `));
10330
+ return;
10331
+ }
10332
+ }
10333
+ const ed = process.env.VISUAL || process.env.EDITOR || "vi";
10334
+ const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
10335
+ if (wasRaw) process.stdin.setRawMode(false);
10336
+ try {
10337
+ const { spawnSync: spawnSync3 } = await import("child_process");
10338
+ spawnSync3(ed, [idx], { stdio: "inherit" });
10339
+ } finally {
10340
+ if (wasRaw) process.stdin.setRawMode(true);
10341
+ }
10342
+ err(dim(` \u270E ${idx}
10226
10343
  `));
10227
10344
  }
10228
10345
  },
@@ -10289,7 +10406,7 @@ ${extra}` : body);
10289
10406
  commands: { desc: "pick a custom slash command to run (./.agent/commands)", run: () => pickAndRun("command") },
10290
10407
  skills: { desc: "pick a skill to run (./.agent/skills)", run: () => pickAndRun("skill") },
10291
10408
  mcp: {
10292
- desc: "manage MCP servers \u2014 /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [tools [name]] [resources [name]]",
10409
+ desc: "manage MCP servers \u2014 /mcp [add <name> <cmd|url>] [login <name>] [reconnect <name>] [remove <name>] [tools [name]] [resources [name]]",
10293
10410
  run: async (a) => {
10294
10411
  const sub = a[0]?.toLowerCase();
10295
10412
  if (sub === "login") {
@@ -10342,6 +10459,36 @@ ${extra}` : body);
10342
10459
  `));
10343
10460
  } catch (e) {
10344
10461
  err(red(` failed to mount "${name}": ${e?.message ?? e}
10462
+ `));
10463
+ }
10464
+ return;
10465
+ }
10466
+ if (sub === "reconnect") {
10467
+ const name = a[1];
10468
+ if (!name) {
10469
+ err(yellow(" usage: /mcp reconnect <name>\n"));
10470
+ return;
10471
+ }
10472
+ const idx = mounted.findIndex((m) => m.name === name);
10473
+ const conf = idx >= 0 ? mounted[idx].config : cfg.mcpServers?.[name];
10474
+ if (!conf) {
10475
+ err(yellow(` MCP "${name}" not found (not mounted and not in config)
10476
+ `));
10477
+ return;
10478
+ }
10479
+ if (idx >= 0) {
10480
+ const old = mounted.splice(idx, 1)[0];
10481
+ removeWorkTools(old.tools.map((t) => t.name));
10482
+ await old.client.close().catch((e) => log17.debug("mcp close failed", e));
10483
+ }
10484
+ try {
10485
+ const m = await mountMcpServer(name, conf);
10486
+ mounted.push(m);
10487
+ addWorkTools(m.tools);
10488
+ err(green(` \u2713 reconnected "${name}"`) + dim(` \u2014 ${m.tools.length} tool(s)
10489
+ `));
10490
+ } catch (e) {
10491
+ err(red(` reconnect failed: ${e?.message ?? e}
10345
10492
  `));
10346
10493
  }
10347
10494
  return;
@@ -10425,6 +10572,7 @@ ${extra}` : body);
10425
10572
  { label: "add", value: "add", desc: "mount a new MCP server" },
10426
10573
  ...mounted.length ? [
10427
10574
  { label: "tools", value: "tools", desc: "list a server's tools" },
10575
+ { label: "reconnect", value: "reconnect", desc: "remount a server (hung/restarted)" },
10428
10576
  { label: "remove", value: "remove", desc: "unmount an MCP server" },
10429
10577
  { label: "resources", value: "resources", desc: "list server resources" }
10430
10578
  ] : []
@@ -10449,10 +10597,10 @@ ${extra}` : body);
10449
10597
  } finally {
10450
10598
  io.close();
10451
10599
  }
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 })) });
10600
+ } else if (picked === "remove" || picked === "reconnect") {
10601
+ const rv = await selectMenu(process.stderr, { title: `${picked} server`, items: mounted.map((m) => ({ label: m.name, value: m.name })) });
10454
10602
  if (!rv) return;
10455
- a = ["remove", rv];
10603
+ a = [picked, rv];
10456
10604
  } else if (picked === "tools" || picked === "resources") {
10457
10605
  if (mounted.length === 1) {
10458
10606
  a = [picked, mounted[0].name];
@@ -10797,6 +10945,7 @@ ${extra}` : body);
10797
10945
  editorRef?.redrawNow();
10798
10946
  };
10799
10947
  if (args.voice && duplex && process.stdin.isTTY) await startVoice(true);
10948
+ let ctxWarned = 0;
10800
10949
  while (true) {
10801
10950
  if (pendingRewind) {
10802
10951
  pendingRewind = false;
@@ -10811,6 +10960,14 @@ ${extra}` : body);
10811
10960
  prefill = void 0;
10812
10961
  const ctxTok = estimateTranscriptTokens(face.transcript);
10813
10962
  const ctxCap = face.options.maxTokens || 2e5;
10963
+ {
10964
+ const pct = Math.round(ctxTok / ctxCap * 100);
10965
+ const step = pct >= 90 ? 90 : pct >= 80 ? 80 : 0;
10966
+ if (step > ctxWarned) {
10967
+ ctxWarned = step;
10968
+ err(yellow(` \u26A0 context ${pct}% full`) + dim(" \u2014 /compact folds older messages (oldest are auto-trimmed at the cap)\n"));
10969
+ } else if (!step) ctxWarned = 0;
10970
+ }
10814
10971
  const usd = session.meta.costUsd ?? 0;
10815
10972
  const computeFooter = () => {
10816
10973
  const parts = [];