agent.libx.js 0.94.4 → 0.94.6

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
@@ -104,14 +104,14 @@ agentx --resume <id> "…" # resume a specific session
104
104
  ```
105
105
 
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
- - **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-335` resolves from any directory, and `/sessions all` lists every project's sessions in one picker.
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
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).
110
110
  - **Project instructions** — `./AGENTS.md` (or `CLAUDE.md`) auto-loads into every run; `/init` scaffolds one.
111
111
  - **Any provider** — set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `GROQ_API_KEY`; choose with `-m provider/model`.
112
112
  - **@-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).
113
113
  - **Tab-completion** — `Tab` completes `/<command>` names and `@<path>` file/dir references (descends subdirs, dotfiles hidden unless typed) straight from the working tree.
114
- - **Duplex mode** — `agentx --duplex` runs the full standard REPL (slash commands, sessions, postures, rewind, MCP) with the three-tier engine driving turns: a fast voice model (`--voice-model`, default `groq/openai/gpt-oss-120b`) answers every line instantly and delegates real work to background workers built with the same wiring as a normal run (fs mode, permissions, MCP); worker activity shows as dim chrome and results are re-voiced when ready. Switch any tier live with `/model` (opens a reflex/act/think picker), or the `/voice-model` · `/think-model` shortcuts. `/tasks` lists background tasks and cancels a running one from a picker (Esc mid-turn cancels the foreground turn; Esc again at the idle prompt cancels running workers).
114
+ - **Duplex mode** — `agentx --duplex` runs the full standard REPL (slash commands, sessions, postures, rewind, MCP) with the three-tier engine driving turns: a fast voice model (`--voice-model`, default `groq/openai/gpt-oss-120b`) answers every line instantly and delegates real work to background workers built with the same wiring as a normal run (fs mode, permissions, MCP); worker activity shows as dim chrome and results are re-voiced when ready. Switch any tier live with `/model` (opens a reflex/act/think picker), or the `/voice-model` · `/think-model` shortcuts. `/tasks` lists background tasks, inspects a task's live output tail, and cancels a running one from a picker (Esc mid-turn cancels the foreground turn; Esc again at the idle prompt cancels running workers).
115
115
  - **MCP servers** — declare `mcpServers: { name: { command, args } | { url } }` in config and they're auto-mounted at startup (in parallel, with an optional `mountTimeoutMs` deadline so one slow/dead server never blocks the rest): the client does the JSON-RPC handshake (stdio or HTTP) + `tools/list`, and the discovered tools appear as `mcp__<name>__<tool>` in `/tools` (inspect with `/mcp`). A bad server is logged and skipped, never blocking the agent. For large tool sets, **deferred mode** (`makeMcpToolSearch` / `mountMcpDeferred`) exposes just two bounded tools (`ToolSearch` + `McpCall`) instead of N defs — dodging the provider tool-cap and improving selection accuracy. **`mountMcpCatalog`** goes further: a cached, hash-keyed catalog + lazy connect means a turn that uses no MCP tool opens **zero** connections, and one that uses a tool connects exactly that server — latency scales with tools-used, not servers-configured. A down server is **negative-cached** (`failureCooldownMs`) so it never re-floors a later turn at the deadline. For zero turn-path latency even on a cold process, call **`warmMcpCatalog`** at boot + on a timer (off-turn discovery) and mount with **`{ discover: 'cache-only' }`** — the turn then never synchronously connects: it serves the warmed catalog and discovers any miss in the background.
116
116
 
117
117
  ## 🧬 It improves itself
package/dist/cli.js CHANGED
@@ -4257,22 +4257,30 @@ ${recent}` : brief) + verify;
4257
4257
  const controller = new AbortController();
4258
4258
  const base = tierOpts?.hooks ?? o.actOptions?.hooks;
4259
4259
  const report = o.progressUpdates ? this.progressReporter(id) : void 0;
4260
- const hooks = report ? {
4260
+ const tail = [];
4261
+ const pushTail = (line) => {
4262
+ tail.push(line.slice(0, 200));
4263
+ if (tail.length > 120) tail.splice(0, tail.length - 120);
4264
+ };
4265
+ const hooks = {
4261
4266
  ...base,
4262
4267
  preToolUse: async (call, meta) => {
4263
4268
  const d = await base?.preToolUse?.(call, meta);
4264
- report.pre(call);
4269
+ pushTail(`\u2699 ${describeCall(call)}`);
4270
+ report?.pre(call);
4265
4271
  return d;
4266
4272
  },
4267
4273
  postToolUse: async (call, result, meta) => {
4268
4274
  await base?.postToolUse?.(call, result, meta);
4269
- report.post(call);
4275
+ const last = result?.trim().split("\n").filter(Boolean).pop();
4276
+ if (last) pushTail(` \u21B3 ${last}`);
4277
+ report?.post(call);
4270
4278
  },
4271
4279
  onToolOutput: (call, chunk, meta) => {
4272
4280
  base?.onToolOutput?.(call, chunk, meta);
4273
- report.output(chunk);
4281
+ report?.output(chunk);
4274
4282
  }
4275
- } : base;
4283
+ };
4276
4284
  const relayAsk = async (q2) => {
4277
4285
  const opts = q2.options?.length ? ` Options: ${q2.options.map((x) => x.label).join(", ")}.` : "";
4278
4286
  const a = await this.parkQuestion(id, `${q2.question}${opts}`);
@@ -4294,7 +4302,7 @@ ${recent}` : brief) + verify;
4294
4302
  // shared with the checker so a cancel tears down both
4295
4303
  };
4296
4304
  const promise = new Agent(agentOpts).run(briefText).then((res) => this.maybeVerify(id, briefText, res, tier, agentOpts)).then((res) => this.onWorkerSettled(id, res)).catch((err2) => this.onWorkerFailed(id, err2));
4297
- this.tasks.set(id, { id, label, status: "running", controller, promise });
4305
+ this.tasks.set(id, { id, label, status: "running", controller, promise, tail });
4298
4306
  }
4299
4307
  /** Fresh-context check of a successful Act task: a NEW agent (same model/fs/tools, but NO shared
4300
4308
  * conversation context) re-reads the file state against the brief and fixes any gap. The fix lands
@@ -4413,6 +4421,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4413
4421
  return this.failTask(rec, msg);
4414
4422
  }
4415
4423
  rec.status = "done";
4424
+ rec.result = res.text;
4416
4425
  log7.verbose(`task ${id} done (${res.steps} steps)`);
4417
4426
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
4418
4427
  id,
@@ -4430,6 +4439,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4430
4439
  failTask(rec, msg) {
4431
4440
  this.dropAsk(rec.id);
4432
4441
  rec.status = "error";
4442
+ rec.result = msg;
4433
4443
  log7.warn(`task ${rec.id} failed: ${msg}`);
4434
4444
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
4435
4445
  this.queueRevoice(`[task ${rec.id} failed] ${msg}`);
@@ -6581,7 +6591,17 @@ var SessionStore = class {
6581
6591
  const d = new Date(now5);
6582
6592
  const p = (n, w = 2) => String(n).padStart(w, "0");
6583
6593
  const slug2 = (cwd ?? process.cwd()).split("/").pop()?.replace(/[^A-Za-z0-9_-]/g, "") || "session";
6584
- return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${slug2}`;
6594
+ let id = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${slug2}`;
6595
+ if (existsSync5(this.dir) && existsSync5(join6(this.dir, `${id}.json`))) {
6596
+ for (let i = 2; i <= 99; i++) {
6597
+ const c = `${id}-${i}`;
6598
+ if (!existsSync5(join6(this.dir, `${c}.json`))) {
6599
+ id = c;
6600
+ break;
6601
+ }
6602
+ }
6603
+ }
6604
+ return id;
6585
6605
  }
6586
6606
  /** A session id must be one safe path segment — blocks `../`-style traversal via --resume/load/save. */
6587
6607
  safeId(id) {
@@ -7771,7 +7791,7 @@ function applyKey(s, key, str) {
7771
7791
  }
7772
7792
  if (s.vim === "normal" && s.buf.length) return "none";
7773
7793
  if (s.buf.length) return "cancel";
7774
- if (wasEsc) return "rewind";
7794
+ if (wasEsc || key?.sequence === "\x1B\x1B") return "rewind";
7775
7795
  s.prevEsc = true;
7776
7796
  return "none";
7777
7797
  // first Esc on empty → arm double-Esc
@@ -10067,7 +10087,7 @@ ${extra}` : body);
10067
10087
  `));
10068
10088
  }
10069
10089
  }, tasks: {
10070
- desc: "background tasks \u2014 /tasks [cancel <id>], or alone for a picker (\u21B5 cancels the selected running task)",
10090
+ desc: "background tasks \u2014 /tasks [cancel <id>], or alone for a picker (\u21B5 inspects output; running tasks can be cancelled)",
10071
10091
  run: async (a) => {
10072
10092
  const all = [...dx.tasks.values()];
10073
10093
  if (!all.length) {
@@ -10084,15 +10104,29 @@ ${extra}` : body);
10084
10104
  return;
10085
10105
  }
10086
10106
  const mark = (s) => s === "running" ? cyan("\u25D4 running") : s === "done" ? green("\u2713 done") : s === "cancelled" ? yellow("\u2298 cancelled") : red(`\u2717 ${s}`);
10087
- if (!process.stderr.isTTY || !process.stdin.isTTY) {
10088
- for (const t of all) err(` ${t.id} ${mark(t.status)} ${dim(t.label.slice(0, 60))}
10107
+ const inspect = (t2) => {
10108
+ err(` ${t2.id} ${mark(t2.status)} ${dim(t2.label)}
10089
10109
  `);
10110
+ for (const l of t2.tail.slice(-20)) err(dim(` ${l}
10111
+ `));
10112
+ if (t2.result) err(dim(` \u29BF ${t2.result.split("\n")[0].slice(0, 160)}
10113
+ `));
10114
+ if (!t2.tail.length && !t2.result) err(dim(" (no activity yet)\n"));
10115
+ };
10116
+ if (!process.stderr.isTTY || !process.stdin.isTTY) {
10117
+ for (const t2 of all) inspect(t2);
10090
10118
  return;
10091
10119
  }
10092
- const items = all.map((t) => ({ label: `${t.id} ${t.label.slice(0, 60)}`, value: t.id, desc: mark(t.status) + (t.status === "running" ? " \xB7 \u21B5 to cancel" : "") }));
10093
- const id = await selectMenu(process.stderr, { title: "Background tasks \xB7 \u21B5 cancel running \xB7 esc close", items });
10094
- if (id) err(dim(` ${dx.cancelTask(String(id))}
10120
+ const items = all.map((t2) => ({ label: `${t2.id} ${t2.label.slice(0, 60)}`, value: t2.id, desc: mark(t2.status) + " \xB7 \u21B5 inspect" }));
10121
+ const id = await selectMenu(process.stderr, { title: "Background tasks \xB7 \u21B5 inspect \xB7 esc close", items });
10122
+ if (!id) return;
10123
+ const t = dx.tasks.get(String(id));
10124
+ inspect(t);
10125
+ if (t.status === "running") {
10126
+ const v = await selectMenu(process.stderr, { title: `Cancel ${t.id}?`, items: [{ label: "Keep running", value: "keep" }, { label: "Cancel the task", value: "cancel" }], current: "keep" });
10127
+ if (v === "cancel") err(dim(` ${dx.cancelTask(t.id)}
10095
10128
  `));
10129
+ }
10096
10130
  }
10097
10131
  } } : {},
10098
10132
  reasoning: {