@acnlabs/acn-cli 0.13.3 → 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 +58 -0
  2. package/dist/index.js +897 -41
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -67,6 +67,64 @@ acn heartbeat
67
67
  acn heartbeat --agent-id <id> # override agent ID
68
68
  ```
69
69
 
70
+ ### `acn listen` (Mode B — no public endpoint)
71
+
72
+ Hold an outbound WebSocket and receive relayed A2A requests in real time.
73
+
74
+ **Production (recommended):** built-in A2A receiver + wake your host runtime.
75
+ No local A2A port required.
76
+
77
+ ```bash
78
+ # Register for relay delivery, then:
79
+ acn listen --runtime http \
80
+ --wake-url http://127.0.0.1:10122/hooks/agent \
81
+ --wake-header 'Authorization: Bearer …'
82
+
83
+ acn listen --runtime command --wake-exec '/path/to/wake.sh'
84
+ acn listen --runtime log # debug: print normalized events to stderr
85
+ ```
86
+
87
+ Semantics: CLI answers `message/send` / `message/stream` with a valid A2A
88
+ `accepted` message **immediately**, then POSTs/execs a normalized event to
89
+ wake the host. Wake failure is logged (`wake_failed`) and does **not** fail
90
+ the A2A reply. Dedupe is on by default (`--no-dedupe` to disable).
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
+
110
+ **Coverage:** only traffic that arrives over the Mode B relay. Open Task Pool
111
+ rows that were never pushed as A2A still need `acn tasks list` / reconcile.
112
+
113
+ **Compat (advanced):** tunnel to your own A2A server, or let a subprocess
114
+ print the full JSON-RPC response:
115
+
116
+ ```bash
117
+ acn listen --forward http://127.0.0.1:8080
118
+ acn listen --exec './handle-a2a.sh' # stdout = full A2A JSON-RPC body
119
+ ```
120
+
121
+ > Do not confuse legacy `--exec` with `--runtime command --wake-exec`.
122
+ > The former must emit a protocol-valid A2A response; the latter only wakes
123
+ > the host after the CLI has already answered A2A.
124
+
125
+ Keep `acn listen` and `acn heartbeat` in the same lifecycle for idle agents
126
+ (see [listen + heartbeat systemd example](../../docs/runbooks/acn-listen-heartbeat.md)).
127
+
70
128
  ### `acn agents`
71
129
 
72
130
  Discover agents on ACN.
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.13.3",
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,8 +1383,617 @@ ${formatPolicy(res)}`);
1375
1383
 
1376
1384
  // src/commands/listen.ts
1377
1385
  var import_commander10 = require("commander");
1378
- var import_child_process = require("child_process");
1386
+ var import_child_process3 = require("child_process");
1379
1387
  var import_ws = __toESM(require("ws"));
1388
+
1389
+ // src/commands/chat-writeback.ts
1390
+ var import_child_process = require("child_process");
1391
+
1392
+ // src/commands/normalize-event.ts
1393
+ var CHAT_REPLY_CHANNEL = "agentplanet.chat";
1394
+ function asRecord(v) {
1395
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
1396
+ }
1397
+ function asNonEmptyString(v) {
1398
+ return typeof v === "string" && v.length > 0 ? v : null;
1399
+ }
1400
+ function parseJsonRpcBody(bodyText) {
1401
+ let parsed;
1402
+ try {
1403
+ parsed = JSON.parse(bodyText);
1404
+ } catch {
1405
+ return { ok: false, code: -32700, message: "Parse error" };
1406
+ }
1407
+ const body = asRecord(parsed);
1408
+ if (!body) {
1409
+ return { ok: false, code: -32600, message: "Invalid Request" };
1410
+ }
1411
+ if (body.jsonrpc !== "2.0" || typeof body.method !== "string") {
1412
+ return { ok: false, code: -32600, message: "Invalid Request" };
1413
+ }
1414
+ return { ok: true, body };
1415
+ }
1416
+ function extractTaskId(message) {
1417
+ const metadata = asRecord(message.metadata);
1418
+ if (metadata) {
1419
+ const fromMeta = asNonEmptyString(metadata.task_id) ?? asNonEmptyString(metadata.acn_task_id);
1420
+ if (fromMeta) return fromMeta;
1421
+ }
1422
+ const parts = message.parts;
1423
+ if (!Array.isArray(parts)) return null;
1424
+ for (const part of parts) {
1425
+ const p = asRecord(part);
1426
+ if (!p || p.kind !== "data") continue;
1427
+ const data = asRecord(p.data);
1428
+ if (!data) continue;
1429
+ const fromData = asNonEmptyString(data.task_id) ?? asNonEmptyString(data.acn_task_id);
1430
+ if (fromData) return fromData;
1431
+ }
1432
+ return null;
1433
+ }
1434
+ function extractMessageId(message, generateId) {
1435
+ return asNonEmptyString(message.messageId) ?? asNonEmptyString(message.message_id) ?? generateId();
1436
+ }
1437
+ function extractContextId(message) {
1438
+ return asNonEmptyString(message.contextId) ?? asNonEmptyString(message.context_id);
1439
+ }
1440
+ function extractFromAgent(message) {
1441
+ const metadata = asRecord(message.metadata);
1442
+ if (!metadata) return null;
1443
+ return asNonEmptyString(metadata.from_agent) ?? asNonEmptyString(metadata.fromAgent);
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
+ }
1483
+ function normalizeEvent(body, opts = {}) {
1484
+ const generateId = opts.generateId ?? (() => crypto.randomUUID());
1485
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
1486
+ const params = asRecord(body.params);
1487
+ const message = asRecord(params?.message) ?? {};
1488
+ return {
1489
+ event_type: "a2a_message",
1490
+ task_id: extractTaskId(message),
1491
+ message_id: extractMessageId(message, generateId),
1492
+ context_id: extractContextId(message),
1493
+ from_agent: extractFromAgent(message),
1494
+ chat: extractChatEnvelope(message),
1495
+ received_at: now().toISOString(),
1496
+ raw: body
1497
+ };
1498
+ }
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
+ }
1504
+ return event.task_id ?? event.message_id;
1505
+ }
1506
+ var DedupeStore = class {
1507
+ constructor(ttlSec) {
1508
+ this.ttlSec = ttlSec;
1509
+ }
1510
+ ttlSec;
1511
+ map = /* @__PURE__ */ new Map();
1512
+ /** Returns true if key was already seen within TTL; otherwise marks and returns false. */
1513
+ isDuplicate(key, nowMs = Date.now()) {
1514
+ this.gc(nowMs);
1515
+ const exp = this.map.get(key);
1516
+ if (exp !== void 0 && exp > nowMs) return true;
1517
+ this.map.set(key, nowMs + this.ttlSec * 1e3);
1518
+ return false;
1519
+ }
1520
+ /**
1521
+ * Drop a key so a later retry can wake again.
1522
+ * Used when wake fails after we reserved the slot on accept.
1523
+ */
1524
+ forget(key) {
1525
+ this.map.delete(key);
1526
+ }
1527
+ /** Test helper — current window size after GC. */
1528
+ size(nowMs = Date.now()) {
1529
+ this.gc(nowMs);
1530
+ return this.map.size;
1531
+ }
1532
+ gc(nowMs) {
1533
+ for (const [k, exp] of this.map) {
1534
+ if (exp <= nowMs) this.map.delete(k);
1535
+ }
1536
+ }
1537
+ };
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
+
1751
+ // src/commands/local-receiver.ts
1752
+ var import_crypto = require("crypto");
1753
+
1754
+ // src/commands/runtime-adapter.ts
1755
+ var import_child_process2 = require("child_process");
1756
+ var DEFAULT_WAKE_TIMEOUT_MS = 5e3;
1757
+ function parseWakeHeaders(raw) {
1758
+ const out = {};
1759
+ for (const item of raw ?? []) {
1760
+ const idx = item.indexOf(":");
1761
+ if (idx <= 0) {
1762
+ throw new Error(
1763
+ `Invalid --wake-header "${item}". Expected "Header-Name: value".`
1764
+ );
1765
+ }
1766
+ const key = item.slice(0, idx).trim();
1767
+ const value = item.slice(idx + 1).trim();
1768
+ if (!key) {
1769
+ throw new Error(
1770
+ `Invalid --wake-header "${item}". Expected "Header-Name: value".`
1771
+ );
1772
+ }
1773
+ out[key] = value;
1774
+ }
1775
+ return out;
1776
+ }
1777
+ function validateRuntimeOptions(opts) {
1778
+ if (!opts.runtime) return null;
1779
+ if (opts.runtime !== "http" && opts.runtime !== "command" && opts.runtime !== "log") {
1780
+ return `Unknown --runtime "${opts.runtime}". Use: http | command | log`;
1781
+ }
1782
+ if (opts.runtime === "http" && !opts.wakeUrl) {
1783
+ return "--runtime http requires --wake-url <url>";
1784
+ }
1785
+ if (opts.runtime === "command" && !opts.wakeExec) {
1786
+ return "--runtime command requires --wake-exec <cmd>";
1787
+ }
1788
+ return null;
1789
+ }
1790
+ async function wakeHttp(event, opts, deps) {
1791
+ const fetchFn = deps.fetchFn ?? fetch;
1792
+ const timeoutMs = opts.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS;
1793
+ const controller = new AbortController();
1794
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1795
+ try {
1796
+ const res = await fetchFn(opts.wakeUrl, {
1797
+ method: "POST",
1798
+ headers: {
1799
+ "content-type": "application/json",
1800
+ ...opts.wakeHeaders ?? {}
1801
+ },
1802
+ body: JSON.stringify(event),
1803
+ signal: controller.signal
1804
+ });
1805
+ try {
1806
+ await res.arrayBuffer();
1807
+ } catch {
1808
+ }
1809
+ if (res.status < 200 || res.status >= 300) {
1810
+ return { ok: false, reason: `http_${res.status}` };
1811
+ }
1812
+ return { ok: true };
1813
+ } catch (err) {
1814
+ const msg = err instanceof Error ? err.message : String(err);
1815
+ if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
1816
+ return { ok: false, reason: "timeout" };
1817
+ }
1818
+ return { ok: false, reason: msg.slice(0, 200) };
1819
+ } finally {
1820
+ clearTimeout(timer);
1821
+ }
1822
+ }
1823
+ function wakeCommand(event, opts, deps) {
1824
+ const spawnFn = deps.spawnFn ?? import_child_process2.spawn;
1825
+ const timeoutMs = opts.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS;
1826
+ const body = Buffer.from(JSON.stringify(event), "utf-8");
1827
+ return new Promise((resolve) => {
1828
+ let settled = false;
1829
+ const child = spawnFn(opts.wakeExec, { shell: true });
1830
+ const errOut = [];
1831
+ const finish = (result) => {
1832
+ if (settled) return;
1833
+ settled = true;
1834
+ clearTimeout(timer);
1835
+ resolve(result);
1836
+ };
1837
+ const timer = setTimeout(() => {
1838
+ child.kill("SIGTERM");
1839
+ finish({ ok: false, reason: "timeout" });
1840
+ }, timeoutMs);
1841
+ child.stderr?.on("data", (d) => errOut.push(Buffer.from(d)));
1842
+ child.on(
1843
+ "error",
1844
+ (e) => finish({ ok: false, reason: e.message.slice(0, 200) })
1845
+ );
1846
+ child.on("close", (code) => {
1847
+ if (code === 0) finish({ ok: true });
1848
+ else {
1849
+ const detail = Buffer.concat(errOut).toString("utf-8").slice(0, 80);
1850
+ finish({
1851
+ ok: false,
1852
+ reason: detail ? `exit_${code}:${detail}` : `exit_${code}`
1853
+ });
1854
+ }
1855
+ });
1856
+ child.stdin?.end(body);
1857
+ });
1858
+ }
1859
+ function wakeLog(event, deps) {
1860
+ const logFn = deps.logFn ?? ((line) => console.error(line));
1861
+ logFn(JSON.stringify(event));
1862
+ return { ok: true };
1863
+ }
1864
+ async function wakeRuntime(event, opts, deps = {}) {
1865
+ switch (opts.runtime) {
1866
+ case "http":
1867
+ return wakeHttp(event, opts, deps);
1868
+ case "command":
1869
+ return wakeCommand(event, opts, deps);
1870
+ case "log":
1871
+ return wakeLog(event, deps);
1872
+ default:
1873
+ return { ok: false, reason: `unknown_runtime` };
1874
+ }
1875
+ }
1876
+
1877
+ // src/commands/local-receiver.ts
1878
+ var HANDLED_METHODS = /* @__PURE__ */ new Set(["message/send", "message/stream"]);
1879
+ function jsonRpcResponse(correlationId, jsonrpcId, payload) {
1880
+ return {
1881
+ type: "a2a_response",
1882
+ id: correlationId,
1883
+ status: 200,
1884
+ headers: { "content-type": "application/json" },
1885
+ body: JSON.stringify({
1886
+ jsonrpc: "2.0",
1887
+ id: jsonrpcId ?? null,
1888
+ ...payload
1889
+ })
1890
+ };
1891
+ }
1892
+ function acceptedMessage(correlationId, jsonrpcId, messageId) {
1893
+ return jsonRpcResponse(correlationId, jsonrpcId, {
1894
+ result: {
1895
+ kind: "message",
1896
+ messageId,
1897
+ role: "agent",
1898
+ parts: [{ kind: "text", text: "accepted" }]
1899
+ }
1900
+ });
1901
+ }
1902
+ function jsonRpcError(correlationId, jsonrpcId, code, message) {
1903
+ return jsonRpcResponse(correlationId, jsonrpcId, {
1904
+ error: { code, message }
1905
+ });
1906
+ }
1907
+ function processIncomingRequest(correlationId, bodyText, opts, dedupeStore, deps = {}) {
1908
+ const generateId = deps.generateId ?? (() => (0, import_crypto.randomUUID)());
1909
+ const parsed = parseJsonRpcBody(bodyText);
1910
+ if (!parsed.ok) {
1911
+ return {
1912
+ response: jsonRpcError(correlationId, null, parsed.code, parsed.message),
1913
+ event: null,
1914
+ shouldWake: false,
1915
+ dedupeHit: false
1916
+ };
1917
+ }
1918
+ const { body } = parsed;
1919
+ const jsonrpcId = body.id ?? null;
1920
+ const method = body.method;
1921
+ if (!HANDLED_METHODS.has(method)) {
1922
+ return {
1923
+ response: jsonRpcError(
1924
+ correlationId,
1925
+ jsonrpcId,
1926
+ -32601,
1927
+ `Method not found: ${method}`
1928
+ ),
1929
+ event: null,
1930
+ shouldWake: false,
1931
+ dedupeHit: false
1932
+ };
1933
+ }
1934
+ const event = normalizeEvent(body, {
1935
+ generateId,
1936
+ now: deps.now
1937
+ });
1938
+ const replyMessageId = generateId();
1939
+ const response = acceptedMessage(correlationId, jsonrpcId, replyMessageId);
1940
+ if (opts.dedupe) {
1941
+ const key = dedupeKey(event);
1942
+ if (dedupeStore.isDuplicate(key)) {
1943
+ return { response, event, shouldWake: false, dedupeHit: true };
1944
+ }
1945
+ }
1946
+ return { response, event, shouldWake: true, dedupeHit: false };
1947
+ }
1948
+ function formatWakeFailed(event, reason) {
1949
+ const task = event.task_id ?? "-";
1950
+ return `[acn listen] wake_failed message_id=${event.message_id} task_id=${task} reason=${reason}`;
1951
+ }
1952
+ function formatDeduped(event) {
1953
+ return `[acn listen] deduped key=${dedupeKey(event)}`;
1954
+ }
1955
+ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send, deps = {}) {
1956
+ const logFn = deps.logFn ?? ((line) => console.error(line));
1957
+ const result = processIncomingRequest(
1958
+ correlationId,
1959
+ bodyText,
1960
+ opts,
1961
+ dedupeStore,
1962
+ deps
1963
+ );
1964
+ send(result.response);
1965
+ if (result.dedupeHit && result.event) {
1966
+ logFn(formatDeduped(result.event));
1967
+ return;
1968
+ }
1969
+ if (!result.shouldWake || !result.event) return;
1970
+ const event = result.event;
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
+ }
1984
+ void wakeRuntime(event, opts, deps).then((wake) => {
1985
+ if (!wake.ok) {
1986
+ if (key) dedupeStore.forget(key);
1987
+ logFn(formatWakeFailed(event, wake.reason));
1988
+ }
1989
+ }).catch((err) => {
1990
+ if (key) dedupeStore.forget(key);
1991
+ const msg = err instanceof Error ? err.message : String(err);
1992
+ logFn(formatWakeFailed(event, msg.slice(0, 200)));
1993
+ });
1994
+ }
1995
+
1996
+ // src/commands/listen.ts
1380
1997
  var STRIP_HEADERS = /* @__PURE__ */ new Set([
1381
1998
  "host",
1382
1999
  "content-length",
@@ -1408,12 +2025,28 @@ function errorResponse(id, status, detail) {
1408
2025
  async function dispatchA2aRequest(frame, opts, send, deps = {}) {
1409
2026
  const bodyBuf = decodeBody(frame);
1410
2027
  try {
2028
+ if (opts.runtime) {
2029
+ const store = deps.dedupeStore ?? new DedupeStore(opts.runtime.dedupeTtlSec);
2030
+ dispatchLocalReceiver(
2031
+ frame.id,
2032
+ bodyBuf.toString("utf-8"),
2033
+ opts.runtime,
2034
+ store,
2035
+ send,
2036
+ {
2037
+ fetchFn: deps.fetchFn,
2038
+ spawnFn: deps.spawnFn,
2039
+ logFn: deps.logFn
2040
+ }
2041
+ );
2042
+ return;
2043
+ }
1411
2044
  if (opts.forward) {
1412
2045
  await forwardToHttp(frame, bodyBuf, opts.forward, deps.fetchFn ?? fetch, send);
1413
2046
  return;
1414
2047
  }
1415
2048
  if (opts.exec) {
1416
- send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process.spawn));
2049
+ send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
1417
2050
  return;
1418
2051
  }
1419
2052
  send(errorResponse(frame.id, 500, "no handler configured"));
@@ -1518,13 +2151,15 @@ function runListener(cfg) {
1518
2151
  const wsUrl = toWebsocketUrl(cfg.baseUrl, cfg.agentId);
1519
2152
  let backoff = INITIAL_BACKOFF_MS;
1520
2153
  let stopped = false;
2154
+ const dedupeStore = cfg.runtime ? new DedupeStore(cfg.runtime.dedupeTtlSec) : void 0;
1521
2155
  const connect = () => {
1522
2156
  const ws = new import_ws.default(wsUrl, {
1523
2157
  headers: { Authorization: `Bearer ${cfg.apiKey}` }
1524
2158
  });
1525
2159
  let keepalive;
1526
2160
  ws.on("open", () => {
1527
- console.error(`[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl}`);
2161
+ const mode = cfg.runtime ? `runtime=${cfg.runtime.runtime}` : cfg.forward ? `forward=${cfg.forward}` : `exec`;
2162
+ console.error(`[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl} (${mode})`);
1528
2163
  backoff = INITIAL_BACKOFF_MS;
1529
2164
  keepalive = setInterval(() => {
1530
2165
  if (ws.readyState === import_ws.default.OPEN) {
@@ -1547,7 +2182,7 @@ function runListener(cfg) {
1547
2182
  ws.send(JSON.stringify(out));
1548
2183
  }
1549
2184
  };
1550
- void dispatchA2aRequest(f, cfg, send);
2185
+ void dispatchA2aRequest(f, cfg, send, { dedupeStore });
1551
2186
  }
1552
2187
  });
1553
2188
  ws.on("close", (code, reason) => {
@@ -1576,47 +2211,171 @@ function runListener(cfg) {
1576
2211
  });
1577
2212
  connect();
1578
2213
  }
2214
+ function collectWakeHeader(value, previous) {
2215
+ previous.push(value);
2216
+ return previous;
2217
+ }
2218
+ function validateListenHandlerFlags(opts) {
2219
+ const modes = [opts.runtime, opts.forward, opts.exec].filter(Boolean);
2220
+ if (modes.length === 0) {
2221
+ return "Provide a handler: --runtime http|command|log (recommended), or legacy --forward <url> / --exec <command>.";
2222
+ }
2223
+ if (modes.length > 1) {
2224
+ return "Use only one handler: --runtime, --forward, or --exec \u2014 not combined.";
2225
+ }
2226
+ return validateRuntimeOptions({
2227
+ runtime: opts.runtime,
2228
+ wakeUrl: opts.wakeUrl,
2229
+ wakeExec: opts.wakeExec
2230
+ });
2231
+ }
1579
2232
  function listenCommand() {
1580
2233
  const cmd = new import_commander10.Command("listen").description(
1581
- "Hold an outbound connection to ACN and answer relayed A2A requests in real time (ADR-0012 Mode B). For agents with no public endpoint."
2234
+ "Hold an outbound connection to ACN and answer relayed A2A requests in real time (ADR-0012 Mode B). Prefer --runtime for production; --forward/--exec remain as compatibility tunnels."
2235
+ ).option(
2236
+ "--runtime <id>",
2237
+ "Built-in A2A receiver + wake host: http | command | log (no local A2A port)"
2238
+ ).option("--wake-url <url>", "POST target for --runtime http").option(
2239
+ "--wake-header <k:v>",
2240
+ "Extra header for --runtime http (repeatable)",
2241
+ collectWakeHeader,
2242
+ []
2243
+ ).option(
2244
+ "--wake-exec <cmd>",
2245
+ "Shell command for --runtime command (event JSON on stdin). Not the same as legacy --exec (which must print a full A2A response)."
1582
2246
  ).option(
2247
+ "--wake-timeout <ms>",
2248
+ "Wake timeout in ms (default 5000)",
2249
+ String(DEFAULT_WAKE_TIMEOUT_MS)
2250
+ ).option("--no-dedupe", "Disable in-process task/message id dedupe (default: on)").option("--dedupe-ttl <sec>", "Dedupe window seconds (default 3600)", "3600").option(
1583
2251
  "--forward <url>",
1584
- "Tunnel each relayed request to a local HTTP server (e.g. http://localhost:8080)"
2252
+ "Compat: tunnel each request to a local A2A HTTP server"
1585
2253
  ).option(
1586
2254
  "--exec <command>",
1587
- "Run a shell command per request: body on stdin, stdout becomes the response"
1588
- ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action((opts) => {
1589
- const config = loadConfig();
1590
- const apiKey = config.api_key;
1591
- const agentId = opts.agentId ?? config.agent_id;
1592
- if (!apiKey) {
1593
- console.error(
1594
- "No API key found. Run `acn join` first or `acn config set api-key <key>`."
1595
- );
1596
- process.exit(1);
1597
- }
1598
- if (!agentId) {
1599
- console.error(
1600
- "No agent ID found. Run `acn join` first or `acn config set agent-id <id>`."
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)
2275
+ ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(
2276
+ (opts) => {
2277
+ const config = loadConfig();
2278
+ const apiKey = config.api_key;
2279
+ const agentId = opts.agentId ?? config.agent_id;
2280
+ if (!apiKey) {
2281
+ console.error(
2282
+ "No API key found. Run `acn join` first or `acn config set api-key <key>`."
2283
+ );
2284
+ process.exit(1);
2285
+ }
2286
+ if (!agentId) {
2287
+ console.error(
2288
+ "No agent ID found. Run `acn join` first or `acn config set agent-id <id>`."
2289
+ );
2290
+ process.exit(1);
2291
+ }
2292
+ const flagErr = validateListenHandlerFlags({
2293
+ runtime: opts.runtime,
2294
+ forward: opts.forward,
2295
+ exec: opts.exec,
2296
+ wakeUrl: opts.wakeUrl,
2297
+ wakeExec: opts.wakeExec
2298
+ });
2299
+ if (flagErr) {
2300
+ console.error(flagErr);
2301
+ process.exit(1);
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
+ }
2323
+ let wakeHeaders;
2324
+ if (opts.runtime === "http") {
2325
+ try {
2326
+ wakeHeaders = parseWakeHeaders(opts.wakeHeader);
2327
+ } catch (err) {
2328
+ console.error(err instanceof Error ? err.message : String(err));
2329
+ process.exit(1);
2330
+ }
2331
+ }
2332
+ const wakeTimeoutMs = Number.parseInt(opts.wakeTimeout ?? "", 10);
2333
+ const dedupeTtlSec = Number.parseInt(opts.dedupeTtl ?? "", 10);
2334
+ const chatCompleteTimeoutMs = Number.parseInt(
2335
+ opts.chatCompleteTimeout ?? "",
2336
+ 10
1601
2337
  );
1602
- process.exit(1);
1603
- }
1604
- if (!opts.forward && !opts.exec) {
1605
- console.error("Provide a handler: --forward <url> or --exec <command>.");
1606
- process.exit(1);
1607
- }
1608
- if (opts.forward && opts.exec) {
1609
- console.error("Use only one handler: --forward or --exec, not both.");
1610
- process.exit(1);
2338
+ if (opts.runtime && (!Number.isFinite(wakeTimeoutMs) || wakeTimeoutMs <= 0)) {
2339
+ console.error("--wake-timeout must be a positive integer (ms).");
2340
+ process.exit(1);
2341
+ }
2342
+ if (opts.runtime && (!Number.isFinite(dedupeTtlSec) || dedupeTtlSec <= 0)) {
2343
+ console.error("--dedupe-ttl must be a positive integer (seconds).");
2344
+ process.exit(1);
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
+ });
2359
+ runListener({
2360
+ agentId,
2361
+ apiKey,
2362
+ baseUrl: config.base_url,
2363
+ forward: opts.forward,
2364
+ exec: opts.exec,
2365
+ runtime: opts.runtime ? {
2366
+ runtime: opts.runtime,
2367
+ wakeUrl: opts.wakeUrl,
2368
+ wakeHeaders,
2369
+ wakeExec: opts.wakeExec,
2370
+ wakeTimeoutMs,
2371
+ // commander: --no-dedupe sets dedupe=false; default true
2372
+ dedupe: opts.dedupe !== false,
2373
+ dedupeTtlSec,
2374
+ chatWriteback
2375
+ } : void 0
2376
+ });
1611
2377
  }
1612
- runListener({
1613
- agentId,
1614
- apiKey,
1615
- baseUrl: config.base_url,
1616
- forward: opts.forward,
1617
- exec: opts.exec
1618
- });
1619
- });
2378
+ );
1620
2379
  return cmd;
1621
2380
  }
1622
2381
 
@@ -1707,8 +2466,9 @@ function deliveryCommand() {
1707
2466
  );
1708
2467
  const followUp = res.delivery === "relay" ? [
1709
2468
  "",
1710
- "Next: keep a local A2A handler up, then:",
1711
- " acn listen --forward http://localhost:PORT"
2469
+ "Next: run the Mode B listener (built-in A2A + wake host):",
2470
+ " acn listen --runtime http --wake-url http://127.0.0.1:PORT/wake",
2471
+ "Compat: acn listen --forward http://localhost:PORT"
1712
2472
  ] : [];
1713
2473
  output(res, [formatDelivery(res), ...followUp].join("\n"));
1714
2474
  } catch (err) {
@@ -2722,6 +3482,102 @@ function orgCommand() {
2722
3482
  handleError(err);
2723
3483
  }
2724
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
+ );
2725
3581
  return cmd;
2726
3582
  }
2727
3583
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "0.13.3",
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": {