@acnlabs/acn-cli 0.14.0 → 0.14.1

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 (3) hide show
  1. package/README.md +18 -0
  2. package/dist/index.js +439 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -89,6 +89,24 @@ Semantics: CLI answers `message/send` / `message/stream` with a valid A2A
89
89
  wake the host. Wake failure is logged (`wake_failed`) and does **not** fail
90
90
  the A2A reply. Dedupe is on by default (`--no-dedupe` to disable).
91
91
 
92
+ **Chat writeback (Interfaze / Chat Gateway):** when the relayed message carries
93
+ `metadata.agentplanet.chat_id` + `reply_path`, enable the CLI to complete a
94
+ host reply and POST `agent-messages` (hosts only return `{"content":"..."}`):
95
+
96
+ ```bash
97
+ export AGENTPLANET_API_BASE=https://api.example.com
98
+ export AGENTPLANET_INTERNAL_TOKEN=… # or AGENTPLANET_INTERNAL_API_TOKEN
99
+
100
+ acn listen --runtime http \
101
+ --wake-url http://127.0.0.1:10122/hooks/agent \
102
+ --chat-writeback \
103
+ --chat-complete-url http://127.0.0.1:10122/chat/complete
104
+ # or: --chat-complete-exec 'python3 /path/to/complete.py'
105
+ ```
106
+
107
+ Task / Org wakes still use `--wake-url` / `--wake-exec`. Chat envelopes skip
108
+ wake and use the complete → writeback path instead.
109
+
92
110
  **Coverage:** only traffic that arrives over the Mode B relay. Open Task Pool
93
111
  rows that were never pushed as A2A still need `acn tasks list` / reconcile.
94
112
 
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@acnlabs/acn-cli",
34
- version: "0.14.0",
34
+ version: "0.14.1",
35
35
  description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -608,6 +608,8 @@ function formatTask(t) {
608
608
  lines.push(` Reward : ${t.reward} ${t.reward_currency ?? ""}`);
609
609
  }
610
610
  if (t.subnet_slug) lines.push(` Subnet : ${t.subnet_slug}`);
611
+ const orgId = t.metadata?.org_id;
612
+ if (typeof orgId === "string" && orgId) lines.push(` Org : ${orgId}`);
611
613
  if (t.description) lines.push(` Desc : ${t.description.slice(0, 120)}`);
612
614
  if (t.created_at) lines.push(` Created : ${t.created_at}`);
613
615
  if (t.deadline) lines.push(` Deadline : ${t.deadline}`);
@@ -697,7 +699,10 @@ ${formatTask(t)}`).join("\n\n")
697
699
  handleError(err);
698
700
  }
699
701
  });
700
- cmd.command("create").description("Create a new task (as agent)").requiredOption("-t, --title <title>", "Task title (min 3 chars)").requiredOption("-d, --description <text>", "Task description (min 10 chars)").requiredOption("--tags <tags>", "Required skill tags, comma-separated (e.g. coding,review)").option("--deadline <hours>", "Deadline in hours (default: 48)", "48").option("--reward <amount>", "Reward amount (default: 0)", "0").option("--currency <currency>", "Reward currency (e.g. USD, USDC, ap_points)", "ap_points").option("--type <type>", "Task type (e.g. coding, general)", "general").option("--max-participants <n>", "Max participants (default: 1)", "1").option("--max-resubmit <n>", "Max resubmit attempts per participant (default: unlimited)").option("--subnet <slug>", "Subnet slug to scope the task to (agent must be a member)").action(
702
+ cmd.command("create").description("Create a new task (as agent)").requiredOption("-t, --title <title>", "Task title (min 3 chars)").requiredOption("-d, --description <text>", "Task description (min 10 chars)").requiredOption("--tags <tags>", "Required skill tags, comma-separated (e.g. coding,review)").option("--deadline <hours>", "Deadline in hours (default: 48)", "48").option("--reward <amount>", "Reward amount (default: 0)", "0").option("--currency <currency>", "Reward currency (e.g. USD, USDC, ap_points)", "ap_points").option("--type <type>", "Task type (e.g. coding, general)", "general").option("--max-participants <n>", "Max participants (default: 1)", "1").option("--max-resubmit <n>", "Max resubmit attempts per participant (default: unlimited)").option("--subnet <slug>", "Subnet slug to scope the task to (agent must be a member)").option(
703
+ "--org-id <org_id>",
704
+ "Attribute to an Org (metadata.org_id + org_publish; prefer `acn org publish-task`)"
705
+ ).action(
701
706
  async (opts) => {
702
707
  const config = loadConfig();
703
708
  if (!config.api_key) {
@@ -722,6 +727,9 @@ ${formatTask(t)}`).join("\n\n")
722
727
  if (opts.subnet) {
723
728
  body.subnet_slug = opts.subnet;
724
729
  }
730
+ if (opts.orgId) {
731
+ body.metadata = { org_id: opts.orgId, org_publish: true };
732
+ }
725
733
  try {
726
734
  const task = await acnPost("/tasks/agent/create", body);
727
735
  output(task, [`Task created!
@@ -1375,10 +1383,14 @@ ${formatPolicy(res)}`);
1375
1383
 
1376
1384
  // src/commands/listen.ts
1377
1385
  var import_commander10 = require("commander");
1378
- var import_child_process2 = require("child_process");
1386
+ var import_child_process3 = require("child_process");
1379
1387
  var import_ws = __toESM(require("ws"));
1380
1388
 
1389
+ // src/commands/chat-writeback.ts
1390
+ var import_child_process = require("child_process");
1391
+
1381
1392
  // src/commands/normalize-event.ts
1393
+ var CHAT_REPLY_CHANNEL = "agentplanet.chat";
1382
1394
  function asRecord(v) {
1383
1395
  return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
1384
1396
  }
@@ -1430,6 +1442,44 @@ function extractFromAgent(message) {
1430
1442
  if (!metadata) return null;
1431
1443
  return asNonEmptyString(metadata.from_agent) ?? asNonEmptyString(metadata.fromAgent);
1432
1444
  }
1445
+ function extractUserText(message) {
1446
+ const parts = message.parts;
1447
+ if (!Array.isArray(parts)) return null;
1448
+ const chunks = [];
1449
+ for (const part of parts) {
1450
+ const p = asRecord(part);
1451
+ if (!p || p.kind !== "text") continue;
1452
+ const t = asNonEmptyString(p.text);
1453
+ if (t) chunks.push(t);
1454
+ }
1455
+ return chunks.length > 0 ? chunks.join("\n") : null;
1456
+ }
1457
+ function isAllowedChatReplyPath(chatId, replyPath) {
1458
+ if (!chatId || !replyPath) return false;
1459
+ if (replyPath.includes("..") || replyPath.includes("?") || replyPath.includes("#") || replyPath.includes("//") || replyPath.includes("\\") || !replyPath.startsWith("/")) {
1460
+ return false;
1461
+ }
1462
+ return replyPath === `/api/chats/${chatId}/agent-messages`;
1463
+ }
1464
+ function extractChatEnvelope(message) {
1465
+ const metadata = asRecord(message.metadata);
1466
+ if (!metadata) return null;
1467
+ const ap = asRecord(metadata.agentplanet);
1468
+ if (!ap) return null;
1469
+ const chatId = asNonEmptyString(ap.chat_id);
1470
+ const replyPath = asNonEmptyString(ap.reply_path);
1471
+ const replyChannel = asNonEmptyString(ap.reply_channel);
1472
+ if (!chatId || !replyPath) return null;
1473
+ if (replyChannel !== CHAT_REPLY_CHANNEL) return null;
1474
+ if (!isAllowedChatReplyPath(chatId, replyPath)) return null;
1475
+ return {
1476
+ chat_id: chatId,
1477
+ reply_path: replyPath,
1478
+ reply_channel: CHAT_REPLY_CHANNEL,
1479
+ gateway_message_id: asNonEmptyString(ap.message_id) ?? asNonEmptyString(ap.messageId),
1480
+ user_text: extractUserText(message)
1481
+ };
1482
+ }
1433
1483
  function normalizeEvent(body, opts = {}) {
1434
1484
  const generateId = opts.generateId ?? (() => crypto.randomUUID());
1435
1485
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
@@ -1441,11 +1491,16 @@ function normalizeEvent(body, opts = {}) {
1441
1491
  message_id: extractMessageId(message, generateId),
1442
1492
  context_id: extractContextId(message),
1443
1493
  from_agent: extractFromAgent(message),
1494
+ chat: extractChatEnvelope(message),
1444
1495
  received_at: now().toISOString(),
1445
1496
  raw: body
1446
1497
  };
1447
1498
  }
1448
1499
  function dedupeKey(event) {
1500
+ if (event.chat) {
1501
+ const mid = event.chat.gateway_message_id ?? event.message_id;
1502
+ return `chat:${event.chat.chat_id}:${mid}`;
1503
+ }
1449
1504
  return event.task_id ?? event.message_id;
1450
1505
  }
1451
1506
  var DedupeStore = class {
@@ -1481,11 +1536,223 @@ var DedupeStore = class {
1481
1536
  }
1482
1537
  };
1483
1538
 
1539
+ // src/commands/chat-writeback.ts
1540
+ var DEFAULT_COMPLETE_TIMEOUT_MS = 12e4;
1541
+ var DEFAULT_WRITEBACK_TIMEOUT_MS = 3e4;
1542
+ function asRecord2(v) {
1543
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
1544
+ }
1545
+ function validateChatWritebackOptions(opts) {
1546
+ if (!opts.chatWriteback) return null;
1547
+ if (!opts.agentId) {
1548
+ return "--chat-writeback requires a known agent id (join / --agent-id).";
1549
+ }
1550
+ if (!opts.chatApiBase?.trim()) {
1551
+ return "--chat-writeback requires --chat-api-base (or ACN_CHAT_API_BASE / AGENTPLANET_API_BASE).";
1552
+ }
1553
+ if (!opts.chatToken?.trim()) {
1554
+ return "--chat-writeback requires --chat-token (or ACN_CHAT_WRITEBACK_TOKEN / AGENTPLANET_INTERNAL_TOKEN / AGENTPLANET_INTERNAL_API_TOKEN).";
1555
+ }
1556
+ const hasUrl = Boolean(opts.chatCompleteUrl?.trim());
1557
+ const hasExec = Boolean(opts.chatCompleteExec?.trim());
1558
+ if (hasUrl === hasExec) {
1559
+ return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."}).';
1560
+ }
1561
+ return null;
1562
+ }
1563
+ function buildChatWritebackOptions(opts) {
1564
+ if (!opts.chatWriteback) return void 0;
1565
+ return {
1566
+ enabled: true,
1567
+ apiBase: opts.chatApiBase.replace(/\/+$/, ""),
1568
+ token: opts.chatToken,
1569
+ agentId: opts.agentId,
1570
+ completeUrl: opts.chatCompleteUrl?.trim() || void 0,
1571
+ completeExec: opts.chatCompleteExec?.trim() || void 0,
1572
+ completeTimeoutMs: opts.chatCompleteTimeoutMs,
1573
+ writebackTimeoutMs: opts.chatWritebackTimeoutMs
1574
+ };
1575
+ }
1576
+ function extractContent(payload) {
1577
+ const rec = asRecord2(payload);
1578
+ if (!rec) return null;
1579
+ for (const key of ["content", "reply", "text"]) {
1580
+ const v = rec[key];
1581
+ if (typeof v === "string" && v.trim()) {
1582
+ const t = v.trim();
1583
+ if (t.toLowerCase() === "accepted") continue;
1584
+ return t;
1585
+ }
1586
+ }
1587
+ return null;
1588
+ }
1589
+ async function completeViaHttp(event, opts, deps) {
1590
+ const fetchFn = deps.fetchFn ?? fetch;
1591
+ const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
1592
+ const controller = new AbortController();
1593
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1594
+ try {
1595
+ const res = await fetchFn(opts.completeUrl, {
1596
+ method: "POST",
1597
+ headers: { "content-type": "application/json" },
1598
+ body: JSON.stringify(event),
1599
+ signal: controller.signal
1600
+ });
1601
+ const text = await res.text();
1602
+ if (res.status < 200 || res.status >= 300) {
1603
+ return { ok: false, reason: `complete_http_${res.status}` };
1604
+ }
1605
+ let parsed;
1606
+ try {
1607
+ parsed = JSON.parse(text);
1608
+ } catch {
1609
+ return { ok: false, reason: "complete_invalid_json" };
1610
+ }
1611
+ const content = extractContent(parsed);
1612
+ if (!content) return { ok: false, reason: "complete_missing_content" };
1613
+ return { ok: true, content };
1614
+ } catch (err) {
1615
+ const msg = err instanceof Error ? err.message : String(err);
1616
+ if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
1617
+ return { ok: false, reason: "complete_timeout" };
1618
+ }
1619
+ return { ok: false, reason: msg.slice(0, 200) };
1620
+ } finally {
1621
+ clearTimeout(timer);
1622
+ }
1623
+ }
1624
+ function completeViaExec(event, opts, deps) {
1625
+ const spawnFn = deps.spawnFn ?? import_child_process.spawn;
1626
+ const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
1627
+ const body = Buffer.from(JSON.stringify(event), "utf-8");
1628
+ return new Promise((resolve) => {
1629
+ let settled = false;
1630
+ const child = spawnFn(opts.completeExec, { shell: true });
1631
+ const stdout = [];
1632
+ const stderr = [];
1633
+ const finish = (result) => {
1634
+ if (settled) return;
1635
+ settled = true;
1636
+ clearTimeout(timer);
1637
+ resolve(result);
1638
+ };
1639
+ const timer = setTimeout(() => {
1640
+ child.kill("SIGTERM");
1641
+ finish({ ok: false, reason: "complete_timeout" });
1642
+ }, timeoutMs);
1643
+ child.stdout?.on("data", (d) => stdout.push(Buffer.from(d)));
1644
+ child.stderr?.on("data", (d) => stderr.push(Buffer.from(d)));
1645
+ child.on(
1646
+ "error",
1647
+ (e) => finish({ ok: false, reason: e.message.slice(0, 200) })
1648
+ );
1649
+ child.on("close", (code) => {
1650
+ if (code !== 0) {
1651
+ const detail = Buffer.concat(stderr).toString("utf-8").slice(0, 80);
1652
+ finish({
1653
+ ok: false,
1654
+ reason: detail ? `complete_exit_${code}:${detail}` : `complete_exit_${code}`
1655
+ });
1656
+ return;
1657
+ }
1658
+ const text = Buffer.concat(stdout).toString("utf-8").trim();
1659
+ try {
1660
+ const parsed = JSON.parse(text);
1661
+ const content = extractContent(parsed);
1662
+ if (!content) {
1663
+ finish({ ok: false, reason: "complete_missing_content" });
1664
+ return;
1665
+ }
1666
+ finish({ ok: true, content });
1667
+ } catch {
1668
+ finish({ ok: false, reason: "complete_invalid_json" });
1669
+ }
1670
+ });
1671
+ child.stdin?.end(body);
1672
+ });
1673
+ }
1674
+ async function postWriteback(event, content, opts, deps) {
1675
+ const chat = event.chat;
1676
+ if (!chat) return { ok: false, reason: "no_chat_envelope" };
1677
+ if (!isAllowedChatReplyPath(chat.chat_id, chat.reply_path)) {
1678
+ return { ok: false, reason: "reply_path_rejected" };
1679
+ }
1680
+ if (chat.reply_channel !== "agentplanet.chat") {
1681
+ return { ok: false, reason: "reply_channel_rejected" };
1682
+ }
1683
+ const fetchFn = deps.fetchFn ?? fetch;
1684
+ const path = chat.reply_path;
1685
+ let url;
1686
+ let baseOrigin;
1687
+ try {
1688
+ baseOrigin = new URL(opts.apiBase).origin;
1689
+ url = new URL(`${opts.apiBase}${path}`);
1690
+ } catch {
1691
+ return { ok: false, reason: "invalid_api_base" };
1692
+ }
1693
+ if (url.origin !== baseOrigin) {
1694
+ return { ok: false, reason: "reply_url_origin_mismatch" };
1695
+ }
1696
+ url.searchParams.set("agent_id", opts.agentId);
1697
+ const timeoutMs = opts.writebackTimeoutMs ?? DEFAULT_WRITEBACK_TIMEOUT_MS;
1698
+ const controller = new AbortController();
1699
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1700
+ try {
1701
+ const res = await fetchFn(url.toString(), {
1702
+ method: "POST",
1703
+ headers: {
1704
+ "content-type": "application/json",
1705
+ "X-Internal-Token": opts.token
1706
+ },
1707
+ body: JSON.stringify({ content }),
1708
+ signal: controller.signal
1709
+ });
1710
+ if (res.status === 200 || res.status === 201) {
1711
+ return { ok: true, httpStatus: res.status };
1712
+ }
1713
+ try {
1714
+ await res.arrayBuffer();
1715
+ } catch {
1716
+ }
1717
+ return { ok: false, reason: `writeback_http_${res.status}` };
1718
+ } catch (err) {
1719
+ const msg = err instanceof Error ? err.message : String(err);
1720
+ if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
1721
+ return { ok: false, reason: "writeback_timeout" };
1722
+ }
1723
+ return { ok: false, reason: msg.slice(0, 200) };
1724
+ } finally {
1725
+ clearTimeout(timer);
1726
+ }
1727
+ }
1728
+ async function handleChatWriteback(event, opts, deps = {}) {
1729
+ const logFn = deps.logFn ?? ((line) => console.error(line));
1730
+ if (!event.chat) return { ok: false, reason: "no_chat_envelope" };
1731
+ const completed = opts.completeUrl ? await completeViaHttp(event, opts, deps) : await completeViaExec(event, opts, deps);
1732
+ if (!completed.ok) {
1733
+ logFn(
1734
+ `[acn listen] chat_complete_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${completed.reason}`
1735
+ );
1736
+ return { ok: false, reason: completed.reason };
1737
+ }
1738
+ const written = await postWriteback(event, completed.content, opts, deps);
1739
+ if (!written.ok) {
1740
+ logFn(
1741
+ `[acn listen] chat_writeback_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${written.reason}`
1742
+ );
1743
+ return written;
1744
+ }
1745
+ logFn(
1746
+ `[acn listen] chat_writeback_ok chat_id=${event.chat.chat_id} message_id=${event.message_id} http=${written.httpStatus}`
1747
+ );
1748
+ return written;
1749
+ }
1750
+
1484
1751
  // src/commands/local-receiver.ts
1485
1752
  var import_crypto = require("crypto");
1486
1753
 
1487
1754
  // src/commands/runtime-adapter.ts
1488
- var import_child_process = require("child_process");
1755
+ var import_child_process2 = require("child_process");
1489
1756
  var DEFAULT_WAKE_TIMEOUT_MS = 5e3;
1490
1757
  function parseWakeHeaders(raw) {
1491
1758
  const out = {};
@@ -1554,7 +1821,7 @@ async function wakeHttp(event, opts, deps) {
1554
1821
  }
1555
1822
  }
1556
1823
  function wakeCommand(event, opts, deps) {
1557
- const spawnFn = deps.spawnFn ?? import_child_process.spawn;
1824
+ const spawnFn = deps.spawnFn ?? import_child_process2.spawn;
1558
1825
  const timeoutMs = opts.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS;
1559
1826
  const body = Buffer.from(JSON.stringify(event), "utf-8");
1560
1827
  return new Promise((resolve) => {
@@ -1702,6 +1969,18 @@ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send,
1702
1969
  if (!result.shouldWake || !result.event) return;
1703
1970
  const event = result.event;
1704
1971
  const key = opts.dedupe ? dedupeKey(event) : null;
1972
+ if (event.chat && opts.chatWriteback?.enabled) {
1973
+ void handleChatWriteback(event, opts.chatWriteback, deps).then((written) => {
1974
+ if (!written.ok && key) dedupeStore.forget(key);
1975
+ }).catch((err) => {
1976
+ if (key) dedupeStore.forget(key);
1977
+ const msg = err instanceof Error ? err.message : String(err);
1978
+ logFn(
1979
+ `[acn listen] chat_writeback_failed message_id=${event.message_id} reason=${msg.slice(0, 200)}`
1980
+ );
1981
+ });
1982
+ return;
1983
+ }
1705
1984
  void wakeRuntime(event, opts, deps).then((wake) => {
1706
1985
  if (!wake.ok) {
1707
1986
  if (key) dedupeStore.forget(key);
@@ -1767,7 +2046,7 @@ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
1767
2046
  return;
1768
2047
  }
1769
2048
  if (opts.exec) {
1770
- send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process2.spawn));
2049
+ send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
1771
2050
  return;
1772
2051
  }
1773
2052
  send(errorResponse(frame.id, 500, "no handler configured"));
@@ -1974,6 +2253,25 @@ function listenCommand() {
1974
2253
  ).option(
1975
2254
  "--exec <command>",
1976
2255
  "Compat: shell per request; stdout must be a full A2A JSON-RPC response"
2256
+ ).option(
2257
+ "--chat-writeback",
2258
+ "On Chat Gateway envelopes: complete host reply then POST agent-messages (requires --runtime)"
2259
+ ).option(
2260
+ "--chat-api-base <url>",
2261
+ "Chat Gateway origin for writeback (env: ACN_CHAT_API_BASE or AGENTPLANET_API_BASE)"
2262
+ ).option(
2263
+ "--chat-token <token>",
2264
+ "X-Internal-Token for agent-messages (env: ACN_CHAT_WRITEBACK_TOKEN, AGENTPLANET_INTERNAL_TOKEN, or AGENTPLANET_INTERNAL_API_TOKEN)"
2265
+ ).option(
2266
+ "--chat-complete-url <url>",
2267
+ 'POST NormalizedEvent \u2192 JSON {"content":"..."} (mutually exclusive with --chat-complete-exec)'
2268
+ ).option(
2269
+ "--chat-complete-exec <cmd>",
2270
+ 'Shell: event JSON on stdin \u2192 stdout JSON {"content":"..."}'
2271
+ ).option(
2272
+ "--chat-complete-timeout <ms>",
2273
+ `Host complete timeout in ms (default ${DEFAULT_COMPLETE_TIMEOUT_MS})`,
2274
+ String(DEFAULT_COMPLETE_TIMEOUT_MS)
1977
2275
  ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(
1978
2276
  (opts) => {
1979
2277
  const config = loadConfig();
@@ -2002,6 +2300,26 @@ function listenCommand() {
2002
2300
  console.error(flagErr);
2003
2301
  process.exit(1);
2004
2302
  }
2303
+ const chatApiBase = opts.chatApiBase?.trim() || process.env.ACN_CHAT_API_BASE?.trim() || process.env.AGENTPLANET_API_BASE?.trim();
2304
+ const chatToken = opts.chatToken?.trim() || process.env.ACN_CHAT_WRITEBACK_TOKEN?.trim() || process.env.AGENTPLANET_INTERNAL_TOKEN?.trim() || process.env.AGENTPLANET_INTERNAL_API_TOKEN?.trim();
2305
+ const chatCompleteUrl = opts.chatCompleteUrl?.trim() || process.env.ACN_CHAT_COMPLETE_URL?.trim();
2306
+ const chatCompleteExec = opts.chatCompleteExec?.trim();
2307
+ if (opts.chatWriteback && !opts.runtime) {
2308
+ console.error("--chat-writeback requires --runtime http|command|log.");
2309
+ process.exit(1);
2310
+ }
2311
+ const chatErr = validateChatWritebackOptions({
2312
+ chatWriteback: opts.chatWriteback,
2313
+ chatApiBase,
2314
+ chatToken,
2315
+ chatCompleteUrl,
2316
+ chatCompleteExec,
2317
+ agentId
2318
+ });
2319
+ if (chatErr) {
2320
+ console.error(chatErr);
2321
+ process.exit(1);
2322
+ }
2005
2323
  let wakeHeaders;
2006
2324
  if (opts.runtime === "http") {
2007
2325
  try {
@@ -2013,6 +2331,10 @@ function listenCommand() {
2013
2331
  }
2014
2332
  const wakeTimeoutMs = Number.parseInt(opts.wakeTimeout ?? "", 10);
2015
2333
  const dedupeTtlSec = Number.parseInt(opts.dedupeTtl ?? "", 10);
2334
+ const chatCompleteTimeoutMs = Number.parseInt(
2335
+ opts.chatCompleteTimeout ?? "",
2336
+ 10
2337
+ );
2016
2338
  if (opts.runtime && (!Number.isFinite(wakeTimeoutMs) || wakeTimeoutMs <= 0)) {
2017
2339
  console.error("--wake-timeout must be a positive integer (ms).");
2018
2340
  process.exit(1);
@@ -2021,6 +2343,19 @@ function listenCommand() {
2021
2343
  console.error("--dedupe-ttl must be a positive integer (seconds).");
2022
2344
  process.exit(1);
2023
2345
  }
2346
+ if (opts.chatWriteback && (!Number.isFinite(chatCompleteTimeoutMs) || chatCompleteTimeoutMs <= 0)) {
2347
+ console.error("--chat-complete-timeout must be a positive integer (ms).");
2348
+ process.exit(1);
2349
+ }
2350
+ const chatWriteback = buildChatWritebackOptions({
2351
+ chatWriteback: opts.chatWriteback,
2352
+ chatApiBase,
2353
+ chatToken,
2354
+ chatCompleteUrl,
2355
+ chatCompleteExec,
2356
+ chatCompleteTimeoutMs: opts.chatWriteback ? chatCompleteTimeoutMs : void 0,
2357
+ agentId
2358
+ });
2024
2359
  runListener({
2025
2360
  agentId,
2026
2361
  apiKey,
@@ -2035,7 +2370,8 @@ function listenCommand() {
2035
2370
  wakeTimeoutMs,
2036
2371
  // commander: --no-dedupe sets dedupe=false; default true
2037
2372
  dedupe: opts.dedupe !== false,
2038
- dedupeTtlSec
2373
+ dedupeTtlSec,
2374
+ chatWriteback
2039
2375
  } : void 0
2040
2376
  });
2041
2377
  }
@@ -3146,6 +3482,102 @@ function orgCommand() {
3146
3482
  handleError(err);
3147
3483
  }
3148
3484
  });
3485
+ cmd.command("publish-task").description(
3486
+ "Publish a Task Pool task attributed to an Org (network by default; not Org work)"
3487
+ ).requiredOption("--org <org_id>", "Org id (stored as metadata.org_id)").requiredOption("-t, --title <title>", "Task title (min 3 chars)").requiredOption("-d, --description <text>", "Task description (min 10 chars)").requiredOption("--tags <tags>", "Required skill tags, comma-separated").option("--deadline <hours>", "Deadline in hours (default: 48)", "48").option("--reward <amount>", "Reward amount (default: 0)", "0").option(
3488
+ "--pay-from <source>",
3489
+ "Who pays: agent (default, attribution only) | org (Org wallet + credits escrow)",
3490
+ "agent"
3491
+ ).option("--type <type>", "Task type", "general").option("--max-participants <n>", "Max participants", "1").option(
3492
+ "--fence",
3493
+ "Scope task to the Org subnet fence (may deliver task.* to Org harness)",
3494
+ false
3495
+ ).option("--subnet <slug>", "Override subnet slug (implies fence; default: Org fence)").action(
3496
+ async (opts) => {
3497
+ const config = loadConfig();
3498
+ if (!config.api_key) {
3499
+ console.error(
3500
+ "No API key found. Run `acn join` first or `acn config set api-key <key>`."
3501
+ );
3502
+ process.exit(1);
3503
+ }
3504
+ const payFrom = (opts.payFrom ?? "agent").toLowerCase();
3505
+ if (payFrom !== "agent" && payFrom !== "org") {
3506
+ console.error('--pay-from must be "agent" or "org"');
3507
+ process.exit(1);
3508
+ }
3509
+ try {
3510
+ const body = {
3511
+ title: opts.title,
3512
+ description: opts.description,
3513
+ deadline_hours: parseInt(opts.deadline ?? "48", 10),
3514
+ required_tags: opts.tags.split(",").map((s) => s.trim()).filter(Boolean),
3515
+ reward: opts.reward ?? "0",
3516
+ task_type: opts.type ?? "general",
3517
+ max_participants: parseInt(opts.maxParticipants ?? "1", 10),
3518
+ pay_from_org: payFrom === "org",
3519
+ fence: Boolean(opts.fence || opts.subnet)
3520
+ };
3521
+ if (opts.subnet) {
3522
+ body.subnet_slug = opts.subnet;
3523
+ }
3524
+ const task = await acnPost(
3525
+ `/orgs/${opts.org}/publish-task`,
3526
+ body
3527
+ );
3528
+ const lines = [
3529
+ payFrom === "org" ? "Task published \u2014 Org-paid (credits escrow when reward > 0)" : "Task published for Org (Task Pool \u2014 not Org work)",
3530
+ ` Task ID : ${task.task_id}`,
3531
+ ` Org : ${opts.org}`,
3532
+ ` Pay from : ${payFrom}`,
3533
+ ` Creator : ${task.creator_type}/${task.creator_id}`,
3534
+ ` Status : ${task.status}`,
3535
+ ` Title : ${task.title}`,
3536
+ ` Currency : ${task.reward_currency}`,
3537
+ ` Escrow : ${String(task.use_escrow ?? false)}`,
3538
+ ` Subnet : ${task.subnet_slug ?? "(network / unscoped)"}`,
3539
+ ` metadata : org_id=${String(task.metadata?.org_id ?? opts.org)} org_publish=${String(task.metadata?.org_publish ?? true)}`
3540
+ ];
3541
+ if (body.fence) {
3542
+ lines.push(
3543
+ " Note : fenced \u2014 task.* may hit the Org harness webhook"
3544
+ );
3545
+ }
3546
+ output(task, lines.join("\n"));
3547
+ } catch (err) {
3548
+ handleError(err);
3549
+ }
3550
+ }
3551
+ );
3552
+ cmd.command("import-task").description(
3553
+ "Import a Task Pool task as Org work (governance only; links via task.metadata)"
3554
+ ).requiredOption("--org <org_id>", "Org id").requiredOption("--task <task_id>", "Task id to import").option("--assignee <agent_id>", "Optional assignee for the work item").action(
3555
+ async (opts) => {
3556
+ const config = loadConfig();
3557
+ if (!config.api_key) {
3558
+ console.error(
3559
+ "No API key found. Run `acn join` first or `acn config set api-key <key>`."
3560
+ );
3561
+ process.exit(1);
3562
+ }
3563
+ try {
3564
+ const body = { task_id: opts.task };
3565
+ if (opts.assignee) body.assignee_agent_id = opts.assignee;
3566
+ const res = await acnPost(`/orgs/${opts.org}/work/import-task`, body);
3567
+ const lines = [
3568
+ res.already_imported ? "Task already imported (idempotent)" : "Task imported as Org work",
3569
+ ` Work ID : ${res.work_id}`,
3570
+ ` Org : ${res.org_id}`,
3571
+ ` Task ID : ${res.source_task_id ?? opts.task}`,
3572
+ ` Status : ${res.status}`,
3573
+ ` Title : ${res.title}`
3574
+ ];
3575
+ output(res, lines.join("\n"));
3576
+ } catch (err) {
3577
+ handleError(err);
3578
+ }
3579
+ }
3580
+ );
3149
3581
  return cmd;
3150
3582
  }
3151
3583
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) — zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {