@acnlabs/acn-cli 0.14.0 → 0.14.2

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 +21 -0
  2. package/dist/index.js +524 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -89,6 +89,27 @@ 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
+ Auth uses an **ACN agent JWT** minted from your config `api_key` (no
96
+ AgentPlanet Internal Token):
97
+
98
+ ```bash
99
+ export AGENTPLANET_API_BASE=https://api.agentplanet.org
100
+ # optional: ACN_CHAT_JWT_AUDIENCE=https://api.agentplanet.org
101
+
102
+ acn listen --runtime http \
103
+ --wake-url http://127.0.0.1:10122/hooks/agent \
104
+ --chat-writeback \
105
+ --chat-complete-url http://127.0.0.1:10122/chat/complete
106
+ # or: --chat-complete-exec 'python3 /path/to/complete.py'
107
+ ```
108
+
109
+ `--chat-token` is deprecated/ignored. Task / Org wakes still use `--wake-url` /
110
+ `--wake-exec`. Chat envelopes skip wake and use the complete → writeback path
111
+ instead.
112
+
92
113
  **Coverage:** only traffic that arrives over the Mode B relay. Open Task Pool
93
114
  rows that were never pushed as A2A still need `acn tasks list` / reconcile.
94
115
 
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.2",
35
35
  description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -469,7 +469,9 @@ function rotateKeyCommand() {
469
469
  }
470
470
  if (!config.api_key) {
471
471
  console.error(
472
- "No API key found in ~/.acn/config.json. The CLI rotates with the current agent key; if you have lost the key, recover via the Labs web UI (Auth0-authorised owner-side rotation)."
472
+ `No API key found in ~/.acn/config.json. The CLI rotates with the current agent key.
473
+ If you have lost the key, sign in to Labs as the agent owner, open /agents/${agentId}, click "Reset API Key", then run:
474
+ acn config set api_key <new>`
473
475
  );
474
476
  process.exit(1);
475
477
  }
@@ -608,6 +610,8 @@ function formatTask(t) {
608
610
  lines.push(` Reward : ${t.reward} ${t.reward_currency ?? ""}`);
609
611
  }
610
612
  if (t.subnet_slug) lines.push(` Subnet : ${t.subnet_slug}`);
613
+ const orgId = t.metadata?.org_id;
614
+ if (typeof orgId === "string" && orgId) lines.push(` Org : ${orgId}`);
611
615
  if (t.description) lines.push(` Desc : ${t.description.slice(0, 120)}`);
612
616
  if (t.created_at) lines.push(` Created : ${t.created_at}`);
613
617
  if (t.deadline) lines.push(` Deadline : ${t.deadline}`);
@@ -697,7 +701,10 @@ ${formatTask(t)}`).join("\n\n")
697
701
  handleError(err);
698
702
  }
699
703
  });
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(
704
+ 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(
705
+ "--org-id <org_id>",
706
+ "Attribute to an Org (metadata.org_id + org_publish; prefer `acn org publish-task`)"
707
+ ).action(
701
708
  async (opts) => {
702
709
  const config = loadConfig();
703
710
  if (!config.api_key) {
@@ -722,6 +729,9 @@ ${formatTask(t)}`).join("\n\n")
722
729
  if (opts.subnet) {
723
730
  body.subnet_slug = opts.subnet;
724
731
  }
732
+ if (opts.orgId) {
733
+ body.metadata = { org_id: opts.orgId, org_publish: true };
734
+ }
725
735
  try {
726
736
  const task = await acnPost("/tasks/agent/create", body);
727
737
  output(task, [`Task created!
@@ -1375,10 +1385,14 @@ ${formatPolicy(res)}`);
1375
1385
 
1376
1386
  // src/commands/listen.ts
1377
1387
  var import_commander10 = require("commander");
1378
- var import_child_process2 = require("child_process");
1388
+ var import_child_process3 = require("child_process");
1379
1389
  var import_ws = __toESM(require("ws"));
1380
1390
 
1391
+ // src/commands/chat-writeback.ts
1392
+ var import_child_process = require("child_process");
1393
+
1381
1394
  // src/commands/normalize-event.ts
1395
+ var CHAT_REPLY_CHANNEL = "agentplanet.chat";
1382
1396
  function asRecord(v) {
1383
1397
  return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
1384
1398
  }
@@ -1430,6 +1444,44 @@ function extractFromAgent(message) {
1430
1444
  if (!metadata) return null;
1431
1445
  return asNonEmptyString(metadata.from_agent) ?? asNonEmptyString(metadata.fromAgent);
1432
1446
  }
1447
+ function extractUserText(message) {
1448
+ const parts = message.parts;
1449
+ if (!Array.isArray(parts)) return null;
1450
+ const chunks = [];
1451
+ for (const part of parts) {
1452
+ const p = asRecord(part);
1453
+ if (!p || p.kind !== "text") continue;
1454
+ const t = asNonEmptyString(p.text);
1455
+ if (t) chunks.push(t);
1456
+ }
1457
+ return chunks.length > 0 ? chunks.join("\n") : null;
1458
+ }
1459
+ function isAllowedChatReplyPath(chatId, replyPath) {
1460
+ if (!chatId || !replyPath) return false;
1461
+ if (replyPath.includes("..") || replyPath.includes("?") || replyPath.includes("#") || replyPath.includes("//") || replyPath.includes("\\") || !replyPath.startsWith("/")) {
1462
+ return false;
1463
+ }
1464
+ return replyPath === `/api/chats/${chatId}/agent-messages`;
1465
+ }
1466
+ function extractChatEnvelope(message) {
1467
+ const metadata = asRecord(message.metadata);
1468
+ if (!metadata) return null;
1469
+ const ap = asRecord(metadata.agentplanet);
1470
+ if (!ap) return null;
1471
+ const chatId = asNonEmptyString(ap.chat_id);
1472
+ const replyPath = asNonEmptyString(ap.reply_path);
1473
+ const replyChannel = asNonEmptyString(ap.reply_channel);
1474
+ if (!chatId || !replyPath) return null;
1475
+ if (replyChannel !== CHAT_REPLY_CHANNEL) return null;
1476
+ if (!isAllowedChatReplyPath(chatId, replyPath)) return null;
1477
+ return {
1478
+ chat_id: chatId,
1479
+ reply_path: replyPath,
1480
+ reply_channel: CHAT_REPLY_CHANNEL,
1481
+ gateway_message_id: asNonEmptyString(ap.message_id) ?? asNonEmptyString(ap.messageId),
1482
+ user_text: extractUserText(message)
1483
+ };
1484
+ }
1433
1485
  function normalizeEvent(body, opts = {}) {
1434
1486
  const generateId = opts.generateId ?? (() => crypto.randomUUID());
1435
1487
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
@@ -1441,11 +1493,16 @@ function normalizeEvent(body, opts = {}) {
1441
1493
  message_id: extractMessageId(message, generateId),
1442
1494
  context_id: extractContextId(message),
1443
1495
  from_agent: extractFromAgent(message),
1496
+ chat: extractChatEnvelope(message),
1444
1497
  received_at: now().toISOString(),
1445
1498
  raw: body
1446
1499
  };
1447
1500
  }
1448
1501
  function dedupeKey(event) {
1502
+ if (event.chat) {
1503
+ const mid = event.chat.gateway_message_id ?? event.message_id;
1504
+ return `chat:${event.chat.chat_id}:${mid}`;
1505
+ }
1449
1506
  return event.task_id ?? event.message_id;
1450
1507
  }
1451
1508
  var DedupeStore = class {
@@ -1481,11 +1538,299 @@ var DedupeStore = class {
1481
1538
  }
1482
1539
  };
1483
1540
 
1541
+ // src/commands/chat-writeback.ts
1542
+ var DEFAULT_COMPLETE_TIMEOUT_MS = 12e4;
1543
+ var DEFAULT_WRITEBACK_TIMEOUT_MS = 3e4;
1544
+ var DEFAULT_CHAT_JWT_AUDIENCE = "https://api.agentplanet.org";
1545
+ var cachedJwt = null;
1546
+ function asRecord2(v) {
1547
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
1548
+ }
1549
+ function validateChatWritebackOptions(opts) {
1550
+ if (!opts.chatWriteback) return null;
1551
+ if (!opts.agentId) {
1552
+ return "--chat-writeback requires a known agent id (join / --agent-id).";
1553
+ }
1554
+ if (!opts.apiKey?.trim()) {
1555
+ return "--chat-writeback requires an ACN API key (acn join / config set api-key).";
1556
+ }
1557
+ if (!opts.chatApiBase?.trim()) {
1558
+ return "--chat-writeback requires --chat-api-base (or ACN_CHAT_API_BASE / AGENTPLANET_API_BASE).";
1559
+ }
1560
+ const hasUrl = Boolean(opts.chatCompleteUrl?.trim());
1561
+ const hasExec = Boolean(opts.chatCompleteExec?.trim());
1562
+ if (hasUrl === hasExec) {
1563
+ return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."}).';
1564
+ }
1565
+ return null;
1566
+ }
1567
+ function buildChatWritebackOptions(opts) {
1568
+ if (!opts.chatWriteback) return void 0;
1569
+ const apiBase = opts.chatApiBase.replace(/\/+$/, "");
1570
+ const audience = opts.audience?.trim() || DEFAULT_CHAT_JWT_AUDIENCE;
1571
+ return {
1572
+ enabled: true,
1573
+ apiBase,
1574
+ acnBaseUrl: opts.acnBaseUrl.replace(/\/+$/, ""),
1575
+ apiKey: opts.apiKey,
1576
+ agentId: opts.agentId,
1577
+ audience,
1578
+ completeUrl: opts.chatCompleteUrl?.trim() || void 0,
1579
+ completeExec: opts.chatCompleteExec?.trim() || void 0,
1580
+ completeTimeoutMs: opts.chatCompleteTimeoutMs,
1581
+ writebackTimeoutMs: opts.chatWritebackTimeoutMs
1582
+ };
1583
+ }
1584
+ function extractContent(payload) {
1585
+ const rec = asRecord2(payload);
1586
+ if (!rec) return null;
1587
+ for (const key of ["content", "reply", "text"]) {
1588
+ const v = rec[key];
1589
+ if (typeof v === "string" && v.trim()) {
1590
+ const t = v.trim();
1591
+ if (t.toLowerCase() === "accepted") continue;
1592
+ return t;
1593
+ }
1594
+ }
1595
+ return null;
1596
+ }
1597
+ async function mintAgentJwt(opts, fetchFn = fetch) {
1598
+ const now = Math.floor(Date.now() / 1e3);
1599
+ if (cachedJwt && cachedJwt.agentId === opts.agentId && cachedJwt.expEpochSec > now + 60) {
1600
+ return { ok: true, token: cachedJwt.token };
1601
+ }
1602
+ const url = `${opts.acnBaseUrl.replace(/\/+$/, "")}/oauth/token`;
1603
+ try {
1604
+ const res = await fetchFn(url, {
1605
+ method: "POST",
1606
+ headers: { "content-type": "application/json" },
1607
+ body: JSON.stringify({
1608
+ grant_type: "client_credentials",
1609
+ client_id: opts.agentId,
1610
+ client_secret: opts.apiKey,
1611
+ audience: opts.audience
1612
+ })
1613
+ });
1614
+ const text = await res.text();
1615
+ if (res.status < 200 || res.status >= 300) {
1616
+ return { ok: false, reason: `oauth_http_${res.status}` };
1617
+ }
1618
+ let parsed;
1619
+ try {
1620
+ parsed = JSON.parse(text);
1621
+ } catch {
1622
+ return { ok: false, reason: "oauth_invalid_json" };
1623
+ }
1624
+ const rec = asRecord2(parsed);
1625
+ const token = typeof rec?.access_token === "string" ? rec.access_token.trim() : "";
1626
+ if (!token) return { ok: false, reason: "oauth_missing_access_token" };
1627
+ const expiresIn = typeof rec?.expires_in === "number" && rec.expires_in > 0 ? rec.expires_in : 1800;
1628
+ cachedJwt = {
1629
+ token,
1630
+ agentId: opts.agentId,
1631
+ expEpochSec: now + expiresIn
1632
+ };
1633
+ return { ok: true, token };
1634
+ } catch (err) {
1635
+ const msg = err instanceof Error ? err.message : String(err);
1636
+ return { ok: false, reason: msg.slice(0, 200) };
1637
+ }
1638
+ }
1639
+ function clearAgentJwtCache() {
1640
+ cachedJwt = null;
1641
+ }
1642
+ async function completeViaHttp(event, opts, deps) {
1643
+ const fetchFn = deps.fetchFn ?? fetch;
1644
+ const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
1645
+ const controller = new AbortController();
1646
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1647
+ try {
1648
+ const res = await fetchFn(opts.completeUrl, {
1649
+ method: "POST",
1650
+ headers: { "content-type": "application/json" },
1651
+ body: JSON.stringify(event),
1652
+ signal: controller.signal
1653
+ });
1654
+ const text = await res.text();
1655
+ if (res.status < 200 || res.status >= 300) {
1656
+ return { ok: false, reason: `complete_http_${res.status}` };
1657
+ }
1658
+ let parsed;
1659
+ try {
1660
+ parsed = JSON.parse(text);
1661
+ } catch {
1662
+ return { ok: false, reason: "complete_invalid_json" };
1663
+ }
1664
+ const content = extractContent(parsed);
1665
+ if (!content) return { ok: false, reason: "complete_missing_content" };
1666
+ return { ok: true, content };
1667
+ } catch (err) {
1668
+ const msg = err instanceof Error ? err.message : String(err);
1669
+ if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
1670
+ return { ok: false, reason: "complete_timeout" };
1671
+ }
1672
+ return { ok: false, reason: msg.slice(0, 200) };
1673
+ } finally {
1674
+ clearTimeout(timer);
1675
+ }
1676
+ }
1677
+ function completeViaExec(event, opts, deps) {
1678
+ const spawnFn = deps.spawnFn ?? import_child_process.spawn;
1679
+ const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
1680
+ const body = Buffer.from(JSON.stringify(event), "utf-8");
1681
+ return new Promise((resolve) => {
1682
+ let settled = false;
1683
+ const child = spawnFn(opts.completeExec, { shell: true });
1684
+ const stdout = [];
1685
+ const stderr = [];
1686
+ const finish = (result) => {
1687
+ if (settled) return;
1688
+ settled = true;
1689
+ clearTimeout(timer);
1690
+ resolve(result);
1691
+ };
1692
+ const timer = setTimeout(() => {
1693
+ child.kill("SIGTERM");
1694
+ finish({ ok: false, reason: "complete_timeout" });
1695
+ }, timeoutMs);
1696
+ child.stdout?.on("data", (d) => stdout.push(Buffer.from(d)));
1697
+ child.stderr?.on("data", (d) => stderr.push(Buffer.from(d)));
1698
+ child.on(
1699
+ "error",
1700
+ (e) => finish({ ok: false, reason: e.message.slice(0, 200) })
1701
+ );
1702
+ child.on("close", (code) => {
1703
+ if (code !== 0) {
1704
+ const detail = Buffer.concat(stderr).toString("utf-8").slice(0, 80);
1705
+ finish({
1706
+ ok: false,
1707
+ reason: detail ? `complete_exit_${code}:${detail}` : `complete_exit_${code}`
1708
+ });
1709
+ return;
1710
+ }
1711
+ const text = Buffer.concat(stdout).toString("utf-8").trim();
1712
+ try {
1713
+ const parsed = JSON.parse(text);
1714
+ const content = extractContent(parsed);
1715
+ if (!content) {
1716
+ finish({ ok: false, reason: "complete_missing_content" });
1717
+ return;
1718
+ }
1719
+ finish({ ok: true, content });
1720
+ } catch {
1721
+ finish({ ok: false, reason: "complete_invalid_json" });
1722
+ }
1723
+ });
1724
+ child.stdin?.end(body);
1725
+ });
1726
+ }
1727
+ async function postWriteback(event, content, opts, deps) {
1728
+ const chat = event.chat;
1729
+ if (!chat) return { ok: false, reason: "no_chat_envelope" };
1730
+ if (!isAllowedChatReplyPath(chat.chat_id, chat.reply_path)) {
1731
+ return { ok: false, reason: "reply_path_rejected" };
1732
+ }
1733
+ if (chat.reply_channel !== "agentplanet.chat") {
1734
+ return { ok: false, reason: "reply_channel_rejected" };
1735
+ }
1736
+ const fetchFn = deps.fetchFn ?? fetch;
1737
+ const path = chat.reply_path;
1738
+ let url;
1739
+ let baseOrigin;
1740
+ try {
1741
+ baseOrigin = new URL(opts.apiBase).origin;
1742
+ url = new URL(`${opts.apiBase}${path}`);
1743
+ } catch {
1744
+ return { ok: false, reason: "invalid_api_base" };
1745
+ }
1746
+ if (url.origin !== baseOrigin) {
1747
+ return { ok: false, reason: "reply_url_origin_mismatch" };
1748
+ }
1749
+ const timeoutMs = opts.writebackTimeoutMs ?? DEFAULT_WRITEBACK_TIMEOUT_MS;
1750
+ const postOnce = async (token) => {
1751
+ const controller = new AbortController();
1752
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1753
+ try {
1754
+ const res = await fetchFn(url.toString(), {
1755
+ method: "POST",
1756
+ headers: {
1757
+ "content-type": "application/json",
1758
+ Authorization: `Bearer ${token}`
1759
+ },
1760
+ body: JSON.stringify({ content }),
1761
+ signal: controller.signal
1762
+ });
1763
+ if (res.status === 200 || res.status === 201) {
1764
+ return { ok: true, status: res.status };
1765
+ }
1766
+ try {
1767
+ await res.arrayBuffer();
1768
+ } catch {
1769
+ }
1770
+ return {
1771
+ ok: false,
1772
+ status: res.status,
1773
+ reason: `writeback_http_${res.status}`
1774
+ };
1775
+ } catch (err) {
1776
+ const msg = err instanceof Error ? err.message : String(err);
1777
+ if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
1778
+ return { ok: false, status: 0, reason: "writeback_timeout" };
1779
+ }
1780
+ return { ok: false, status: 0, reason: msg.slice(0, 200) };
1781
+ } finally {
1782
+ clearTimeout(timer);
1783
+ }
1784
+ };
1785
+ const minted = await mintAgentJwt(opts, fetchFn);
1786
+ if (!minted.ok) {
1787
+ return { ok: false, reason: minted.reason };
1788
+ }
1789
+ let result = await postOnce(minted.token);
1790
+ if (result.ok) {
1791
+ return { ok: true, httpStatus: result.status };
1792
+ }
1793
+ if (result.status === 401) {
1794
+ clearAgentJwtCache();
1795
+ const reminted = await mintAgentJwt(opts, fetchFn);
1796
+ if (!reminted.ok) {
1797
+ return { ok: false, reason: reminted.reason };
1798
+ }
1799
+ result = await postOnce(reminted.token);
1800
+ if (result.ok) {
1801
+ return { ok: true, httpStatus: result.status };
1802
+ }
1803
+ }
1804
+ return { ok: false, reason: result.reason };
1805
+ }
1806
+ async function handleChatWriteback(event, opts, deps = {}) {
1807
+ const logFn = deps.logFn ?? ((line) => console.error(line));
1808
+ if (!event.chat) return { ok: false, reason: "no_chat_envelope" };
1809
+ const completed = opts.completeUrl ? await completeViaHttp(event, opts, deps) : await completeViaExec(event, opts, deps);
1810
+ if (!completed.ok) {
1811
+ logFn(
1812
+ `[acn listen] chat_complete_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${completed.reason}`
1813
+ );
1814
+ return { ok: false, reason: completed.reason };
1815
+ }
1816
+ const written = await postWriteback(event, completed.content, opts, deps);
1817
+ if (!written.ok) {
1818
+ logFn(
1819
+ `[acn listen] chat_writeback_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${written.reason}`
1820
+ );
1821
+ return written;
1822
+ }
1823
+ logFn(
1824
+ `[acn listen] chat_writeback_ok chat_id=${event.chat.chat_id} message_id=${event.message_id} http=${written.httpStatus}`
1825
+ );
1826
+ return written;
1827
+ }
1828
+
1484
1829
  // src/commands/local-receiver.ts
1485
1830
  var import_crypto = require("crypto");
1486
1831
 
1487
1832
  // src/commands/runtime-adapter.ts
1488
- var import_child_process = require("child_process");
1833
+ var import_child_process2 = require("child_process");
1489
1834
  var DEFAULT_WAKE_TIMEOUT_MS = 5e3;
1490
1835
  function parseWakeHeaders(raw) {
1491
1836
  const out = {};
@@ -1554,7 +1899,7 @@ async function wakeHttp(event, opts, deps) {
1554
1899
  }
1555
1900
  }
1556
1901
  function wakeCommand(event, opts, deps) {
1557
- const spawnFn = deps.spawnFn ?? import_child_process.spawn;
1902
+ const spawnFn = deps.spawnFn ?? import_child_process2.spawn;
1558
1903
  const timeoutMs = opts.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS;
1559
1904
  const body = Buffer.from(JSON.stringify(event), "utf-8");
1560
1905
  return new Promise((resolve) => {
@@ -1702,6 +2047,18 @@ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send,
1702
2047
  if (!result.shouldWake || !result.event) return;
1703
2048
  const event = result.event;
1704
2049
  const key = opts.dedupe ? dedupeKey(event) : null;
2050
+ if (event.chat && opts.chatWriteback?.enabled) {
2051
+ void handleChatWriteback(event, opts.chatWriteback, deps).then((written) => {
2052
+ if (!written.ok && key) dedupeStore.forget(key);
2053
+ }).catch((err) => {
2054
+ if (key) dedupeStore.forget(key);
2055
+ const msg = err instanceof Error ? err.message : String(err);
2056
+ logFn(
2057
+ `[acn listen] chat_writeback_failed message_id=${event.message_id} reason=${msg.slice(0, 200)}`
2058
+ );
2059
+ });
2060
+ return;
2061
+ }
1705
2062
  void wakeRuntime(event, opts, deps).then((wake) => {
1706
2063
  if (!wake.ok) {
1707
2064
  if (key) dedupeStore.forget(key);
@@ -1767,7 +2124,7 @@ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
1767
2124
  return;
1768
2125
  }
1769
2126
  if (opts.exec) {
1770
- send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process2.spawn));
2127
+ send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
1771
2128
  return;
1772
2129
  }
1773
2130
  send(errorResponse(frame.id, 500, "no handler configured"));
@@ -1974,6 +2331,25 @@ function listenCommand() {
1974
2331
  ).option(
1975
2332
  "--exec <command>",
1976
2333
  "Compat: shell per request; stdout must be a full A2A JSON-RPC response"
2334
+ ).option(
2335
+ "--chat-writeback",
2336
+ "On Chat Gateway envelopes: complete host reply then POST agent-messages with ACN agent JWT (requires --runtime; uses config api-key)"
2337
+ ).option(
2338
+ "--chat-api-base <url>",
2339
+ "Chat Gateway origin for writeback (env: ACN_CHAT_API_BASE or AGENTPLANET_API_BASE)"
2340
+ ).option(
2341
+ "--chat-token <token>",
2342
+ "Deprecated/ignored: writeback now mints ACN agent JWT from api-key"
2343
+ ).option(
2344
+ "--chat-complete-url <url>",
2345
+ 'POST NormalizedEvent \u2192 JSON {"content":"..."} (mutually exclusive with --chat-complete-exec)'
2346
+ ).option(
2347
+ "--chat-complete-exec <cmd>",
2348
+ 'Shell: event JSON on stdin \u2192 stdout JSON {"content":"..."}'
2349
+ ).option(
2350
+ "--chat-complete-timeout <ms>",
2351
+ `Host complete timeout in ms (default ${DEFAULT_COMPLETE_TIMEOUT_MS})`,
2352
+ String(DEFAULT_COMPLETE_TIMEOUT_MS)
1977
2353
  ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(
1978
2354
  (opts) => {
1979
2355
  const config = loadConfig();
@@ -2002,6 +2378,30 @@ function listenCommand() {
2002
2378
  console.error(flagErr);
2003
2379
  process.exit(1);
2004
2380
  }
2381
+ const chatApiBase = opts.chatApiBase?.trim() || process.env.ACN_CHAT_API_BASE?.trim() || process.env.AGENTPLANET_API_BASE?.trim();
2382
+ const chatCompleteUrl = opts.chatCompleteUrl?.trim() || process.env.ACN_CHAT_COMPLETE_URL?.trim();
2383
+ const chatCompleteExec = opts.chatCompleteExec?.trim();
2384
+ if (opts.chatWriteback && !opts.runtime) {
2385
+ console.error("--chat-writeback requires --runtime http|command|log.");
2386
+ process.exit(1);
2387
+ }
2388
+ if (opts.chatWriteback && (opts.chatToken?.trim() || process.env.ACN_CHAT_WRITEBACK_TOKEN?.trim() || process.env.AGENTPLANET_INTERNAL_TOKEN?.trim() || process.env.AGENTPLANET_INTERNAL_API_TOKEN?.trim())) {
2389
+ console.error(
2390
+ "[acn listen] warning: --chat-token / AGENTPLANET_INTERNAL_TOKEN is ignored; writeback authenticates with ACN agent JWT minted from your api-key."
2391
+ );
2392
+ }
2393
+ const chatErr = validateChatWritebackOptions({
2394
+ chatWriteback: opts.chatWriteback,
2395
+ chatApiBase,
2396
+ chatCompleteUrl,
2397
+ chatCompleteExec,
2398
+ agentId,
2399
+ apiKey
2400
+ });
2401
+ if (chatErr) {
2402
+ console.error(chatErr);
2403
+ process.exit(1);
2404
+ }
2005
2405
  let wakeHeaders;
2006
2406
  if (opts.runtime === "http") {
2007
2407
  try {
@@ -2013,6 +2413,10 @@ function listenCommand() {
2013
2413
  }
2014
2414
  const wakeTimeoutMs = Number.parseInt(opts.wakeTimeout ?? "", 10);
2015
2415
  const dedupeTtlSec = Number.parseInt(opts.dedupeTtl ?? "", 10);
2416
+ const chatCompleteTimeoutMs = Number.parseInt(
2417
+ opts.chatCompleteTimeout ?? "",
2418
+ 10
2419
+ );
2016
2420
  if (opts.runtime && (!Number.isFinite(wakeTimeoutMs) || wakeTimeoutMs <= 0)) {
2017
2421
  console.error("--wake-timeout must be a positive integer (ms).");
2018
2422
  process.exit(1);
@@ -2021,6 +2425,21 @@ function listenCommand() {
2021
2425
  console.error("--dedupe-ttl must be a positive integer (seconds).");
2022
2426
  process.exit(1);
2023
2427
  }
2428
+ if (opts.chatWriteback && (!Number.isFinite(chatCompleteTimeoutMs) || chatCompleteTimeoutMs <= 0)) {
2429
+ console.error("--chat-complete-timeout must be a positive integer (ms).");
2430
+ process.exit(1);
2431
+ }
2432
+ const chatWriteback = buildChatWritebackOptions({
2433
+ chatWriteback: opts.chatWriteback,
2434
+ chatApiBase,
2435
+ acnBaseUrl: config.base_url,
2436
+ apiKey,
2437
+ chatCompleteUrl,
2438
+ chatCompleteExec,
2439
+ chatCompleteTimeoutMs: opts.chatWriteback ? chatCompleteTimeoutMs : void 0,
2440
+ agentId,
2441
+ audience: process.env.ACN_CHAT_JWT_AUDIENCE?.trim() || process.env.AGENTPLANET_JWT_AUDIENCE?.trim()
2442
+ });
2024
2443
  runListener({
2025
2444
  agentId,
2026
2445
  apiKey,
@@ -2035,7 +2454,8 @@ function listenCommand() {
2035
2454
  wakeTimeoutMs,
2036
2455
  // commander: --no-dedupe sets dedupe=false; default true
2037
2456
  dedupe: opts.dedupe !== false,
2038
- dedupeTtlSec
2457
+ dedupeTtlSec,
2458
+ chatWriteback
2039
2459
  } : void 0
2040
2460
  });
2041
2461
  }
@@ -3146,6 +3566,102 @@ function orgCommand() {
3146
3566
  handleError(err);
3147
3567
  }
3148
3568
  });
3569
+ cmd.command("publish-task").description(
3570
+ "Publish a Task Pool task attributed to an Org (network by default; not Org work)"
3571
+ ).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(
3572
+ "--pay-from <source>",
3573
+ "Who pays: agent (default, attribution only) | org (Org wallet + credits escrow)",
3574
+ "agent"
3575
+ ).option("--type <type>", "Task type", "general").option("--max-participants <n>", "Max participants", "1").option(
3576
+ "--fence",
3577
+ "Scope task to the Org subnet fence (may deliver task.* to Org harness)",
3578
+ false
3579
+ ).option("--subnet <slug>", "Override subnet slug (implies fence; default: Org fence)").action(
3580
+ async (opts) => {
3581
+ const config = loadConfig();
3582
+ if (!config.api_key) {
3583
+ console.error(
3584
+ "No API key found. Run `acn join` first or `acn config set api-key <key>`."
3585
+ );
3586
+ process.exit(1);
3587
+ }
3588
+ const payFrom = (opts.payFrom ?? "agent").toLowerCase();
3589
+ if (payFrom !== "agent" && payFrom !== "org") {
3590
+ console.error('--pay-from must be "agent" or "org"');
3591
+ process.exit(1);
3592
+ }
3593
+ try {
3594
+ const body = {
3595
+ title: opts.title,
3596
+ description: opts.description,
3597
+ deadline_hours: parseInt(opts.deadline ?? "48", 10),
3598
+ required_tags: opts.tags.split(",").map((s) => s.trim()).filter(Boolean),
3599
+ reward: opts.reward ?? "0",
3600
+ task_type: opts.type ?? "general",
3601
+ max_participants: parseInt(opts.maxParticipants ?? "1", 10),
3602
+ pay_from_org: payFrom === "org",
3603
+ fence: Boolean(opts.fence || opts.subnet)
3604
+ };
3605
+ if (opts.subnet) {
3606
+ body.subnet_slug = opts.subnet;
3607
+ }
3608
+ const task = await acnPost(
3609
+ `/orgs/${opts.org}/publish-task`,
3610
+ body
3611
+ );
3612
+ const lines = [
3613
+ payFrom === "org" ? "Task published \u2014 Org-paid (credits escrow when reward > 0)" : "Task published for Org (Task Pool \u2014 not Org work)",
3614
+ ` Task ID : ${task.task_id}`,
3615
+ ` Org : ${opts.org}`,
3616
+ ` Pay from : ${payFrom}`,
3617
+ ` Creator : ${task.creator_type}/${task.creator_id}`,
3618
+ ` Status : ${task.status}`,
3619
+ ` Title : ${task.title}`,
3620
+ ` Currency : ${task.reward_currency}`,
3621
+ ` Escrow : ${String(task.use_escrow ?? false)}`,
3622
+ ` Subnet : ${task.subnet_slug ?? "(network / unscoped)"}`,
3623
+ ` metadata : org_id=${String(task.metadata?.org_id ?? opts.org)} org_publish=${String(task.metadata?.org_publish ?? true)}`
3624
+ ];
3625
+ if (body.fence) {
3626
+ lines.push(
3627
+ " Note : fenced \u2014 task.* may hit the Org harness webhook"
3628
+ );
3629
+ }
3630
+ output(task, lines.join("\n"));
3631
+ } catch (err) {
3632
+ handleError(err);
3633
+ }
3634
+ }
3635
+ );
3636
+ cmd.command("import-task").description(
3637
+ "Import a Task Pool task as Org work (governance only; links via task.metadata)"
3638
+ ).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(
3639
+ async (opts) => {
3640
+ const config = loadConfig();
3641
+ if (!config.api_key) {
3642
+ console.error(
3643
+ "No API key found. Run `acn join` first or `acn config set api-key <key>`."
3644
+ );
3645
+ process.exit(1);
3646
+ }
3647
+ try {
3648
+ const body = { task_id: opts.task };
3649
+ if (opts.assignee) body.assignee_agent_id = opts.assignee;
3650
+ const res = await acnPost(`/orgs/${opts.org}/work/import-task`, body);
3651
+ const lines = [
3652
+ res.already_imported ? "Task already imported (idempotent)" : "Task imported as Org work",
3653
+ ` Work ID : ${res.work_id}`,
3654
+ ` Org : ${res.org_id}`,
3655
+ ` Task ID : ${res.source_task_id ?? opts.task}`,
3656
+ ` Status : ${res.status}`,
3657
+ ` Title : ${res.title}`
3658
+ ];
3659
+ output(res, lines.join("\n"));
3660
+ } catch (err) {
3661
+ handleError(err);
3662
+ }
3663
+ }
3664
+ );
3149
3665
  return cmd;
3150
3666
  }
3151
3667
 
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.2",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) — zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {