@livx.cc/agentx 0.99.18 → 0.99.20

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.d.ts CHANGED
@@ -150,6 +150,7 @@ declare function makeHost(format?: 'text' | 'json' | 'stream-json', opts?: {
150
150
  stream?: boolean;
151
151
  }): HostBridge & {
152
152
  flushText: () => void;
153
+ takeToolCount: () => number;
153
154
  };
154
155
  /** Hooks that render tool activity to stderr: a preToolUse header, and for edits a colorized diff.
155
156
  * `opts.background` marks the agent as a BACKGROUND one (duplex workers): its chrome lands async on
package/dist/cli.js CHANGED
@@ -9894,11 +9894,15 @@ function lastToken(line) {
9894
9894
  const m = line.match(/(\S*)$/);
9895
9895
  return m ? m[1] : "";
9896
9896
  }
9897
+ function atLineStart(line, token) {
9898
+ return line.slice(0, line.length - token.length).trim() === "";
9899
+ }
9897
9900
  function completeLine(line, ctx) {
9898
9901
  const token = lastToken(line);
9899
- if (token.startsWith("/") && line.trimStart() === token) {
9902
+ if (token.startsWith("/")) {
9903
+ const pool = atLineStart(line, token) ? ctx.commands : ctx.agentCommands ?? ctx.commands;
9900
9904
  const want = token.slice(1);
9901
- const hits = rank(ctx.commands.filter((c) => fuzzy(want, c)), want).map((c) => "/" + c);
9905
+ const hits = rank(pool.filter((c) => fuzzy(want, c)), want).map((c) => "/" + c);
9902
9906
  return [hits, token];
9903
9907
  }
9904
9908
  if (line.startsWith("!") && line.length > 1 && ctx.bangHistory?.length) {
@@ -10156,19 +10160,112 @@ function renderMdLine(line, inFence, p, cols = 80, fenceLang) {
10156
10160
  function renderMdBlock(text, p, cols = 80) {
10157
10161
  let inFence = false, fenceLang;
10158
10162
  const out = [];
10163
+ let table = [];
10164
+ const flush = () => {
10165
+ if (table.length) {
10166
+ out.push(...renderTableLines(table, p, cols));
10167
+ table = [];
10168
+ }
10169
+ };
10159
10170
  for (const line of text.split("\n")) {
10171
+ if (!inFence && isTableRow(line)) {
10172
+ table.push(line);
10173
+ continue;
10174
+ }
10175
+ flush();
10160
10176
  const r = renderMdLine(line, inFence, p, cols, fenceLang);
10161
10177
  inFence = r.inFence;
10162
10178
  fenceLang = r.fenceLang;
10163
10179
  out.push(r.inFence || /^\s*```/.test(line) ? r.out : wrapAnsi(r.out, cols, hangIndent(line)));
10164
10180
  }
10181
+ flush();
10165
10182
  return out.join("\n");
10166
10183
  }
10184
+ var T_PAD = " ";
10185
+ var T_GAP = 3;
10186
+ var SGR_CLOSES = {
10187
+ "0": /./,
10188
+ "22": /^[12]$/,
10189
+ "23": /^3$/,
10190
+ "24": /^4$/,
10191
+ "27": /^7$/,
10192
+ "29": /^9$/,
10193
+ "39": /^(3[0-7]|9[0-7])$/,
10194
+ "49": /^(4[0-7]|10[0-7])$/
10195
+ };
10196
+ function wrapCell(styled, w) {
10197
+ const segs = wrapAnsi(styled, w).split("\r\n");
10198
+ if (segs.length < 2 || !styled.includes("\x1B")) return segs;
10199
+ const tok = new RegExp(ANSI_TOKEN.source, "g");
10200
+ let open = [];
10201
+ return segs.map((seg) => {
10202
+ const prefix = open.join("");
10203
+ for (const m of seg.match(tok) ?? []) {
10204
+ const code = /^\x1b\[([0-9;]*)m$/.exec(m)?.[1];
10205
+ if (code === void 0) continue;
10206
+ const closes = SGR_CLOSES[code || "0"];
10207
+ if (closes) open = open.filter((o) => !closes.test(/^\x1b\[([0-9;]*)m$/.exec(o)[1]));
10208
+ else open.push(m);
10209
+ }
10210
+ return prefix + seg + (open.length ? "\x1B[0m" : "");
10211
+ });
10212
+ }
10213
+ var T_MINCOL = 2;
10214
+ function fitWidths(natural, avail) {
10215
+ const total = natural.reduce((a, b) => a + b, 0);
10216
+ if (total <= avail || !natural.length) return natural;
10217
+ const room = Math.max(T_MINCOL * natural.length, avail);
10218
+ let cap = 1;
10219
+ for (let c = 1; c <= Math.max(...natural); c++) {
10220
+ if (natural.reduce((a, w2) => a + Math.min(w2, c), 0) <= room) cap = c;
10221
+ else break;
10222
+ }
10223
+ const w = natural.map((n) => Math.min(n, cap));
10224
+ let left = room - w.reduce((a, b) => a + b, 0);
10225
+ for (let i = 0; i < w.length && left > 0; i++) {
10226
+ const take = Math.min(left, natural[i] - w[i]);
10227
+ w[i] += take;
10228
+ left -= take;
10229
+ }
10230
+ return w;
10231
+ }
10232
+ function renderTableLines(rows, p, cols = 80) {
10233
+ const sepIdx = rows.findIndex((r, i) => i > 0 && r.includes("-") && /^\s*\|[\s:|-]+\|\s*$/.test(r));
10234
+ if (sepIdx < 0) return rows.map((r) => renderMdLine(r, false, p, cols).out);
10235
+ const cells = (r) => r.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
10236
+ const data = rows.filter((_, i) => i !== sepIdx).map(cells);
10237
+ const ncol = Math.max(...data.map((r) => r.length));
10238
+ const natural = Array.from({ length: ncol }, (_, i) => Math.max(...data.map((r) => displayText(r[i] ?? "").length)));
10239
+ const widths = fitWidths(natural, cols - T_PAD.length - T_GAP * (ncol - 1));
10240
+ const grid = data.map((row, ri) => widths.map((w, ci) => {
10241
+ const c = row[ci] ?? "";
10242
+ const styled = ri === 0 ? p.bold(displayText(vis(c))) : mdInline(vis(c), p);
10243
+ return wrapCell(styled, w);
10244
+ }));
10245
+ const airy = grid.slice(1).some((row) => row.some((c) => c.length > 1));
10246
+ const out = [];
10247
+ grid.forEach((col, ri) => {
10248
+ const header = ri === 0;
10249
+ if (airy && ri > 1) out.push("");
10250
+ const height = Math.max(...col.map((c) => c.length));
10251
+ for (let li = 0; li < height; li++) {
10252
+ out.push((T_PAD + widths.map((w, ci) => {
10253
+ const seg = col[ci][li] ?? "";
10254
+ return seg + " ".repeat(Math.max(0, w - visWidth(seg)));
10255
+ }).join(" ".repeat(T_GAP))).replace(/\s+$/, ""));
10256
+ }
10257
+ if (header) out.push(T_PAD + p.dim("\u2500".repeat(widths.reduce((a, b) => a + b, 0) + T_GAP * (ncol - 1))));
10258
+ });
10259
+ return out;
10260
+ }
10167
10261
  function isTableRow(line) {
10168
10262
  return /^\s*\|.*\|\s*$/.test(line);
10169
10263
  }
10170
10264
  var ANSI_TOKEN = /\x1b\[[0-9;]*m|\x1b\]8;;[^\x07\x1b]*(?:\x07|\x1b\\)/;
10171
10265
  var WRAP_TOKENS = new RegExp(`${ANSI_TOKEN.source}|[ \\t]+|[^\\s\\x1b]+`, "g");
10266
+ function visWidth(s) {
10267
+ return s.replace(new RegExp(ANSI_TOKEN.source, "g"), "").length;
10268
+ }
10172
10269
  function wrapAnsi(s, cols, indent = "") {
10173
10270
  if (cols <= 1 || s.replace(new RegExp(ANSI_TOKEN.source, "g"), "").length <= cols) return s;
10174
10271
  if (indent.length >= cols) indent = "";
@@ -10235,29 +10332,11 @@ var MarkdownStream = class {
10235
10332
  this.cols = Math.max(1, cols);
10236
10333
  if (this.buf) this.lineRows = Math.max(1, Math.ceil(this.buf.length / this.cols));
10237
10334
  }
10238
- /** Render the held table block as aligned columns and reset the buffer. If the block lacks a `|---|`
10239
- * separator row it isn't a real table — emit the rows verbatim (rendered as normal lines) instead. */
10335
+ /** Render the held table block as aligned columns (shared with renderMdBlock) and reset the buffer. */
10240
10336
  flushTable() {
10241
10337
  const rows = this.tableBuf;
10242
10338
  this.tableBuf = [];
10243
- const sepIdx = rows.findIndex((r, i) => i > 0 && r.includes("-") && /^\s*\|[\s:|-]+\|\s*$/.test(r));
10244
- if (sepIdx < 0) return rows.map((r) => renderMdLine(r, false, this.p, this.cols).out + "\r\n").join("");
10245
- const cells = (r) => r.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
10246
- const data = rows.filter((_, i) => i !== sepIdx).map(cells);
10247
- const ncol = Math.max(...data.map((r) => r.length));
10248
- const widths = Array.from({ length: ncol }, (_, i) => Math.max(...data.map((r) => displayText(r[i] ?? "").length)));
10249
- const out = [];
10250
- data.forEach((row, ri) => {
10251
- const header = ri === 0;
10252
- const line = " " + widths.map((w, ci) => {
10253
- const c = row[ci] ?? "";
10254
- const styled = header ? this.p.bold(displayText(vis(c))) : mdInline(vis(c), this.p);
10255
- return styled + " ".repeat(Math.max(0, w - displayText(c).length));
10256
- }).join(" ").replace(/\s+$/, "");
10257
- out.push(line);
10258
- if (header) out.push(" " + this.p.dim("\u2500".repeat(widths.reduce((a, b) => a + b, 0) + 3 * (ncol - 1))));
10259
- });
10260
- return out.map((l) => l + "\r\n").join("");
10339
+ return renderTableLines(rows, this.p, this.cols).map((l) => l + "\r\n").join("");
10261
10340
  }
10262
10341
  feed(text) {
10263
10342
  this.buf += text;
@@ -10484,6 +10563,14 @@ var EditorState = class _EditorState {
10484
10563
  for (const [ph, full] of this.pastes) if (out.includes(ph)) out = out.split(ph).join(full);
10485
10564
  return out;
10486
10565
  }
10566
+ /**
10567
+ * A `/cmd` the REPL will DISPATCH — i.e. the line's first word. A slash token elsewhere in the
10568
+ * prompt is prose the model reads, so it completes like an `@path`: no accept-and-run, no
10569
+ * exact-match row. Kept in sync with the REPL's whole-line `/` dispatch via `atLineStart`.
10570
+ */
10571
+ get runnable() {
10572
+ return this.token.startsWith("/") && atLineStart(this.buf.slice(0, this.cursor), this.token);
10573
+ }
10487
10574
  /** Recompute suggestions from the line up to the cursor. */
10488
10575
  refresh() {
10489
10576
  const r = this.suggest(this.buf.slice(0, this.cursor));
@@ -10491,7 +10578,7 @@ var EditorState = class _EditorState {
10491
10578
  this.token = r.token;
10492
10579
  this.describe = r.describe;
10493
10580
  const exact = r.hits.length === 1 && r.hits[0] === r.token;
10494
- this.menuOpen = r.hits.length > 0 && (!exact || r.token.startsWith("/"));
10581
+ this.menuOpen = r.hits.length > 0 && (!exact || this.runnable);
10495
10582
  if (this.sel >= r.hits.length) this.sel = 0;
10496
10583
  }
10497
10584
  // ── Editor undo (Ctrl-_): snapshot before every mutation, pop to restore ──
@@ -10687,13 +10774,22 @@ var EditorState = class _EditorState {
10687
10774
  this.hits = [];
10688
10775
  this.sel = 0;
10689
10776
  }
10690
- /** Replace the trailing token with the highlighted hit; re-suggest (e.g. `@src/` opens its children). */
10777
+ /**
10778
+ * Replace the trailing token with the highlighted hit; re-suggest (e.g. `@src/` opens its children).
10779
+ * A finished hit gets a trailing space so the next word is typed straight away (CC/shell parity).
10780
+ * Two exceptions: a DIRECTORY hit (`@src/`) is a waypoint, not a word — a space there would end the
10781
+ * mention mid-path; and an existing following space isn't doubled. A spaced dir rides the quoted
10782
+ * form (`@"my dir/"`), so the `/` test must look past a trailing quote.
10783
+ */
10691
10784
  accept() {
10692
10785
  if (!this.menuOpen || !this.hits.length) return false;
10693
10786
  const hit = this.hits[this.sel] ?? this.hits[0];
10694
10787
  const start = this.cursor - this.token.length;
10695
- this.buf = this.buf.slice(0, start) + hit + this.buf.slice(this.cursor);
10696
- this.cursor = start + hit.length;
10788
+ const rest = this.buf.slice(this.cursor);
10789
+ const isDir = hit.replace(/"$/, "").endsWith("/");
10790
+ const pad = isDir || rest.startsWith(" ") ? "" : " ";
10791
+ this.buf = this.buf.slice(0, start) + hit + pad + rest;
10792
+ this.cursor = start + hit.length + pad.length;
10697
10793
  this.refresh();
10698
10794
  return true;
10699
10795
  }
@@ -10907,9 +11003,9 @@ function applyKey(s, key, str) {
10907
11003
  case "return":
10908
11004
  case "enter":
10909
11005
  if (s.menuOpen) {
10910
- const isCmd = s.token.startsWith("/");
11006
+ const run = s.runnable;
10911
11007
  s.accept();
10912
- return isCmd ? "submit" : "none";
11008
+ return run ? "submit" : "none";
10913
11009
  }
10914
11010
  if (s.buf.slice(0, s.cursor).endsWith("\\")) {
10915
11011
  s.backspace();
@@ -12137,8 +12233,17 @@ function makeHost(format = "text", opts) {
12137
12233
  }
12138
12234
  };
12139
12235
  const activitySeen = /* @__PURE__ */ new Set();
12236
+ const turnActivityIds = /* @__PURE__ */ new Set();
12237
+ let anonActivityCount = 0;
12140
12238
  return {
12141
12239
  flushText,
12240
+ /** Read + reset this turn's self-executing-backend tool tally (see turnActivityIds). */
12241
+ takeToolCount: () => {
12242
+ const n = turnActivityIds.size + anonActivityCount;
12243
+ turnActivityIds.clear();
12244
+ anonActivityCount = 0;
12245
+ return n;
12246
+ },
12142
12247
  notify(e) {
12143
12248
  if (e.kind === "turn_start") {
12144
12249
  activitySeen.clear();
@@ -12171,6 +12276,8 @@ function makeHost(format = "text", opts) {
12171
12276
  }
12172
12277
  if (e.kind === "tool_activity") {
12173
12278
  const a = e;
12279
+ if (a.id) turnActivityIds.add(a.id);
12280
+ else if (a.status === "running") anonActivityCount++;
12174
12281
  flushLookupGroup();
12175
12282
  closeReasonLine();
12176
12283
  flushText();
@@ -12869,6 +12976,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
12869
12976
  spinner.turnStart = t0;
12870
12977
  spinner.start(sendFn ? "voice\u2026" : void 0);
12871
12978
  const msgsBefore = agent.transcript.length;
12979
+ agent.options.host?.takeToolCount?.();
12872
12980
  const host = agent.options.host;
12873
12981
  const origNotify = host?.notify?.bind(host);
12874
12982
  if (host && origNotify) host.notify = (e) => {
@@ -12910,7 +13018,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
12910
13018
  const m = res.usageEstimated ? "~" : "";
12911
13019
  const tok = res.usage?.totalTokens ? `${m}${(res.usage.totalTokens / 1e3).toFixed(1)}k tok \xB7 ${cost > 0 ? m + fmtUsd(cost) + " \xB7 " : ""}` : "";
12912
13020
  const lastUser = res.messages.map((m2) => m2.role).lastIndexOf("user");
12913
- const tools = res.messages.slice(lastUser).filter((m2) => m2.role === "tool").length;
13021
+ const tools = res.messages.slice(lastUser).filter((m2) => m2.role === "tool").length + (agent.options.host?.takeToolCount?.() ?? 0);
12914
13022
  const ok = res.finishReason === "stop";
12915
13023
  const shortId = session.meta.id.replace(/^\d{8}-/, "");
12916
13024
  const didWork = agent.transcript.slice(msgsBefore).some((m2) => m2.role === "assistant" || m2.role === "tool");
@@ -14751,10 +14859,11 @@ ${task}`;
14751
14859
  return null;
14752
14860
  }
14753
14861
  };
14754
- const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
14862
+ const agentCommandNames = () => [...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
14863
+ const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...agentCommandNames()];
14755
14864
  const describe = (hit) => hit.startsWith("/") ? builtins[hit.slice(1)]?.desc ?? cmds.find((c) => c.name === hit.slice(1))?.description ?? skills.find((s) => s.name === hit.slice(1))?.description : void 0;
14756
14865
  const suggest = (lineToCursor) => {
14757
- const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), listDir, bangHistory: history });
14866
+ const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), agentCommands: agentCommandNames(), listDir, bangHistory: history });
14758
14867
  return { hits, token, describe };
14759
14868
  };
14760
14869
  const editor = createLineEditor(process.stderr);