@agent-commons/cli 0.1.14 → 0.1.16

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 +261 -17
  2. package/package.json +1 -1
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.16") {
110
110
  const line = import_chalk.default.cyan(" \u2500".padEnd(2) + "\u2500".repeat(44));
111
111
  console.log("");
112
112
  console.log(line);
@@ -1257,6 +1257,12 @@ 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 managedProcesses = /* @__PURE__ */ new Map();
1261
+ function capBuffer(existing, chunk, maxBytes) {
1262
+ const joined = existing + chunk;
1263
+ if (joined.length <= maxBytes) return joined;
1264
+ return "\u2026(truncated)\n" + joined.slice(-(maxBytes - 20));
1265
+ }
1260
1266
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".cache", "__pycache__", ".next", "dist", "build", ".DS_Store"]);
1261
1267
  function buildDirSnapshot(dir, maxDepth = 2) {
1262
1268
  const lines = [`${dir}/`];
@@ -1335,27 +1341,59 @@ ${fileSection}
1335
1341
 
1336
1342
  | Tool | What it does |
1337
1343
  |------|-------------|
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 |
1344
+ | \`cli_list_directory\` | List files and folders at a path |
1345
+ | \`cli_read_file\` | Read a file (PDF and Word docs are extracted to text) |
1340
1346
  | \`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) |
1347
+ | \`cli_search_files\` | Find files matching a pattern |
1348
+ | \`cli_run_command\` | Run a short command and return its output (user confirmation required) |
1349
+ | \`cli_start_process\` | Start a long-running command in the background; returns a processId immediately |
1350
+ | \`cli_wait_for_process\` | Block up to N seconds for a background process, then return current output |
1351
+ | \`cli_process_status\` | Instant non-blocking check on a background process |
1352
+ | \`cli_kill_process\` | Kill a running background process |
1353
+ | \`cli_list_processes\` | List all background processes started this session |
1354
+
1355
+ ### Choosing between run_command and start_process
1356
+
1357
+ | Situation | Use |
1358
+ |-----------|-----|
1359
+ | Command finishes in under ~30s | \`cli_run_command\` |
1360
+ | Command may take minutes (npm install, build, scaffold) | \`cli_start_process\` + \`cli_wait_for_process\` |
1361
+ | Command needs live stdin (e.g. a REPL) | \`cli_run_command\` with \`"interactive": true\` |
1343
1362
 
1344
- ### Example \u2014 listing a directory
1363
+ ### run_command options
1364
+ - \`timeout_seconds\` (default 120, max 300) \u2014 kill the process after N seconds
1365
+ - \`interactive\` (boolean) \u2014 connects the user's terminal stdin for commands that need input
1345
1366
 
1346
- When the user asks "what's on my desktop?", call \`cli_list_directory\` with \`{"path": "Desktop"}\` immediately. Then show the result.
1367
+ ### start_process + wait_for_process pattern
1347
1368
 
1348
- ### Example \u2014 reading a file
1369
+ For long commands like \`npx create-next-app@latest my-app --yes\`:
1349
1370
 
1350
- Call \`cli_read_file\` with \`{"path": "Desktop/notes.txt"}\`. Then quote the content in your reply.
1371
+ 1. Call \`cli_start_process\` \u2014 returns \`{processId, status: "running"}\` immediately. Tell the user it has started.
1372
+ 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.
1373
+ 3. Repeat step 2 until \`status\` is \`"done"\` or \`"error"\`.
1374
+ 4. Report the final output to the user.
1351
1375
 
1352
- ### Example \u2014 writing a file
1376
+ Never hold the user in silence. Between each \`cli_wait_for_process\` call, tell them what you saw so far.
1377
+
1378
+ ### Example \u2014 scaffolding a Next.js project
1379
+
1380
+ \`\`\`
1381
+ cli_start_process: {"command": "npx", "args": ["create-next-app@latest", "my-app", "--yes"], "cwd": "Desktop"}
1382
+ \u2192 {processId: "proc_1a2b", status: "running"}
1353
1383
 
1354
- Call \`cli_write_file\` with \`{"path": "output.txt", "content": "Hello"}\`. The user will be prompted to confirm.
1384
+ Tell user: "Started! Installing dependencies, this takes a minute or two. Checking in 60s\u2026"
1355
1385
 
1356
- ### Example \u2014 running a command
1386
+ cli_wait_for_process: {"processId": "proc_1a2b", "wait_seconds": 60}
1387
+ \u2192 {status: "running", elapsedSec: 60, stdout: "Creating project...
1388
+ Installing packages\u2026"}
1357
1389
 
1358
- Call \`cli_run_command\` with \`{"command": "ls", "args": ["-la"]}\`. The user will be prompted to confirm.
1390
+ Tell user: "Still installing \u2014 here's output so far: [stdout]. Checking again\u2026"
1391
+
1392
+ cli_wait_for_process: {"processId": "proc_1a2b", "wait_seconds": 60}
1393
+ \u2192 {status: "done", exitCode: 0, elapsedSec: 93, stdout: "Success! Created my-app"}
1394
+
1395
+ Tell user: "Done! Project created in Desktop/my-app"
1396
+ \`\`\`
1359
1397
  `;
1360
1398
  }
1361
1399
  var TOOL_CALL_RE = /```tool\s*\n([\s\S]*?)\n```/;
@@ -1421,6 +1459,61 @@ async function confirm(message, config, permissionKey) {
1421
1459
  });
1422
1460
  });
1423
1461
  }
1462
+ var OFFICE_EXTS = /* @__PURE__ */ new Set([".docx", ".doc", ".rtf", ".odt", ".pages"]);
1463
+ var PDF_EXTS = /* @__PURE__ */ new Set([".pdf"]);
1464
+ var UNREADABLE_BINARY_EXTS = /* @__PURE__ */ new Set([
1465
+ ".png",
1466
+ ".jpg",
1467
+ ".jpeg",
1468
+ ".gif",
1469
+ ".bmp",
1470
+ ".ico",
1471
+ ".webp",
1472
+ ".tiff",
1473
+ ".mp3",
1474
+ ".mp4",
1475
+ ".wav",
1476
+ ".aac",
1477
+ ".ogg",
1478
+ ".flac",
1479
+ ".zip",
1480
+ ".tar",
1481
+ ".gz",
1482
+ ".bz2",
1483
+ ".7z",
1484
+ ".rar",
1485
+ ".exe",
1486
+ ".dll",
1487
+ ".so",
1488
+ ".dylib",
1489
+ ".bin",
1490
+ ".psd",
1491
+ ".ai",
1492
+ ".sketch",
1493
+ ".figma",
1494
+ ".xlsx",
1495
+ ".xls",
1496
+ ".pptx",
1497
+ ".ppt"
1498
+ ]);
1499
+ function extractViaCommand(cmd, cmdArgs) {
1500
+ return new Promise((res) => {
1501
+ (0, import_child_process2.execFile)(cmd, cmdArgs, { timeout: 3e4, maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
1502
+ if (err) res("");
1503
+ else res(stdout.trim());
1504
+ });
1505
+ });
1506
+ }
1507
+ async function extractPdfText(abs) {
1508
+ const text = await extractViaCommand("pdftotext", [abs, "-"]);
1509
+ if (text) return text;
1510
+ return `[Cannot extract PDF text: pdftotext not found. Install with: brew install poppler]`;
1511
+ }
1512
+ async function extractOfficeText(abs, ext) {
1513
+ const text = await extractViaCommand("textutil", ["-stdout", "-cat", "txt", abs]);
1514
+ if (text) return text;
1515
+ return `[Cannot extract ${ext} text: textutil failed or is unavailable on this system]`;
1516
+ }
1424
1517
  async function toolReadFile(args, cfg) {
1425
1518
  const { path: userPath } = args;
1426
1519
  if (!userPath) throw new Error('read_file requires a "path" argument');
@@ -1430,6 +1523,12 @@ async function toolReadFile(args, cfg) {
1430
1523
  const stat = (0, import_fs3.statSync)(abs);
1431
1524
  if (stat.isDirectory()) throw new Error(`"${userPath}" is a directory, not a file`);
1432
1525
  if (stat.size > 5e5) throw new Error(`File too large to read (${Math.round(stat.size / 1024)} KB). Max 500 KB.`);
1526
+ const ext = (0, import_path3.extname)(abs).toLowerCase();
1527
+ if (PDF_EXTS.has(ext)) return extractPdfText(abs);
1528
+ if (OFFICE_EXTS.has(ext)) return extractOfficeText(abs, ext);
1529
+ if (UNREADABLE_BINARY_EXTS.has(ext)) {
1530
+ throw new Error(`Cannot read binary file "${userPath}" (${ext} format). Only text, PDF, and Word documents are supported.`);
1531
+ }
1433
1532
  return (0, import_fs3.readFileSync)(abs, "utf8");
1434
1533
  }
1435
1534
  async function toolWriteFile(args, cfg) {
@@ -1487,11 +1586,12 @@ async function toolSearchFiles(args, cfg) {
1487
1586
  return results.length ? results.join("\n") : "No files found matching: " + pattern;
1488
1587
  }
1489
1588
  async function toolRunCommand(args, cfg) {
1490
- const { command, args: cmdArgs = [], cwd } = args;
1589
+ const { command, args: cmdArgs = [], cwd, timeout_seconds, interactive } = args;
1491
1590
  if (!command || typeof command !== "string") throw new Error('run_command requires a "command" string');
1492
1591
  if (!Array.isArray(cmdArgs)) throw new Error('"args" must be an array of strings');
1493
1592
  const workDir = cwd ? safePath(cfg.rootDir, cwd) : cfg.rootDir;
1494
1593
  const preview = [command, ...cmdArgs].join(" ");
1594
+ const timeoutMs = Math.min((typeof timeout_seconds === "number" ? timeout_seconds : 120) * 1e3, 3e5);
1495
1595
  const ok = await confirm(
1496
1596
  `Agent wants to run: \x1B[1m${preview}\x1B[0m
1497
1597
  \x1B[2min: ${workDir}\x1B[0m`,
@@ -1499,14 +1599,142 @@ async function toolRunCommand(args, cfg) {
1499
1599
  "run_command"
1500
1600
  );
1501
1601
  if (!ok) return "User denied command execution.";
1602
+ if (interactive) {
1603
+ return new Promise((resolve2) => {
1604
+ const child = (0, import_child_process2.spawn)(command, cmdArgs.map(String), { cwd: workDir, stdio: "inherit" });
1605
+ const timer = setTimeout(() => {
1606
+ child.kill();
1607
+ resolve2(`(command timed out after ${timeoutMs / 1e3}s)`);
1608
+ }, timeoutMs);
1609
+ child.on("close", (code) => {
1610
+ clearTimeout(timer);
1611
+ resolve2(`(command exited with code ${code ?? "unknown"})`);
1612
+ });
1613
+ child.on("error", (err) => {
1614
+ clearTimeout(timer);
1615
+ resolve2(`Error: ${err.message}`);
1616
+ });
1617
+ });
1618
+ }
1502
1619
  return new Promise((resolve2) => {
1503
- (0, import_child_process2.execFile)(command, cmdArgs.map(String), { cwd: workDir, timeout: 3e4, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
1620
+ (0, import_child_process2.execFile)(command, cmdArgs.map(String), { cwd: workDir, timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
1504
1621
  const out = [stdout, stderr].filter(Boolean).join("\n--- stderr ---\n");
1505
1622
  if (err && !out) return resolve2(`Error: ${err.message}`);
1506
1623
  resolve2(out || "(no output)");
1507
1624
  });
1508
1625
  });
1509
1626
  }
1627
+ async function toolStartProcess(args, cfg) {
1628
+ const { command, args: cmdArgs = [], cwd } = args;
1629
+ if (!command || typeof command !== "string") throw new Error('start_process requires a "command" string');
1630
+ if (!Array.isArray(cmdArgs)) throw new Error('"args" must be an array of strings');
1631
+ const workDir = cwd ? safePath(cfg.rootDir, cwd) : cfg.rootDir;
1632
+ const preview = [command, ...cmdArgs].join(" ");
1633
+ const ok = await confirm(
1634
+ `Agent wants to start background process: \x1B[1m${preview}\x1B[0m
1635
+ \x1B[2min: ${workDir}\x1B[0m`,
1636
+ cfg,
1637
+ "start_process"
1638
+ );
1639
+ if (!ok) return JSON.stringify({ error: "User denied process start." });
1640
+ const id = `proc_${Date.now().toString(36)}`;
1641
+ const child = (0, import_child_process2.spawn)(command, cmdArgs.map(String), {
1642
+ cwd: workDir,
1643
+ stdio: ["ignore", "pipe", "pipe"],
1644
+ detached: false
1645
+ });
1646
+ const proc = {
1647
+ id,
1648
+ command: preview,
1649
+ status: "running",
1650
+ exitCode: null,
1651
+ stdout: "",
1652
+ stderr: "",
1653
+ startedAt: /* @__PURE__ */ new Date(),
1654
+ endedAt: null,
1655
+ child
1656
+ };
1657
+ child.stdout?.on("data", (chunk) => {
1658
+ proc.stdout = capBuffer(proc.stdout, chunk.toString(), 2e5);
1659
+ });
1660
+ child.stderr?.on("data", (chunk) => {
1661
+ proc.stderr = capBuffer(proc.stderr, chunk.toString(), 5e4);
1662
+ });
1663
+ child.on("close", (code) => {
1664
+ proc.status = code === 0 ? "done" : "error";
1665
+ proc.exitCode = code;
1666
+ proc.endedAt = /* @__PURE__ */ new Date();
1667
+ });
1668
+ child.on("error", (err) => {
1669
+ proc.status = "error";
1670
+ proc.endedAt = /* @__PURE__ */ new Date();
1671
+ proc.stderr = capBuffer(proc.stderr, `
1672
+ Spawn error: ${err.message}`, 5e4);
1673
+ });
1674
+ managedProcesses.set(id, proc);
1675
+ cfg.appendLog({ type: "process_start", processId: id, command: preview, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1676
+ return JSON.stringify({ processId: id, status: "running", command: preview });
1677
+ }
1678
+ function processSnapshot(proc) {
1679
+ const elapsedSec = Math.round((Date.now() - proc.startedAt.getTime()) / 1e3);
1680
+ const recentStdout = proc.stdout.length > 4e3 ? "\u2026(earlier output truncated)\n" + proc.stdout.slice(-4e3) : proc.stdout;
1681
+ return JSON.stringify({
1682
+ processId: proc.id,
1683
+ command: proc.command,
1684
+ status: proc.status,
1685
+ exitCode: proc.exitCode,
1686
+ elapsedSec,
1687
+ stdout: recentStdout || "(no output yet)",
1688
+ stderr: proc.stderr.slice(-1e3) || void 0
1689
+ });
1690
+ }
1691
+ async function toolProcessStatus(args, _cfg) {
1692
+ const { processId } = args;
1693
+ if (!processId) throw new Error('process_status requires a "processId" argument');
1694
+ const proc = managedProcesses.get(processId);
1695
+ if (!proc) return JSON.stringify({ error: `No process found with id "${processId}"` });
1696
+ return processSnapshot(proc);
1697
+ }
1698
+ async function toolWaitForProcess(args, _cfg) {
1699
+ const { processId, wait_seconds = 60 } = args;
1700
+ if (!processId) throw new Error('wait_for_process requires a "processId" argument');
1701
+ const proc = managedProcesses.get(processId);
1702
+ if (!proc) return JSON.stringify({ error: `No process found with id "${processId}"` });
1703
+ if (proc.status !== "running") return processSnapshot(proc);
1704
+ const maxWait = Math.min((typeof wait_seconds === "number" ? wait_seconds : 60) * 1e3, 12e4);
1705
+ const deadline = Date.now() + maxWait;
1706
+ await new Promise((resolve2) => {
1707
+ const tick = setInterval(() => {
1708
+ if (proc.status !== "running" || Date.now() >= deadline) {
1709
+ clearInterval(tick);
1710
+ resolve2();
1711
+ }
1712
+ }, 500);
1713
+ });
1714
+ return processSnapshot(proc);
1715
+ }
1716
+ async function toolKillProcess(args, cfg) {
1717
+ const { processId } = args;
1718
+ if (!processId) throw new Error('kill_process requires a "processId" argument');
1719
+ const proc = managedProcesses.get(processId);
1720
+ if (!proc) return JSON.stringify({ error: `No process found with id "${processId}"` });
1721
+ if (proc.status !== "running") return JSON.stringify({ error: `Process "${processId}" is not running (status: ${proc.status})` });
1722
+ proc.child.kill("SIGTERM");
1723
+ proc.status = "killed";
1724
+ proc.endedAt = /* @__PURE__ */ new Date();
1725
+ cfg.appendLog({ type: "process_killed", processId, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1726
+ return JSON.stringify({ processId, status: "killed" });
1727
+ }
1728
+ async function toolListProcesses(_args, _cfg) {
1729
+ if (managedProcesses.size === 0) return JSON.stringify([]);
1730
+ const list = [...managedProcesses.values()].map((p) => ({
1731
+ processId: p.id,
1732
+ command: p.command,
1733
+ status: p.status,
1734
+ elapsedSec: Math.round((Date.now() - p.startedAt.getTime()) / 1e3)
1735
+ }));
1736
+ return JSON.stringify(list);
1737
+ }
1510
1738
  async function runLocalTool(call, cfg) {
1511
1739
  const { tool, args } = call;
1512
1740
  cfg.appendLog({
@@ -1533,8 +1761,23 @@ async function runLocalTool(call, cfg) {
1533
1761
  case "run_command":
1534
1762
  result = await toolRunCommand(args, cfg);
1535
1763
  break;
1764
+ case "start_process":
1765
+ result = await toolStartProcess(args, cfg);
1766
+ break;
1767
+ case "process_status":
1768
+ result = await toolProcessStatus(args, cfg);
1769
+ break;
1770
+ case "wait_for_process":
1771
+ result = await toolWaitForProcess(args, cfg);
1772
+ break;
1773
+ case "kill_process":
1774
+ result = await toolKillProcess(args, cfg);
1775
+ break;
1776
+ case "list_processes":
1777
+ result = await toolListProcesses(args, cfg);
1778
+ break;
1536
1779
  default:
1537
- result = `Unknown tool: "${tool}". Available: read_file, write_file, list_directory, search_files, run_command`;
1780
+ 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
1781
  }
1539
1782
  } catch (err) {
1540
1783
  result = `Error: ${err?.message ?? String(err)}`;
@@ -1840,6 +2083,7 @@ ${sym.fail} ${c.error(err.message ?? String(err))}`);
1840
2083
  console.error(c.warn(`
1841
2084
  [local] Failed to submit tool result: ${postErr?.message}`));
1842
2085
  }
2086
+ } else if (event.type === "ping") {
1843
2087
  } else if (event.type === "toolStart") {
1844
2088
  const name = event.toolName ?? "";
1845
2089
  if (hasOutput) process.stdout.write("\n");
@@ -3279,7 +3523,7 @@ async function pickAgentInteractively(action) {
3279
3523
  return agentId;
3280
3524
  }
3281
3525
  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 () => {
3526
+ program.name("agc").description("Agent Commons CLI \u2014 interact with the Agent Commons platform").version("0.1.16", "-v, --version").action(async () => {
3283
3527
  await interactiveMenu();
3284
3528
  });
3285
3529
  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.16",
4
4
  "description": "Agent Commons CLI — chat, run, and manage agents from your terminal",
5
5
  "license": "MIT",
6
6
  "bin": {