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 +2 -2
- package/dist/cli.js +48 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +16 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4281
|
+
report?.output(chunk);
|
|
4274
4282
|
}
|
|
4275
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
10088
|
-
|
|
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((
|
|
10093
|
-
const id = await selectMenu(process.stderr, { title: "Background tasks \xB7 \u21B5
|
|
10094
|
-
if (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: {
|