@agent-commons/cli 0.1.14 → 0.1.17

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.
Files changed (2) hide show
  1. package/dist/bin.js +434 -39
  2. package/package.json +7 -5
package/dist/bin.js CHANGED
@@ -106,7 +106,7 @@ var sym = {
106
106
  bullet: import_chalk.default.dim("\u2022"),
107
107
  dot: import_chalk.default.dim("\xB7")
108
108
  };
109
- function banner(version = "0.1.14") {
109
+ function banner(version = "0.1.17") {
110
110
  const line = import_chalk.default.cyan(" \u2500".padEnd(2) + "\u2500".repeat(44));
111
111
  console.log("");
112
112
  console.log(line);
@@ -515,7 +515,7 @@ ${sym.ok} Agent created`);
515
515
  process.exit(1);
516
516
  }
517
517
  });
518
- const autonomy = cmd.command("autonomy").description("Manage agent heartbeat / autonomy");
518
+ const autonomy = cmd.command("autonomy").description("Manage agent heartbeat");
519
519
  autonomy.command("status").description("Show autonomy status for an agent").requiredOption("--agent <agentId>", "Agent ID").option("--json", "Output as JSON").action(async (opts) => {
520
520
  const client = makeClient();
521
521
  const spinner = spin("Fetching autonomy status\u2026");
@@ -525,7 +525,7 @@ ${sym.ok} Agent created`);
525
525
  const s = res.data;
526
526
  if (opts.json) return jsonOut(s);
527
527
  console.log(`
528
- ${c.bold("Autonomy Status")}`);
528
+ ${c.bold("Heartbeat Status")}`);
529
529
  detail([
530
530
  ["Enabled", s.enabled ? c.bold("yes") : "no"],
531
531
  ["Interval", s.intervalSec ? `${s.intervalSec}s` : "n/a"],
@@ -539,7 +539,7 @@ ${c.bold("Autonomy Status")}`);
539
539
  process.exit(1);
540
540
  }
541
541
  });
542
- autonomy.command("enable").description("Enable autonomous heartbeat for an agent").requiredOption("--agent <agentId>", "Agent ID").option("--interval <seconds>", "Heartbeat interval in seconds (min 30)", "300").action(async (opts) => {
542
+ autonomy.command("enable").description("Enable heartbeat for an agent").requiredOption("--agent <agentId>", "Agent ID").option("--interval <seconds>", "Heartbeat interval in seconds (min 30)", "300").action(async (opts) => {
543
543
  const client = makeClient();
544
544
  const spinner = spin("Enabling autonomy\u2026");
545
545
  try {
@@ -557,7 +557,7 @@ ${sym.ok} Autonomy enabled for agent ${c.id(opts.agent)}`);
557
557
  process.exit(1);
558
558
  }
559
559
  });
560
- autonomy.command("disable").description("Disable autonomous heartbeat for an agent").requiredOption("--agent <agentId>", "Agent ID").action(async (opts) => {
560
+ autonomy.command("disable").description("Disable heartbeat for an agent").requiredOption("--agent <agentId>", "Agent ID").action(async (opts) => {
561
561
  const client = makeClient();
562
562
  const spinner = spin("Disabling autonomy\u2026");
563
563
  try {
@@ -571,7 +571,7 @@ ${sym.ok} Autonomy disabled for agent ${c.id(opts.agent)}`);
571
571
  process.exit(1);
572
572
  }
573
573
  });
574
- autonomy.command("trigger").description("Trigger a single heartbeat beat immediately").requiredOption("--agent <agentId>", "Agent ID").action(async (opts) => {
574
+ autonomy.command("trigger").description("Trigger a single heartbeat immediately").requiredOption("--agent <agentId>", "Agent ID").action(async (opts) => {
575
575
  const client = makeClient();
576
576
  const spinner = spin("Triggering heartbeat\u2026");
577
577
  try {
@@ -1257,6 +1257,13 @@ var import_fs3 = require("fs");
1257
1257
  var import_path3 = require("path");
1258
1258
  var import_child_process2 = require("child_process");
1259
1259
  var readline2 = __toESM(require("readline"));
1260
+ var pdfParse = require("pdf-parse/lib/pdf-parse.js");
1261
+ var managedProcesses = /* @__PURE__ */ new Map();
1262
+ function capBuffer(existing, chunk, maxBytes) {
1263
+ const joined = existing + chunk;
1264
+ if (joined.length <= maxBytes) return joined;
1265
+ return "\u2026(truncated)\n" + joined.slice(-(maxBytes - 20));
1266
+ }
1260
1267
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".cache", "__pycache__", ".next", "dist", "build", ".DS_Store"]);
1261
1268
  function buildDirSnapshot(dir, maxDepth = 2) {
1262
1269
  const lines = [`${dir}/`];
@@ -1335,27 +1342,59 @@ ${fileSection}
1335
1342
 
1336
1343
  | Tool | What it does |
1337
1344
  |------|-------------|
1338
- | \`cli_list_directory\` | List files and folders at a path (default: session root) |
1339
- | \`cli_read_file\` | Read the full contents of a file |
1345
+ | \`cli_list_directory\` | List files and folders at a path |
1346
+ | \`cli_read_file\` | Read a file (PDF and Word docs are extracted to text) |
1340
1347
  | \`cli_write_file\` | Write or overwrite a file (user confirmation required) |
1341
- | \`cli_search_files\` | Find files matching a pattern, e.g. "*.ts" |
1342
- | \`cli_run_command\` | Run a shell command and return output (user confirmation required) |
1348
+ | \`cli_search_files\` | Find files matching a pattern |
1349
+ | \`cli_run_command\` | Run a short command and return its output (user confirmation required) |
1350
+ | \`cli_start_process\` | Start a long-running command in the background; returns a processId immediately |
1351
+ | \`cli_wait_for_process\` | Block up to N seconds for a background process, then return current output |
1352
+ | \`cli_process_status\` | Instant non-blocking check on a background process |
1353
+ | \`cli_kill_process\` | Kill a running background process |
1354
+ | \`cli_list_processes\` | List all background processes started this session |
1355
+
1356
+ ### Choosing between run_command and start_process
1357
+
1358
+ | Situation | Use |
1359
+ |-----------|-----|
1360
+ | Command finishes in under ~30s | \`cli_run_command\` |
1361
+ | Command may take minutes (npm install, build, scaffold) | \`cli_start_process\` + \`cli_wait_for_process\` |
1362
+ | Command needs live stdin (e.g. a REPL) | \`cli_run_command\` with \`"interactive": true\` |
1343
1363
 
1344
- ### Example \u2014 listing a directory
1364
+ ### run_command options
1365
+ - \`timeout_seconds\` (default 120, max 300) \u2014 kill the process after N seconds
1366
+ - \`interactive\` (boolean) \u2014 connects the user's terminal stdin for commands that need input
1345
1367
 
1346
- When the user asks "what's on my desktop?", call \`cli_list_directory\` with \`{"path": "Desktop"}\` immediately. Then show the result.
1368
+ ### start_process + wait_for_process pattern
1347
1369
 
1348
- ### Example \u2014 reading a file
1370
+ For long commands like \`npx create-next-app@latest my-app --yes\`:
1349
1371
 
1350
- Call \`cli_read_file\` with \`{"path": "Desktop/notes.txt"}\`. Then quote the content in your reply.
1372
+ 1. Call \`cli_start_process\` \u2014 returns \`{processId, status: "running"}\` immediately. Tell the user it has started.
1373
+ 2. Call \`cli_wait_for_process\` with \`{"processId": "...", "wait_seconds": 60}\` \u2014 blocks up to 60s then returns current stdout/status. Report progress to the user.
1374
+ 3. Repeat step 2 until \`status\` is \`"done"\` or \`"error"\`.
1375
+ 4. Report the final output to the user.
1351
1376
 
1352
- ### Example \u2014 writing a file
1377
+ Never hold the user in silence. Between each \`cli_wait_for_process\` call, tell them what you saw so far.
1353
1378
 
1354
- Call \`cli_write_file\` with \`{"path": "output.txt", "content": "Hello"}\`. The user will be prompted to confirm.
1379
+ ### Example \u2014 scaffolding a Next.js project
1380
+
1381
+ \`\`\`
1382
+ cli_start_process: {"command": "npx", "args": ["create-next-app@latest", "my-app", "--yes"], "cwd": "Desktop"}
1383
+ \u2192 {processId: "proc_1a2b", status: "running"}
1355
1384
 
1356
- ### Example \u2014 running a command
1385
+ Tell user: "Started! Installing dependencies, this takes a minute or two. Checking in 60s\u2026"
1357
1386
 
1358
- Call \`cli_run_command\` with \`{"command": "ls", "args": ["-la"]}\`. The user will be prompted to confirm.
1387
+ cli_wait_for_process: {"processId": "proc_1a2b", "wait_seconds": 60}
1388
+ \u2192 {status: "running", elapsedSec: 60, stdout: "Creating project...
1389
+ Installing packages\u2026"}
1390
+
1391
+ Tell user: "Still installing \u2014 here's output so far: [stdout]. Checking again\u2026"
1392
+
1393
+ cli_wait_for_process: {"processId": "proc_1a2b", "wait_seconds": 60}
1394
+ \u2192 {status: "done", exitCode: 0, elapsedSec: 93, stdout: "Success! Created my-app"}
1395
+
1396
+ Tell user: "Done! Project created in Desktop/my-app"
1397
+ \`\`\`
1359
1398
  `;
1360
1399
  }
1361
1400
  var TOOL_CALL_RE = /```tool\s*\n([\s\S]*?)\n```/;
@@ -1421,6 +1460,76 @@ async function confirm(message, config, permissionKey) {
1421
1460
  });
1422
1461
  });
1423
1462
  }
1463
+ var OFFICE_EXTS = /* @__PURE__ */ new Set([".docx", ".doc", ".rtf", ".odt", ".pages"]);
1464
+ var PDF_EXTS = /* @__PURE__ */ new Set([".pdf"]);
1465
+ var UNREADABLE_BINARY_EXTS = /* @__PURE__ */ new Set([
1466
+ ".png",
1467
+ ".jpg",
1468
+ ".jpeg",
1469
+ ".gif",
1470
+ ".bmp",
1471
+ ".ico",
1472
+ ".webp",
1473
+ ".tiff",
1474
+ ".mp3",
1475
+ ".mp4",
1476
+ ".wav",
1477
+ ".aac",
1478
+ ".ogg",
1479
+ ".flac",
1480
+ ".zip",
1481
+ ".tar",
1482
+ ".gz",
1483
+ ".bz2",
1484
+ ".7z",
1485
+ ".rar",
1486
+ ".exe",
1487
+ ".dll",
1488
+ ".so",
1489
+ ".dylib",
1490
+ ".bin",
1491
+ ".psd",
1492
+ ".ai",
1493
+ ".sketch",
1494
+ ".figma",
1495
+ ".xlsx",
1496
+ ".xls",
1497
+ ".pptx",
1498
+ ".ppt"
1499
+ ]);
1500
+ function extractViaCommand(cmd, cmdArgs) {
1501
+ return new Promise((res) => {
1502
+ (0, import_child_process2.execFile)(cmd, cmdArgs, { timeout: 3e4, maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
1503
+ if (err) res("");
1504
+ else res(stdout.trim());
1505
+ });
1506
+ });
1507
+ }
1508
+ async function extractPdfText(abs) {
1509
+ try {
1510
+ const buffer = (0, import_fs3.readFileSync)(abs);
1511
+ const data = await pdfParse(buffer);
1512
+ const text2 = data.text?.trim();
1513
+ if (text2) {
1514
+ const MAX_CHARS = 15e4;
1515
+ if (text2.length > MAX_CHARS) {
1516
+ return text2.slice(0, MAX_CHARS) + `
1517
+
1518
+ [\u2026truncated \u2014 showing first ${MAX_CHARS.toLocaleString()} characters of ${text2.length.toLocaleString()} total]`;
1519
+ }
1520
+ return text2;
1521
+ }
1522
+ } catch {
1523
+ }
1524
+ const text = await extractViaCommand("pdftotext", [abs, "-"]);
1525
+ if (text) return text;
1526
+ return `[Cannot extract PDF text: the file may be scanned/image-only or password-protected]`;
1527
+ }
1528
+ async function extractOfficeText(abs, ext) {
1529
+ const text = await extractViaCommand("textutil", ["-stdout", "-cat", "txt", abs]);
1530
+ if (text) return text;
1531
+ return `[Cannot extract ${ext} text: textutil failed or is unavailable on this system]`;
1532
+ }
1424
1533
  async function toolReadFile(args, cfg) {
1425
1534
  const { path: userPath } = args;
1426
1535
  if (!userPath) throw new Error('read_file requires a "path" argument');
@@ -1429,6 +1538,18 @@ async function toolReadFile(args, cfg) {
1429
1538
  if (!(0, import_fs3.existsSync)(abs)) throw new Error(`File not found: ${userPath}`);
1430
1539
  const stat = (0, import_fs3.statSync)(abs);
1431
1540
  if (stat.isDirectory()) throw new Error(`"${userPath}" is a directory, not a file`);
1541
+ const ext = (0, import_path3.extname)(abs).toLowerCase();
1542
+ if (PDF_EXTS.has(ext)) {
1543
+ if (stat.size > 5e7) throw new Error(`PDF too large to read (${Math.round(stat.size / 1e6)} MB). Max 50 MB.`);
1544
+ return extractPdfText(abs);
1545
+ }
1546
+ if (OFFICE_EXTS.has(ext)) {
1547
+ if (stat.size > 2e7) throw new Error(`Document too large to read (${Math.round(stat.size / 1e6)} MB). Max 20 MB.`);
1548
+ return extractOfficeText(abs, ext);
1549
+ }
1550
+ if (UNREADABLE_BINARY_EXTS.has(ext)) {
1551
+ throw new Error(`Cannot read binary file "${userPath}" (${ext} format). Only text, PDF, and Office documents are supported.`);
1552
+ }
1432
1553
  if (stat.size > 5e5) throw new Error(`File too large to read (${Math.round(stat.size / 1024)} KB). Max 500 KB.`);
1433
1554
  return (0, import_fs3.readFileSync)(abs, "utf8");
1434
1555
  }
@@ -1487,11 +1608,12 @@ async function toolSearchFiles(args, cfg) {
1487
1608
  return results.length ? results.join("\n") : "No files found matching: " + pattern;
1488
1609
  }
1489
1610
  async function toolRunCommand(args, cfg) {
1490
- const { command, args: cmdArgs = [], cwd } = args;
1611
+ const { command, args: cmdArgs = [], cwd, timeout_seconds, interactive } = args;
1491
1612
  if (!command || typeof command !== "string") throw new Error('run_command requires a "command" string');
1492
1613
  if (!Array.isArray(cmdArgs)) throw new Error('"args" must be an array of strings');
1493
1614
  const workDir = cwd ? safePath(cfg.rootDir, cwd) : cfg.rootDir;
1494
1615
  const preview = [command, ...cmdArgs].join(" ");
1616
+ const timeoutMs = Math.min((typeof timeout_seconds === "number" ? timeout_seconds : 120) * 1e3, 3e5);
1495
1617
  const ok = await confirm(
1496
1618
  `Agent wants to run: \x1B[1m${preview}\x1B[0m
1497
1619
  \x1B[2min: ${workDir}\x1B[0m`,
@@ -1499,14 +1621,142 @@ async function toolRunCommand(args, cfg) {
1499
1621
  "run_command"
1500
1622
  );
1501
1623
  if (!ok) return "User denied command execution.";
1624
+ if (interactive) {
1625
+ return new Promise((resolve2) => {
1626
+ const child = (0, import_child_process2.spawn)(command, cmdArgs.map(String), { cwd: workDir, stdio: "inherit" });
1627
+ const timer = setTimeout(() => {
1628
+ child.kill();
1629
+ resolve2(`(command timed out after ${timeoutMs / 1e3}s)`);
1630
+ }, timeoutMs);
1631
+ child.on("close", (code) => {
1632
+ clearTimeout(timer);
1633
+ resolve2(`(command exited with code ${code ?? "unknown"})`);
1634
+ });
1635
+ child.on("error", (err) => {
1636
+ clearTimeout(timer);
1637
+ resolve2(`Error: ${err.message}`);
1638
+ });
1639
+ });
1640
+ }
1502
1641
  return new Promise((resolve2) => {
1503
- (0, import_child_process2.execFile)(command, cmdArgs.map(String), { cwd: workDir, timeout: 3e4, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
1642
+ (0, import_child_process2.execFile)(command, cmdArgs.map(String), { cwd: workDir, timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
1504
1643
  const out = [stdout, stderr].filter(Boolean).join("\n--- stderr ---\n");
1505
1644
  if (err && !out) return resolve2(`Error: ${err.message}`);
1506
1645
  resolve2(out || "(no output)");
1507
1646
  });
1508
1647
  });
1509
1648
  }
1649
+ async function toolStartProcess(args, cfg) {
1650
+ const { command, args: cmdArgs = [], cwd } = args;
1651
+ if (!command || typeof command !== "string") throw new Error('start_process requires a "command" string');
1652
+ if (!Array.isArray(cmdArgs)) throw new Error('"args" must be an array of strings');
1653
+ const workDir = cwd ? safePath(cfg.rootDir, cwd) : cfg.rootDir;
1654
+ const preview = [command, ...cmdArgs].join(" ");
1655
+ const ok = await confirm(
1656
+ `Agent wants to start background process: \x1B[1m${preview}\x1B[0m
1657
+ \x1B[2min: ${workDir}\x1B[0m`,
1658
+ cfg,
1659
+ "start_process"
1660
+ );
1661
+ if (!ok) return JSON.stringify({ error: "User denied process start." });
1662
+ const id = `proc_${Date.now().toString(36)}`;
1663
+ const child = (0, import_child_process2.spawn)(command, cmdArgs.map(String), {
1664
+ cwd: workDir,
1665
+ stdio: ["ignore", "pipe", "pipe"],
1666
+ detached: false
1667
+ });
1668
+ const proc = {
1669
+ id,
1670
+ command: preview,
1671
+ status: "running",
1672
+ exitCode: null,
1673
+ stdout: "",
1674
+ stderr: "",
1675
+ startedAt: /* @__PURE__ */ new Date(),
1676
+ endedAt: null,
1677
+ child
1678
+ };
1679
+ child.stdout?.on("data", (chunk) => {
1680
+ proc.stdout = capBuffer(proc.stdout, chunk.toString(), 2e5);
1681
+ });
1682
+ child.stderr?.on("data", (chunk) => {
1683
+ proc.stderr = capBuffer(proc.stderr, chunk.toString(), 5e4);
1684
+ });
1685
+ child.on("close", (code) => {
1686
+ proc.status = code === 0 ? "done" : "error";
1687
+ proc.exitCode = code;
1688
+ proc.endedAt = /* @__PURE__ */ new Date();
1689
+ });
1690
+ child.on("error", (err) => {
1691
+ proc.status = "error";
1692
+ proc.endedAt = /* @__PURE__ */ new Date();
1693
+ proc.stderr = capBuffer(proc.stderr, `
1694
+ Spawn error: ${err.message}`, 5e4);
1695
+ });
1696
+ managedProcesses.set(id, proc);
1697
+ cfg.appendLog({ type: "process_start", processId: id, command: preview, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1698
+ return JSON.stringify({ processId: id, status: "running", command: preview });
1699
+ }
1700
+ function processSnapshot(proc) {
1701
+ const elapsedSec = Math.round((Date.now() - proc.startedAt.getTime()) / 1e3);
1702
+ const recentStdout = proc.stdout.length > 4e3 ? "\u2026(earlier output truncated)\n" + proc.stdout.slice(-4e3) : proc.stdout;
1703
+ return JSON.stringify({
1704
+ processId: proc.id,
1705
+ command: proc.command,
1706
+ status: proc.status,
1707
+ exitCode: proc.exitCode,
1708
+ elapsedSec,
1709
+ stdout: recentStdout || "(no output yet)",
1710
+ stderr: proc.stderr.slice(-1e3) || void 0
1711
+ });
1712
+ }
1713
+ async function toolProcessStatus(args, _cfg) {
1714
+ const { processId } = args;
1715
+ if (!processId) throw new Error('process_status requires a "processId" argument');
1716
+ const proc = managedProcesses.get(processId);
1717
+ if (!proc) return JSON.stringify({ error: `No process found with id "${processId}"` });
1718
+ return processSnapshot(proc);
1719
+ }
1720
+ async function toolWaitForProcess(args, _cfg) {
1721
+ const { processId, wait_seconds = 60 } = args;
1722
+ if (!processId) throw new Error('wait_for_process requires a "processId" argument');
1723
+ const proc = managedProcesses.get(processId);
1724
+ if (!proc) return JSON.stringify({ error: `No process found with id "${processId}"` });
1725
+ if (proc.status !== "running") return processSnapshot(proc);
1726
+ const maxWait = Math.min((typeof wait_seconds === "number" ? wait_seconds : 60) * 1e3, 12e4);
1727
+ const deadline = Date.now() + maxWait;
1728
+ await new Promise((resolve2) => {
1729
+ const tick = setInterval(() => {
1730
+ if (proc.status !== "running" || Date.now() >= deadline) {
1731
+ clearInterval(tick);
1732
+ resolve2();
1733
+ }
1734
+ }, 500);
1735
+ });
1736
+ return processSnapshot(proc);
1737
+ }
1738
+ async function toolKillProcess(args, cfg) {
1739
+ const { processId } = args;
1740
+ if (!processId) throw new Error('kill_process requires a "processId" argument');
1741
+ const proc = managedProcesses.get(processId);
1742
+ if (!proc) return JSON.stringify({ error: `No process found with id "${processId}"` });
1743
+ if (proc.status !== "running") return JSON.stringify({ error: `Process "${processId}" is not running (status: ${proc.status})` });
1744
+ proc.child.kill("SIGTERM");
1745
+ proc.status = "killed";
1746
+ proc.endedAt = /* @__PURE__ */ new Date();
1747
+ cfg.appendLog({ type: "process_killed", processId, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1748
+ return JSON.stringify({ processId, status: "killed" });
1749
+ }
1750
+ async function toolListProcesses(_args, _cfg) {
1751
+ if (managedProcesses.size === 0) return JSON.stringify([]);
1752
+ const list = [...managedProcesses.values()].map((p) => ({
1753
+ processId: p.id,
1754
+ command: p.command,
1755
+ status: p.status,
1756
+ elapsedSec: Math.round((Date.now() - p.startedAt.getTime()) / 1e3)
1757
+ }));
1758
+ return JSON.stringify(list);
1759
+ }
1510
1760
  async function runLocalTool(call, cfg) {
1511
1761
  const { tool, args } = call;
1512
1762
  cfg.appendLog({
@@ -1533,8 +1783,23 @@ async function runLocalTool(call, cfg) {
1533
1783
  case "run_command":
1534
1784
  result = await toolRunCommand(args, cfg);
1535
1785
  break;
1786
+ case "start_process":
1787
+ result = await toolStartProcess(args, cfg);
1788
+ break;
1789
+ case "process_status":
1790
+ result = await toolProcessStatus(args, cfg);
1791
+ break;
1792
+ case "wait_for_process":
1793
+ result = await toolWaitForProcess(args, cfg);
1794
+ break;
1795
+ case "kill_process":
1796
+ result = await toolKillProcess(args, cfg);
1797
+ break;
1798
+ case "list_processes":
1799
+ result = await toolListProcesses(args, cfg);
1800
+ break;
1536
1801
  default:
1537
- result = `Unknown tool: "${tool}". Available: read_file, write_file, list_directory, search_files, run_command`;
1802
+ result = `Unknown tool: "${tool}". Available: read_file, write_file, list_directory, search_files, run_command, start_process, wait_for_process, process_status, kill_process, list_processes`;
1538
1803
  }
1539
1804
  } catch (err) {
1540
1805
  result = `Error: ${err?.message ?? String(err)}`;
@@ -1775,12 +2040,13 @@ ${content}
1775
2040
  messages: [{ role: "user", content: userMessage }],
1776
2041
  ...cliContext && { cliContext }
1777
2042
  };
1778
- process.stdout.write(c.primary("agent") + c.dim(" \u203A "));
1779
2043
  if (opts.noStream) {
1780
- const spinner = spin("");
2044
+ process.stdout.write(c.primary("agent") + c.dim(" \u203A "));
2045
+ const spinner = spin("thinking\u2026");
1781
2046
  try {
1782
2047
  const result = await client.run.once(params);
1783
2048
  spinner.stop();
2049
+ process.stdout.write(c.primary("agent") + c.dim(" \u203A "));
1784
2050
  const text = extractText(result);
1785
2051
  console.log(text);
1786
2052
  appendSessionLog(sessionId, {
@@ -1798,29 +2064,57 @@ ${sym.fail} ${c.error(err.message ?? String(err))}`);
1798
2064
  try {
1799
2065
  let hasOutput = false;
1800
2066
  let agentContent = "";
2067
+ let toolStartMs = 0;
2068
+ let lastToolName = "";
2069
+ const thinkingSpinner = spin("thinking\u2026");
1801
2070
  for await (const event of client.agents.stream(params)) {
1802
2071
  if (event.type === "token") {
2072
+ if (thinkingSpinner.isSpinning) {
2073
+ thinkingSpinner.stop();
2074
+ process.stdout.write(c.primary("agent") + c.dim(" \u203A "));
2075
+ }
1803
2076
  const tok = event.content ?? "";
1804
2077
  process.stdout.write(tok);
1805
2078
  agentContent += tok;
1806
2079
  hasOutput = true;
1807
2080
  } else if (event.type === "cli_tool_request" && localToolsCfg) {
2081
+ if (thinkingSpinner.isSpinning) thinkingSpinner.stop();
1808
2082
  const { requestId, tool: toolName, args } = event;
1809
2083
  const displayName = String(toolName).replace("cli_", "");
2084
+ const argStr = toolArgSummary(displayName, args ?? {});
2085
+ const isWaiting = displayName === "wait_for_process";
1810
2086
  if (hasOutput) {
1811
2087
  process.stdout.write("\n");
1812
2088
  hasOutput = false;
1813
2089
  }
1814
- process.stdout.write(c.dim(` [local] ${displayName}\u2026`));
2090
+ process.stdout.write(` ${c.dim("\u2500")} ${c.bold(displayName)}${argStr ? " " + c.dim(argStr) : ""}`);
2091
+ const startMs = Date.now();
2092
+ let elapsedSec = 0;
2093
+ let elapsedInterval = null;
2094
+ if (isWaiting) {
2095
+ elapsedInterval = setInterval(() => {
2096
+ elapsedSec++;
2097
+ readline3.cursorTo(process.stdout, 0);
2098
+ process.stdout.write(` ${c.dim("\u2500")} ${c.bold(displayName)}${argStr ? " " + c.dim(argStr) : ""} ${c.dim(elapsedSec + "s\u2026")}`);
2099
+ }, 1e3);
2100
+ }
1815
2101
  let result;
2102
+ let toolOk = true;
1816
2103
  try {
1817
- const localToolName = String(toolName).replace("cli_", "");
1818
- result = await runLocalTool({ tool: localToolName, args: args ?? {} }, localToolsCfg);
1819
- process.stdout.write(c.dim(" \u2713\n"));
2104
+ result = await runLocalTool({ tool: displayName, args: args ?? {} }, localToolsCfg);
1820
2105
  } catch (err) {
1821
2106
  result = `Error: ${err?.message ?? String(err)}`;
1822
- process.stdout.write(c.dim(" \u2717\n"));
2107
+ toolOk = false;
1823
2108
  }
2109
+ if (elapsedInterval) clearInterval(elapsedInterval);
2110
+ const elapsed = ((Date.now() - startMs) / 1e3).toFixed(1);
2111
+ const preview = toolOk ? toolResultPreview(displayName, result) : "";
2112
+ readline3.cursorTo(process.stdout, 0);
2113
+ readline3.clearLine(process.stdout, 0);
2114
+ const statusIcon = toolOk ? sym.ok : sym.fail;
2115
+ const previewPart = preview ? ` ${c.dim(preview)}` : "";
2116
+ process.stdout.write(` ${c.dim("\u2500")} ${c.bold(displayName)}${argStr ? " " + c.dim(argStr) : ""} ${statusIcon}${previewPart} ${c.dim("(" + elapsed + "s)")}
2117
+ `);
1824
2118
  appendSessionLog(sessionId, {
1825
2119
  type: "local_tool_result",
1826
2120
  tool: toolName,
@@ -1840,13 +2134,20 @@ ${sym.fail} ${c.error(err.message ?? String(err))}`);
1840
2134
  console.error(c.warn(`
1841
2135
  [local] Failed to submit tool result: ${postErr?.message}`));
1842
2136
  }
2137
+ } else if (event.type === "keepalive") {
1843
2138
  } else if (event.type === "toolStart") {
1844
- const name = event.toolName ?? "";
2139
+ if (thinkingSpinner.isSpinning) thinkingSpinner.stop();
2140
+ lastToolName = event.toolName ?? "";
2141
+ toolStartMs = Date.now();
1845
2142
  if (hasOutput) process.stdout.write("\n");
1846
- process.stdout.write(c.dim(` [tool] ${name}\u2026`));
2143
+ process.stdout.write(` ${c.dim("\u2500")} ${c.bold(lastToolName)}`);
1847
2144
  hasOutput = false;
1848
2145
  } else if (event.type === "toolEnd") {
1849
- process.stdout.write(c.dim(" done\n"));
2146
+ const elapsed = ((Date.now() - toolStartMs) / 1e3).toFixed(1);
2147
+ readline3.cursorTo(process.stdout, 0);
2148
+ readline3.clearLine(process.stdout, 0);
2149
+ process.stdout.write(` ${c.dim("\u2500")} ${c.bold(lastToolName)} ${sym.ok} ${c.dim("(" + elapsed + "s)")}
2150
+ `);
1850
2151
  process.stdout.write(c.primary("agent") + c.dim(" \u203A "));
1851
2152
  hasOutput = false;
1852
2153
  } else if (event.type === "final") {
@@ -1883,12 +2184,14 @@ ${sym.fail} ${c.error(err.message ?? String(err))}`);
1883
2184
  }
1884
2185
  break;
1885
2186
  } else if (event.type === "error") {
2187
+ if (thinkingSpinner.isSpinning) thinkingSpinner.stop();
1886
2188
  if (hasOutput) process.stdout.write("\n");
1887
2189
  console.error(`
1888
2190
  ${sym.fail} ${c.error(event.message ?? "Stream error")}`);
1889
2191
  break;
1890
2192
  }
1891
2193
  }
2194
+ if (thinkingSpinner.isSpinning) thinkingSpinner.stop();
1892
2195
  process.stdout.write("\n");
1893
2196
  if (localToolsCfg && agentContent) {
1894
2197
  await handleLocalToolLoop(agentContent, localToolsCfg, client, agentId, sessionId, appendSessionLog);
@@ -1899,6 +2202,8 @@ ${sym.fail} ${c.error(event.message ?? "Stream error")}`);
1899
2202
  }
1900
2203
  }
1901
2204
  console.log();
2205
+ readline3.cursorTo(process.stdout, 0);
2206
+ readline3.clearLine(process.stdout, 0);
1902
2207
  rl.resume();
1903
2208
  rl.prompt();
1904
2209
  });
@@ -1922,16 +2227,25 @@ async function handleLocalToolLoop(agentText, cfg, client, agentId, sessionId, a
1922
2227
  }
1923
2228
  const toolCall = extractToolCall(agentText);
1924
2229
  if (!toolCall) return;
1925
- process.stdout.write(c.dim(`
1926
- [local] ${toolCall.tool}`));
2230
+ const argStr = toolArgSummary(toolCall.tool, toolCall.args ?? {});
2231
+ process.stdout.write(`
2232
+ ${c.dim("\u2500")} ${c.bold(toolCall.tool)}${argStr ? " " + c.dim(argStr) : ""}`);
2233
+ const startMs = Date.now();
1927
2234
  let result;
2235
+ let toolOk = true;
1928
2236
  try {
1929
2237
  result = await runLocalTool(toolCall, cfg);
1930
- process.stdout.write(c.dim(" \u2713\n"));
1931
2238
  } catch (err) {
1932
2239
  result = `Error: ${err?.message ?? String(err)}`;
1933
- process.stdout.write(c.dim(" \u2717\n"));
2240
+ toolOk = false;
1934
2241
  }
2242
+ const elapsed = ((Date.now() - startMs) / 1e3).toFixed(1);
2243
+ const preview = toolOk ? toolResultPreview(toolCall.tool, result) : "";
2244
+ readline3.cursorTo(process.stdout, 0);
2245
+ readline3.clearLine(process.stdout, 0);
2246
+ const previewPart = preview ? ` ${c.dim(preview)}` : "";
2247
+ process.stdout.write(` ${c.dim("\u2500")} ${c.bold(toolCall.tool)}${argStr ? " " + c.dim(argStr) : ""} ${toolOk ? sym.ok : sym.fail}${previewPart} ${c.dim("(" + elapsed + "s)")}
2248
+ `);
1935
2249
  const resultMsg = `[Tool result: ${toolCall.tool}]
1936
2250
  \`\`\`
1937
2251
  ${result}
@@ -1946,6 +2260,8 @@ ${result}
1946
2260
  process.stdout.write(c.primary("agent") + c.dim(" \u203A "));
1947
2261
  let followContent = "";
1948
2262
  try {
2263
+ let loopToolName = "";
2264
+ let loopToolStartMs = 0;
1949
2265
  for await (const evt of client.agents.stream({
1950
2266
  agentId,
1951
2267
  sessionId,
@@ -1956,11 +2272,16 @@ ${result}
1956
2272
  process.stdout.write(tok);
1957
2273
  followContent += tok;
1958
2274
  } else if (evt.type === "toolStart") {
1959
- const name = evt.toolName ?? "";
2275
+ loopToolName = evt.toolName ?? "";
2276
+ loopToolStartMs = Date.now();
1960
2277
  if (followContent) process.stdout.write("\n");
1961
- process.stdout.write(c.dim(` [tool] ${name}\u2026`));
2278
+ process.stdout.write(` ${c.dim("\u2500")} ${c.bold(loopToolName)}`);
1962
2279
  } else if (evt.type === "toolEnd") {
1963
- process.stdout.write(c.dim(" done\n"));
2280
+ const elapsed2 = ((Date.now() - loopToolStartMs) / 1e3).toFixed(1);
2281
+ readline3.cursorTo(process.stdout, 0);
2282
+ readline3.clearLine(process.stdout, 0);
2283
+ process.stdout.write(` ${c.dim("\u2500")} ${c.bold(loopToolName)} ${sym.ok} ${c.dim("(" + elapsed2 + "s)")}
2284
+ `);
1964
2285
  process.stdout.write(c.primary("agent") + c.dim(" \u203A "));
1965
2286
  } else if (evt.type === "final") {
1966
2287
  const txt = extractText(evt?.payload);
@@ -1984,6 +2305,80 @@ ${sym.fail} ${c.error(evt.message ?? "Stream error")}`);
1984
2305
  }
1985
2306
  await handleLocalToolLoop(followContent, cfg, client, agentId, sessionId, appendLog, depth + 1);
1986
2307
  }
2308
+ function truncate(s, max) {
2309
+ const str = String(s ?? "");
2310
+ return str.length <= max ? str : str.slice(0, max - 1) + "\u2026";
2311
+ }
2312
+ function toolArgSummary(toolName, args) {
2313
+ switch (toolName) {
2314
+ case "read_file":
2315
+ return truncate(args.path ?? "", 60);
2316
+ case "write_file":
2317
+ return truncate(args.path ?? "", 60);
2318
+ case "delete_file":
2319
+ return truncate(args.path ?? "", 60);
2320
+ case "list_directory":
2321
+ return truncate(args.path ?? ".", 60);
2322
+ case "run_command":
2323
+ return truncate(args.command ?? "", 60);
2324
+ case "start_process":
2325
+ return truncate(args.command ?? "", 60);
2326
+ case "wait_for_process":
2327
+ return truncate(args.process_id ?? "", 20);
2328
+ case "process_status":
2329
+ return truncate(args.process_id ?? "", 20);
2330
+ case "kill_process":
2331
+ return truncate(args.process_id ?? "", 20);
2332
+ case "list_processes":
2333
+ return "";
2334
+ case "search_files": {
2335
+ const parts = [args.pattern, args.query].filter(Boolean);
2336
+ return truncate(parts.join(" "), 60);
2337
+ }
2338
+ default: {
2339
+ const first = args.path ?? args.query ?? args.command ?? args.pattern ?? "";
2340
+ return truncate(String(first), 60);
2341
+ }
2342
+ }
2343
+ }
2344
+ function toolResultPreview(toolName, result) {
2345
+ if (!result || result.startsWith("Error:")) return "";
2346
+ switch (toolName) {
2347
+ case "read_file": {
2348
+ const lines = result.split("\n").length;
2349
+ return `${lines} lines`;
2350
+ }
2351
+ case "write_file":
2352
+ return "written";
2353
+ case "delete_file":
2354
+ return "deleted";
2355
+ case "list_directory": {
2356
+ const count = result.split("\n").filter(Boolean).length;
2357
+ return `${count} entries`;
2358
+ }
2359
+ case "run_command": {
2360
+ const first = result.split("\n").find((l) => l.trim());
2361
+ return first ? truncate(first.trim(), 50) : "done";
2362
+ }
2363
+ case "start_process": {
2364
+ const match = result.match(/process[_\s-]?id[:\s]+([a-zA-Z0-9_-]+)/i) ?? result.match(/"id"[:\s]+"([^"]+)"/);
2365
+ return match ? `pid ${match[1]}` : "started";
2366
+ }
2367
+ case "wait_for_process": {
2368
+ if (/done|complete|exit/i.test(result)) return "done";
2369
+ if (/running/i.test(result)) return "still running";
2370
+ return truncate(result.split("\n")[0]?.trim() ?? "", 40);
2371
+ }
2372
+ case "search_files": {
2373
+ const count = result.split("\n").filter(Boolean).length;
2374
+ return `${count} match${count === 1 ? "" : "es"}`;
2375
+ }
2376
+ default: {
2377
+ const first = result.split("\n").find((l) => l.trim());
2378
+ return first ? truncate(first.trim(), 50) : "";
2379
+ }
2380
+ }
2381
+ }
1987
2382
  function extractText(payload) {
1988
2383
  if (!payload) return "";
1989
2384
  if (typeof payload === "string") return payload;
@@ -3279,7 +3674,7 @@ async function pickAgentInteractively(action) {
3279
3674
  return agentId;
3280
3675
  }
3281
3676
  var program = new import_commander16.Command();
3282
- program.name("agc").description("Agent Commons CLI \u2014 interact with the Agent Commons platform").version("0.1.14", "-v, --version").action(async () => {
3677
+ program.name("agc").description("Agent Commons CLI \u2014 interact with the Agent Commons platform").version("0.1.17", "-v, --version").action(async () => {
3283
3678
  await interactiveMenu();
3284
3679
  });
3285
3680
  program.addCommand(loginCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-commons/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.17",
4
4
  "description": "Agent Commons CLI — chat, run, and manage agents from your terminal",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -11,15 +11,17 @@
11
11
  "dist"
12
12
  ],
13
13
  "dependencies": {
14
- "commander": "^12.1.0",
14
+ "@types/pdf-parse": "^1.1.5",
15
15
  "chalk": "^5.3.0",
16
+ "commander": "^12.1.0",
16
17
  "ora": "^8.1.1",
17
- "@agent-commons/sdk": "0.1.12"
18
+ "pdf-parse": "^1.1.1",
19
+ "@agent-commons/sdk": "0.1.13"
18
20
  },
19
21
  "devDependencies": {
22
+ "@types/node": "^22.10.2",
20
23
  "tsup": "^8.3.5",
21
- "typescript": "^5.7.2",
22
- "@types/node": "^22.10.2"
24
+ "typescript": "^5.7.2"
23
25
  },
24
26
  "engines": {
25
27
  "node": ">=18"