@acnlabs/acn-cli 1.0.7 → 1.0.9

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 +6 -1
  2. package/dist/index.js +127 -158
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,8 +51,12 @@ Register this agent with ACN. Saves `api_key` and `agent_id` automatically.
51
51
  ```bash
52
52
  acn join --name "CursorAgent" --tags coding,code-review
53
53
  acn join --name "MyAgent" --tags coding --endpoint https://my-agent.example.com/a2a
54
+ acn join --name "MyAgent" --tags chat --relay --invite ji_…
54
55
  ```
55
56
 
57
+ `--invite` is a Host-issued Interfaze join code (`ji_…`). It is **not** an
58
+ owner account id. Omit it when you are not connecting through a `/join` page.
59
+
56
60
  ### `acn heartbeat`
57
61
 
58
62
  Send a heartbeat to remain `online`. **Most agents do not need to run
@@ -108,7 +112,8 @@ acn listen --runtime http \
108
112
 
109
113
  `--chat-token` is deprecated/ignored. Task / Org wakes still use `--wake-url` /
110
114
  `--wake-exec`. Chat envelopes skip wake and use the complete → writeback path
111
- instead.
115
+ instead. CLI **1.0.9+** completes official hops via Host; `--chat-complete-*`
116
+ is BYO only and may be omitted for official-only.
112
117
 
113
118
  **Coverage:** only traffic that arrives over the Mode B relay. Open Task Pool
114
119
  rows that were never pushed as A2A still need `acn tasks list` / reconcile.
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: "1.0.7",
34
+ version: "1.0.9",
35
35
  description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -365,6 +365,9 @@ function joinCommand() {
365
365
  ).option(
366
366
  "--base-url <url>",
367
367
  "Override ACN origin (no /api/v1). Overrides --region and ACN_BASE_URL for this join."
368
+ ).option(
369
+ "--invite <code>",
370
+ "Host-issued human join invite (ji_\u2026). Stored as metadata only \u2014 not an owner account."
368
371
  ).action(
369
372
  async (opts) => {
370
373
  if (opts.region && opts.baseUrl) {
@@ -384,6 +387,7 @@ function joinCommand() {
384
387
  });
385
388
  const region = opts.region?.trim().toLowerCase() === "cn" || opts.region?.trim().toLowerCase() === "global" ? opts.region.trim().toLowerCase() : inferRegion(base_url);
386
389
  const tags = opts.tags.split(",").map((s) => s.trim()).filter(Boolean);
390
+ const invite = opts.invite?.trim();
387
391
  const body = {
388
392
  name: opts.name,
389
393
  description: opts.description ?? `${opts.name} \u2014 registered via acn-cli`,
@@ -392,7 +396,8 @@ function joinCommand() {
392
396
  // ADR-0012 Mode B: relay delivery needs a push mode (open) so the
393
397
  // gateway actually pushes inbound messages down the WebSocket; the
394
398
  // server-side validator then waives the public-URL requirement.
395
- ...opts.relay ? { delivery: "relay", communication_policy: { mode: "open" } } : {}
399
+ ...opts.relay ? { delivery: "relay", communication_policy: { mode: "open" } } : {},
400
+ ...invite ? { invite } : {}
396
401
  };
397
402
  try {
398
403
  const res = await acnPost("/agents/join", body, {
@@ -1751,112 +1756,11 @@ var DedupeStore = class {
1751
1756
 
1752
1757
  // src/commands/official-hop-door.ts
1753
1758
  var import_node_http = __toESM(require("http"));
1754
- function shouldOpenOfficialDoor(opts) {
1759
+ function canCompleteOfficialHop(opts) {
1755
1760
  return Boolean(
1756
1761
  opts.inferencePath === "official" && opts.hopId?.trim() && asHostInferenceUrl(opts.hostInferenceUrl) && opts.jwt?.trim()
1757
1762
  );
1758
1763
  }
1759
- function readBody(req) {
1760
- return new Promise((resolve, reject) => {
1761
- const chunks = [];
1762
- req.on("data", (c) => chunks.push(Buffer.from(c)));
1763
- req.on("end", () => resolve(Buffer.concat(chunks)));
1764
- req.on("error", reject);
1765
- });
1766
- }
1767
- function send(res, status, body, contentType = "application/json") {
1768
- res.writeHead(status, {
1769
- "content-type": contentType,
1770
- "content-length": String(body.length)
1771
- });
1772
- res.end(body);
1773
- }
1774
- async function handleDoorRequest(req, res, opts) {
1775
- const path = (req.url ?? "").split("?")[0];
1776
- if (req.method !== "POST" || path !== "/v1/chat/completions" && path !== "/chat/completions") {
1777
- send(res, 404, Buffer.from('{"error":"not_found"}'));
1778
- return;
1779
- }
1780
- let payload;
1781
- try {
1782
- const raw = await readBody(req);
1783
- payload = JSON.parse(raw.length ? raw.toString("utf-8") : "{}");
1784
- } catch {
1785
- send(res, 400, Buffer.from('{"error":"invalid_json"}'));
1786
- return;
1787
- }
1788
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
1789
- send(res, 400, Buffer.from('{"error":"invalid_json"}'));
1790
- return;
1791
- }
1792
- const body = { ...payload };
1793
- delete body.agent_id;
1794
- body.hop_id = opts.hopId;
1795
- const headers = {
1796
- authorization: `Bearer ${opts.jwt}`,
1797
- "content-type": "application/json",
1798
- "X-Hop-Id": opts.hopId
1799
- };
1800
- if (opts.agentId) headers["X-Agent-Id"] = opts.agentId;
1801
- try {
1802
- const upstream = await opts.fetchFn(opts.upstream, {
1803
- method: "POST",
1804
- headers,
1805
- body: JSON.stringify(body)
1806
- });
1807
- const out = Buffer.from(await upstream.arrayBuffer());
1808
- const ct = upstream.headers.get("content-type") || "application/json";
1809
- send(res, upstream.status, out, ct);
1810
- } catch (err) {
1811
- const msg = err instanceof Error ? err.message : String(err);
1812
- send(
1813
- res,
1814
- 502,
1815
- Buffer.from(JSON.stringify({ error: `upstream_unreachable:${msg.slice(0, 120)}` }))
1816
- );
1817
- }
1818
- }
1819
- function closeServer(server) {
1820
- return new Promise((resolve) => {
1821
- server.closeAllConnections?.();
1822
- server.close(() => resolve());
1823
- });
1824
- }
1825
- async function startOfficialHopDoor(opts) {
1826
- const dest = asHostInferenceUrl(opts.hostInferenceUrl);
1827
- const hopId = opts.hopId.trim();
1828
- const jwt = opts.jwt.trim();
1829
- if (!dest || !hopId || !jwt) return null;
1830
- const fetchFn = opts.fetchFn ?? fetch;
1831
- const upstream = `${dest}/chat/completions`;
1832
- const server = import_node_http.default.createServer((req, res) => {
1833
- void handleDoorRequest(req, res, {
1834
- upstream,
1835
- hopId,
1836
- agentId: opts.agentId,
1837
- jwt,
1838
- fetchFn
1839
- });
1840
- });
1841
- try {
1842
- await new Promise((resolve, reject) => {
1843
- server.once("error", reject);
1844
- server.listen(0, "127.0.0.1", () => resolve());
1845
- });
1846
- } catch {
1847
- return null;
1848
- }
1849
- const addr = server.address();
1850
- const port = typeof addr === "object" && addr ? addr.port : 0;
1851
- if (!port) {
1852
- await closeServer(server);
1853
- return null;
1854
- }
1855
- return {
1856
- baseUrl: `http://127.0.0.1:${port}/v1`,
1857
- close: () => closeServer(server)
1858
- };
1859
- }
1860
1764
 
1861
1765
  // src/commands/chat-writeback.ts
1862
1766
  var DEFAULT_COMPLETE_TIMEOUT_MS = 12e4;
@@ -1879,8 +1783,8 @@ function validateChatWritebackOptions(opts) {
1879
1783
  }
1880
1784
  const hasUrl = Boolean(opts.chatCompleteUrl?.trim());
1881
1785
  const hasExec = Boolean(opts.chatCompleteExec?.trim());
1882
- if (hasUrl === hasExec) {
1883
- return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."} and optional usage).';
1786
+ if (hasUrl && hasExec) {
1787
+ return "--chat-writeback: --chat-complete-url and --chat-complete-exec are mutually exclusive (omit both for official-only; BYO hops need exactly one).";
1884
1788
  }
1885
1789
  return null;
1886
1790
  }
@@ -1914,6 +1818,28 @@ function extractContent(payload) {
1914
1818
  }
1915
1819
  return null;
1916
1820
  }
1821
+ function extractChatCompletionContent(payload) {
1822
+ const rec = asRecord2(payload);
1823
+ if (!rec) return null;
1824
+ const choices = rec.choices;
1825
+ if (Array.isArray(choices) && choices.length > 0) {
1826
+ const first = asRecord2(choices[0]);
1827
+ if (first) {
1828
+ const msg = asRecord2(first.message);
1829
+ const fromMsg = msg?.content;
1830
+ if (typeof fromMsg === "string" && fromMsg.trim()) {
1831
+ const t = fromMsg.trim();
1832
+ if (t.toLowerCase() !== "accepted") return t;
1833
+ }
1834
+ const fromText = first.text;
1835
+ if (typeof fromText === "string" && fromText.trim()) {
1836
+ const t = fromText.trim();
1837
+ if (t.toLowerCase() !== "accepted") return t;
1838
+ }
1839
+ }
1840
+ }
1841
+ return extractContent(payload);
1842
+ }
1917
1843
  function asNonNegInt(v) {
1918
1844
  if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
1919
1845
  return Math.floor(v);
@@ -2133,44 +2059,72 @@ function spawnCompleteExec(event, opts, deps, jwt, door) {
2133
2059
  child.stdin?.end(body);
2134
2060
  });
2135
2061
  }
2136
- async function completeViaExec(event, opts, deps, jwt) {
2137
- let door = null;
2062
+ async function completeOfficialViaHost(event, opts, deps, jwt) {
2063
+ const chat = event.chat;
2064
+ if (!chat) return { ok: false, reason: "no_chat_envelope" };
2065
+ if (!canCompleteOfficialHop({
2066
+ inferencePath: chat.inference_path,
2067
+ hopId: chat.hop_id,
2068
+ hostInferenceUrl: chat.host_inference_url,
2069
+ jwt
2070
+ })) {
2071
+ return { ok: false, reason: "official_complete_skipped" };
2072
+ }
2073
+ const model = chat.requested_model?.trim();
2074
+ const text = chat.user_text?.trim();
2075
+ if (!model) return { ok: false, reason: "official_complete_missing_model" };
2076
+ if (!text) return { ok: false, reason: "official_complete_missing_text" };
2138
2077
  const logFn = deps.logFn ?? ((line) => console.error(line));
2078
+ logFn(
2079
+ `[acn listen] official_complete chat_id=${chat.chat_id} model=${model}`
2080
+ );
2081
+ const fetchFn = deps.fetchFn ?? fetch;
2082
+ const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
2083
+ const controller = new AbortController();
2084
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2139
2085
  try {
2140
- if (shouldOpenOfficialDoor({
2141
- inferencePath: event.chat?.inference_path,
2142
- hopId: event.chat?.hop_id,
2143
- hostInferenceUrl: event.chat?.host_inference_url,
2144
- jwt
2145
- })) {
2146
- try {
2147
- door = await startOfficialHopDoor({
2148
- hostInferenceUrl: event.chat.host_inference_url,
2149
- hopId: event.chat.hop_id,
2150
- agentId: opts.agentId,
2151
- jwt,
2152
- fetchFn: deps.fetchFn
2153
- });
2154
- } catch {
2155
- door = null;
2156
- }
2157
- if (door) {
2158
- logFn(
2159
- `[acn listen] official_door chat_id=${event.chat.chat_id} base=${door.baseUrl}`
2160
- );
2161
- } else {
2162
- logFn(
2163
- `[acn listen] official_door_skipped chat_id=${event.chat.chat_id}`
2164
- );
2165
- }
2086
+ const res = await fetchFn(`${chat.host_inference_url}/chat/completions`, {
2087
+ method: "POST",
2088
+ headers: {
2089
+ authorization: `Bearer ${jwt}`,
2090
+ "content-type": "application/json",
2091
+ "X-Hop-Id": chat.hop_id,
2092
+ "X-Agent-Id": opts.agentId
2093
+ },
2094
+ body: JSON.stringify({
2095
+ model,
2096
+ messages: [{ role: "user", content: text }],
2097
+ hop_id: chat.hop_id,
2098
+ ...chat.max_output_tokens && chat.max_output_tokens > 0 ? { max_tokens: chat.max_output_tokens } : {}
2099
+ }),
2100
+ signal: controller.signal
2101
+ });
2102
+ const raw = await res.text();
2103
+ if (res.status < 200 || res.status >= 300) {
2104
+ return { ok: false, reason: `official_complete_http_${res.status}` };
2166
2105
  }
2167
- return await spawnCompleteExec(event, opts, deps, jwt, door);
2168
- } finally {
2169
- if (door) {
2170
- await door.close().catch(() => void 0);
2106
+ let payload;
2107
+ try {
2108
+ payload = JSON.parse(raw);
2109
+ } catch {
2110
+ return { ok: false, reason: "official_complete_invalid_json" };
2111
+ }
2112
+ const content = extractChatCompletionContent(payload);
2113
+ if (!content) return { ok: false, reason: "official_complete_missing_content" };
2114
+ return { ok: true, result: { content } };
2115
+ } catch (err) {
2116
+ const msg = err instanceof Error ? err.message : String(err);
2117
+ if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
2118
+ return { ok: false, reason: "official_complete_timeout" };
2171
2119
  }
2120
+ return { ok: false, reason: msg.slice(0, 200) };
2121
+ } finally {
2122
+ clearTimeout(timer);
2172
2123
  }
2173
2124
  }
2125
+ async function completeViaExec(event, opts, deps, jwt) {
2126
+ return spawnCompleteExec(event, opts, deps, jwt);
2127
+ }
2174
2128
  async function postWriteback(event, complete, opts, deps) {
2175
2129
  const chat = event.chat;
2176
2130
  if (!chat) return { ok: false, reason: "no_chat_envelope" };
@@ -2290,11 +2244,26 @@ async function handleChatWriteback(event, opts, deps = {}) {
2290
2244
  const logFn = deps.logFn ?? ((line) => console.error(line));
2291
2245
  if (!event.chat) return { ok: false, reason: "no_chat_envelope" };
2292
2246
  let jwt = null;
2293
- if (event.chat.inference_path === "official" && opts.completeExec) {
2247
+ if (event.chat.inference_path === "official") {
2294
2248
  const minted = await mintAgentJwt(opts, deps.fetchFn ?? fetch);
2295
- if (minted.ok) jwt = minted.token;
2249
+ if (!minted.ok) {
2250
+ logFn(
2251
+ `[acn listen] official_jwt_failed chat_id=${event.chat.chat_id} reason=${minted.reason}`
2252
+ );
2253
+ return { ok: false, reason: minted.reason };
2254
+ }
2255
+ jwt = minted.token;
2256
+ }
2257
+ let completed;
2258
+ if (event.chat.inference_path === "official") {
2259
+ completed = await completeOfficialViaHost(event, opts, deps, jwt);
2260
+ } else if (opts.completeUrl) {
2261
+ completed = await completeViaHttp(event, opts, deps);
2262
+ } else if (opts.completeExec) {
2263
+ completed = await completeViaExec(event, opts, deps, jwt);
2264
+ } else {
2265
+ completed = { ok: false, reason: "byo_complete_missing" };
2296
2266
  }
2297
- const completed = opts.completeUrl ? await completeViaHttp(event, opts, deps) : await completeViaExec(event, opts, deps, jwt);
2298
2267
  if (!completed.ok) {
2299
2268
  logFn(
2300
2269
  `[acn listen] chat_complete_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${completed.reason}`
@@ -2519,7 +2488,7 @@ function formatWakeFailed(event, reason) {
2519
2488
  function formatDeduped(event) {
2520
2489
  return `[acn listen] deduped key=${dedupeKey(event)}`;
2521
2490
  }
2522
- function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send2, deps = {}) {
2491
+ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send, deps = {}) {
2523
2492
  const logFn = deps.logFn ?? ((line) => console.error(line));
2524
2493
  const result = processIncomingRequest(
2525
2494
  correlationId,
@@ -2528,7 +2497,7 @@ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send2
2528
2497
  dedupeStore,
2529
2498
  deps
2530
2499
  );
2531
- send2(result.response);
2500
+ send(result.response);
2532
2501
  if (result.dedupeHit && result.event) {
2533
2502
  logFn(formatDeduped(result.event));
2534
2503
  return;
@@ -2589,7 +2558,7 @@ function errorResponse(id, status, detail) {
2589
2558
  body: JSON.stringify({ error: detail })
2590
2559
  };
2591
2560
  }
2592
- async function dispatchA2aRequest(frame, opts, send2, deps = {}) {
2561
+ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
2593
2562
  const bodyBuf = decodeBody(frame);
2594
2563
  try {
2595
2564
  if (opts.runtime) {
@@ -2599,7 +2568,7 @@ async function dispatchA2aRequest(frame, opts, send2, deps = {}) {
2599
2568
  bodyBuf.toString("utf-8"),
2600
2569
  opts.runtime,
2601
2570
  store,
2602
- send2,
2571
+ send,
2603
2572
  {
2604
2573
  fetchFn: deps.fetchFn,
2605
2574
  spawnFn: deps.spawnFn,
@@ -2609,17 +2578,17 @@ async function dispatchA2aRequest(frame, opts, send2, deps = {}) {
2609
2578
  return;
2610
2579
  }
2611
2580
  if (opts.forward) {
2612
- await forwardToHttp(frame, bodyBuf, opts.forward, deps.fetchFn ?? fetch, send2);
2581
+ await forwardToHttp(frame, bodyBuf, opts.forward, deps.fetchFn ?? fetch, send);
2613
2582
  return;
2614
2583
  }
2615
2584
  if (opts.exec) {
2616
- send2(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
2585
+ send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
2617
2586
  return;
2618
2587
  }
2619
- send2(errorResponse(frame.id, 500, "no handler configured"));
2588
+ send(errorResponse(frame.id, 500, "no handler configured"));
2620
2589
  } catch (err) {
2621
2590
  const msg = err instanceof Error ? err.message : String(err);
2622
- send2(errorResponse(frame.id, 502, `handler failed: ${msg}`));
2591
+ send(errorResponse(frame.id, 502, `handler failed: ${msg}`));
2623
2592
  }
2624
2593
  }
2625
2594
  function buildForwardHeaders(frame) {
@@ -2629,7 +2598,7 @@ function buildForwardHeaders(frame) {
2629
2598
  }
2630
2599
  return headers;
2631
2600
  }
2632
- async function forwardToHttp(frame, bodyBuf, base, fetchFn, send2) {
2601
+ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send) {
2633
2602
  const suffix = frame.path && frame.path !== "/" ? "/" + frame.path.replace(/^\//, "") : "";
2634
2603
  const targetUrl = base.replace(/\/$/, "") + suffix;
2635
2604
  const method = (frame.method ?? "POST").toUpperCase();
@@ -2647,7 +2616,7 @@ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send2) {
2647
2616
  const { done, value } = await reader.read();
2648
2617
  if (done) break;
2649
2618
  if (value && value.length > 0) {
2650
- send2({
2619
+ send({
2651
2620
  type: "a2a_stream_chunk",
2652
2621
  id: frame.id,
2653
2622
  seq: seq++,
@@ -2656,17 +2625,17 @@ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send2) {
2656
2625
  });
2657
2626
  }
2658
2627
  }
2659
- send2({ type: "a2a_stream_end", id: frame.id, status: res.status });
2628
+ send({ type: "a2a_stream_end", id: frame.id, status: res.status });
2660
2629
  } catch (err) {
2661
2630
  const msg = err instanceof Error ? err.message : String(err);
2662
- send2({ type: "a2a_stream_end", id: frame.id, error: msg });
2631
+ send({ type: "a2a_stream_end", id: frame.id, error: msg });
2663
2632
  } finally {
2664
2633
  reader.releaseLock();
2665
2634
  }
2666
2635
  return;
2667
2636
  }
2668
2637
  const respText = await res.text();
2669
- send2({
2638
+ send({
2670
2639
  type: "a2a_response",
2671
2640
  id: frame.id,
2672
2641
  status: res.status,
@@ -2777,12 +2746,12 @@ function runListener(cfg) {
2777
2746
  if (!frame || typeof frame !== "object") return;
2778
2747
  const f = frame;
2779
2748
  if (f.type === "a2a_request" && typeof f.id === "string") {
2780
- const send2 = (out) => {
2749
+ const send = (out) => {
2781
2750
  if (ws.readyState === import_ws.default.OPEN) {
2782
2751
  ws.send(JSON.stringify(out));
2783
2752
  }
2784
2753
  };
2785
- void dispatchA2aRequest(f, cfg, send2, { dedupeStore });
2754
+ void dispatchA2aRequest(f, cfg, send, { dedupeStore });
2786
2755
  }
2787
2756
  });
2788
2757
  ws.on("close", (code, reason) => {
@@ -2865,10 +2834,10 @@ function listenCommand() {
2865
2834
  "Deprecated/ignored: writeback now mints ACN agent JWT from api-key"
2866
2835
  ).option(
2867
2836
  "--chat-complete-url <url>",
2868
- 'POST NormalizedEvent \u2192 JSON {"content":"..."} (mutually exclusive with --chat-complete-exec)'
2837
+ "BYO complete URL (optional if official-only; mutually exclusive with --chat-complete-exec)"
2869
2838
  ).option(
2870
2839
  "--chat-complete-exec <cmd>",
2871
- 'Shell: event JSON on stdin \u2192 stdout JSON {"content":"..."}'
2840
+ 'BYO only (optional if official-only): stdin event \u2192 stdout {"content"}. Official hops complete via Host.'
2872
2841
  ).option(
2873
2842
  "--chat-complete-timeout <ms>",
2874
2843
  `Host complete timeout in ms (default ${DEFAULT_COMPLETE_TIMEOUT_MS})`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {