@acnlabs/acn-cli 1.0.8 → 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 +2 -1
  2. package/dist/index.js +121 -157
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -112,7 +112,8 @@ acn listen --runtime http \
112
112
 
113
113
  `--chat-token` is deprecated/ignored. Task / Org wakes still use `--wake-url` /
114
114
  `--wake-exec`. Chat envelopes skip wake and use the complete → writeback path
115
- 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.
116
117
 
117
118
  **Coverage:** only traffic that arrives over the Mode B relay. Open Task Pool
118
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.8",
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: {
@@ -1756,112 +1756,11 @@ var DedupeStore = class {
1756
1756
 
1757
1757
  // src/commands/official-hop-door.ts
1758
1758
  var import_node_http = __toESM(require("http"));
1759
- function shouldOpenOfficialDoor(opts) {
1759
+ function canCompleteOfficialHop(opts) {
1760
1760
  return Boolean(
1761
1761
  opts.inferencePath === "official" && opts.hopId?.trim() && asHostInferenceUrl(opts.hostInferenceUrl) && opts.jwt?.trim()
1762
1762
  );
1763
1763
  }
1764
- function readBody(req) {
1765
- return new Promise((resolve, reject) => {
1766
- const chunks = [];
1767
- req.on("data", (c) => chunks.push(Buffer.from(c)));
1768
- req.on("end", () => resolve(Buffer.concat(chunks)));
1769
- req.on("error", reject);
1770
- });
1771
- }
1772
- function send(res, status, body, contentType = "application/json") {
1773
- res.writeHead(status, {
1774
- "content-type": contentType,
1775
- "content-length": String(body.length)
1776
- });
1777
- res.end(body);
1778
- }
1779
- async function handleDoorRequest(req, res, opts) {
1780
- const path = (req.url ?? "").split("?")[0];
1781
- if (req.method !== "POST" || path !== "/v1/chat/completions" && path !== "/chat/completions") {
1782
- send(res, 404, Buffer.from('{"error":"not_found"}'));
1783
- return;
1784
- }
1785
- let payload;
1786
- try {
1787
- const raw = await readBody(req);
1788
- payload = JSON.parse(raw.length ? raw.toString("utf-8") : "{}");
1789
- } catch {
1790
- send(res, 400, Buffer.from('{"error":"invalid_json"}'));
1791
- return;
1792
- }
1793
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
1794
- send(res, 400, Buffer.from('{"error":"invalid_json"}'));
1795
- return;
1796
- }
1797
- const body = { ...payload };
1798
- delete body.agent_id;
1799
- body.hop_id = opts.hopId;
1800
- const headers = {
1801
- authorization: `Bearer ${opts.jwt}`,
1802
- "content-type": "application/json",
1803
- "X-Hop-Id": opts.hopId
1804
- };
1805
- if (opts.agentId) headers["X-Agent-Id"] = opts.agentId;
1806
- try {
1807
- const upstream = await opts.fetchFn(opts.upstream, {
1808
- method: "POST",
1809
- headers,
1810
- body: JSON.stringify(body)
1811
- });
1812
- const out = Buffer.from(await upstream.arrayBuffer());
1813
- const ct = upstream.headers.get("content-type") || "application/json";
1814
- send(res, upstream.status, out, ct);
1815
- } catch (err) {
1816
- const msg = err instanceof Error ? err.message : String(err);
1817
- send(
1818
- res,
1819
- 502,
1820
- Buffer.from(JSON.stringify({ error: `upstream_unreachable:${msg.slice(0, 120)}` }))
1821
- );
1822
- }
1823
- }
1824
- function closeServer(server) {
1825
- return new Promise((resolve) => {
1826
- server.closeAllConnections?.();
1827
- server.close(() => resolve());
1828
- });
1829
- }
1830
- async function startOfficialHopDoor(opts) {
1831
- const dest = asHostInferenceUrl(opts.hostInferenceUrl);
1832
- const hopId = opts.hopId.trim();
1833
- const jwt = opts.jwt.trim();
1834
- if (!dest || !hopId || !jwt) return null;
1835
- const fetchFn = opts.fetchFn ?? fetch;
1836
- const upstream = `${dest}/chat/completions`;
1837
- const server = import_node_http.default.createServer((req, res) => {
1838
- void handleDoorRequest(req, res, {
1839
- upstream,
1840
- hopId,
1841
- agentId: opts.agentId,
1842
- jwt,
1843
- fetchFn
1844
- });
1845
- });
1846
- try {
1847
- await new Promise((resolve, reject) => {
1848
- server.once("error", reject);
1849
- server.listen(0, "127.0.0.1", () => resolve());
1850
- });
1851
- } catch {
1852
- return null;
1853
- }
1854
- const addr = server.address();
1855
- const port = typeof addr === "object" && addr ? addr.port : 0;
1856
- if (!port) {
1857
- await closeServer(server);
1858
- return null;
1859
- }
1860
- return {
1861
- baseUrl: `http://127.0.0.1:${port}/v1`,
1862
- close: () => closeServer(server)
1863
- };
1864
- }
1865
1764
 
1866
1765
  // src/commands/chat-writeback.ts
1867
1766
  var DEFAULT_COMPLETE_TIMEOUT_MS = 12e4;
@@ -1884,8 +1783,8 @@ function validateChatWritebackOptions(opts) {
1884
1783
  }
1885
1784
  const hasUrl = Boolean(opts.chatCompleteUrl?.trim());
1886
1785
  const hasExec = Boolean(opts.chatCompleteExec?.trim());
1887
- if (hasUrl === hasExec) {
1888
- 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).";
1889
1788
  }
1890
1789
  return null;
1891
1790
  }
@@ -1919,6 +1818,28 @@ function extractContent(payload) {
1919
1818
  }
1920
1819
  return null;
1921
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
+ }
1922
1843
  function asNonNegInt(v) {
1923
1844
  if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
1924
1845
  return Math.floor(v);
@@ -2138,44 +2059,72 @@ function spawnCompleteExec(event, opts, deps, jwt, door) {
2138
2059
  child.stdin?.end(body);
2139
2060
  });
2140
2061
  }
2141
- async function completeViaExec(event, opts, deps, jwt) {
2142
- 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" };
2143
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);
2144
2085
  try {
2145
- if (shouldOpenOfficialDoor({
2146
- inferencePath: event.chat?.inference_path,
2147
- hopId: event.chat?.hop_id,
2148
- hostInferenceUrl: event.chat?.host_inference_url,
2149
- jwt
2150
- })) {
2151
- try {
2152
- door = await startOfficialHopDoor({
2153
- hostInferenceUrl: event.chat.host_inference_url,
2154
- hopId: event.chat.hop_id,
2155
- agentId: opts.agentId,
2156
- jwt,
2157
- fetchFn: deps.fetchFn
2158
- });
2159
- } catch {
2160
- door = null;
2161
- }
2162
- if (door) {
2163
- logFn(
2164
- `[acn listen] official_door chat_id=${event.chat.chat_id} base=${door.baseUrl}`
2165
- );
2166
- } else {
2167
- logFn(
2168
- `[acn listen] official_door_skipped chat_id=${event.chat.chat_id}`
2169
- );
2170
- }
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}` };
2171
2105
  }
2172
- return await spawnCompleteExec(event, opts, deps, jwt, door);
2173
- } finally {
2174
- if (door) {
2175
- 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" };
2176
2119
  }
2120
+ return { ok: false, reason: msg.slice(0, 200) };
2121
+ } finally {
2122
+ clearTimeout(timer);
2177
2123
  }
2178
2124
  }
2125
+ async function completeViaExec(event, opts, deps, jwt) {
2126
+ return spawnCompleteExec(event, opts, deps, jwt);
2127
+ }
2179
2128
  async function postWriteback(event, complete, opts, deps) {
2180
2129
  const chat = event.chat;
2181
2130
  if (!chat) return { ok: false, reason: "no_chat_envelope" };
@@ -2295,11 +2244,26 @@ async function handleChatWriteback(event, opts, deps = {}) {
2295
2244
  const logFn = deps.logFn ?? ((line) => console.error(line));
2296
2245
  if (!event.chat) return { ok: false, reason: "no_chat_envelope" };
2297
2246
  let jwt = null;
2298
- if (event.chat.inference_path === "official" && opts.completeExec) {
2247
+ if (event.chat.inference_path === "official") {
2299
2248
  const minted = await mintAgentJwt(opts, deps.fetchFn ?? fetch);
2300
- 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" };
2301
2266
  }
2302
- const completed = opts.completeUrl ? await completeViaHttp(event, opts, deps) : await completeViaExec(event, opts, deps, jwt);
2303
2267
  if (!completed.ok) {
2304
2268
  logFn(
2305
2269
  `[acn listen] chat_complete_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${completed.reason}`
@@ -2524,7 +2488,7 @@ function formatWakeFailed(event, reason) {
2524
2488
  function formatDeduped(event) {
2525
2489
  return `[acn listen] deduped key=${dedupeKey(event)}`;
2526
2490
  }
2527
- function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send2, deps = {}) {
2491
+ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send, deps = {}) {
2528
2492
  const logFn = deps.logFn ?? ((line) => console.error(line));
2529
2493
  const result = processIncomingRequest(
2530
2494
  correlationId,
@@ -2533,7 +2497,7 @@ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send2
2533
2497
  dedupeStore,
2534
2498
  deps
2535
2499
  );
2536
- send2(result.response);
2500
+ send(result.response);
2537
2501
  if (result.dedupeHit && result.event) {
2538
2502
  logFn(formatDeduped(result.event));
2539
2503
  return;
@@ -2594,7 +2558,7 @@ function errorResponse(id, status, detail) {
2594
2558
  body: JSON.stringify({ error: detail })
2595
2559
  };
2596
2560
  }
2597
- async function dispatchA2aRequest(frame, opts, send2, deps = {}) {
2561
+ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
2598
2562
  const bodyBuf = decodeBody(frame);
2599
2563
  try {
2600
2564
  if (opts.runtime) {
@@ -2604,7 +2568,7 @@ async function dispatchA2aRequest(frame, opts, send2, deps = {}) {
2604
2568
  bodyBuf.toString("utf-8"),
2605
2569
  opts.runtime,
2606
2570
  store,
2607
- send2,
2571
+ send,
2608
2572
  {
2609
2573
  fetchFn: deps.fetchFn,
2610
2574
  spawnFn: deps.spawnFn,
@@ -2614,17 +2578,17 @@ async function dispatchA2aRequest(frame, opts, send2, deps = {}) {
2614
2578
  return;
2615
2579
  }
2616
2580
  if (opts.forward) {
2617
- await forwardToHttp(frame, bodyBuf, opts.forward, deps.fetchFn ?? fetch, send2);
2581
+ await forwardToHttp(frame, bodyBuf, opts.forward, deps.fetchFn ?? fetch, send);
2618
2582
  return;
2619
2583
  }
2620
2584
  if (opts.exec) {
2621
- 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));
2622
2586
  return;
2623
2587
  }
2624
- send2(errorResponse(frame.id, 500, "no handler configured"));
2588
+ send(errorResponse(frame.id, 500, "no handler configured"));
2625
2589
  } catch (err) {
2626
2590
  const msg = err instanceof Error ? err.message : String(err);
2627
- send2(errorResponse(frame.id, 502, `handler failed: ${msg}`));
2591
+ send(errorResponse(frame.id, 502, `handler failed: ${msg}`));
2628
2592
  }
2629
2593
  }
2630
2594
  function buildForwardHeaders(frame) {
@@ -2634,7 +2598,7 @@ function buildForwardHeaders(frame) {
2634
2598
  }
2635
2599
  return headers;
2636
2600
  }
2637
- async function forwardToHttp(frame, bodyBuf, base, fetchFn, send2) {
2601
+ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send) {
2638
2602
  const suffix = frame.path && frame.path !== "/" ? "/" + frame.path.replace(/^\//, "") : "";
2639
2603
  const targetUrl = base.replace(/\/$/, "") + suffix;
2640
2604
  const method = (frame.method ?? "POST").toUpperCase();
@@ -2652,7 +2616,7 @@ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send2) {
2652
2616
  const { done, value } = await reader.read();
2653
2617
  if (done) break;
2654
2618
  if (value && value.length > 0) {
2655
- send2({
2619
+ send({
2656
2620
  type: "a2a_stream_chunk",
2657
2621
  id: frame.id,
2658
2622
  seq: seq++,
@@ -2661,17 +2625,17 @@ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send2) {
2661
2625
  });
2662
2626
  }
2663
2627
  }
2664
- send2({ type: "a2a_stream_end", id: frame.id, status: res.status });
2628
+ send({ type: "a2a_stream_end", id: frame.id, status: res.status });
2665
2629
  } catch (err) {
2666
2630
  const msg = err instanceof Error ? err.message : String(err);
2667
- send2({ type: "a2a_stream_end", id: frame.id, error: msg });
2631
+ send({ type: "a2a_stream_end", id: frame.id, error: msg });
2668
2632
  } finally {
2669
2633
  reader.releaseLock();
2670
2634
  }
2671
2635
  return;
2672
2636
  }
2673
2637
  const respText = await res.text();
2674
- send2({
2638
+ send({
2675
2639
  type: "a2a_response",
2676
2640
  id: frame.id,
2677
2641
  status: res.status,
@@ -2782,12 +2746,12 @@ function runListener(cfg) {
2782
2746
  if (!frame || typeof frame !== "object") return;
2783
2747
  const f = frame;
2784
2748
  if (f.type === "a2a_request" && typeof f.id === "string") {
2785
- const send2 = (out) => {
2749
+ const send = (out) => {
2786
2750
  if (ws.readyState === import_ws.default.OPEN) {
2787
2751
  ws.send(JSON.stringify(out));
2788
2752
  }
2789
2753
  };
2790
- void dispatchA2aRequest(f, cfg, send2, { dedupeStore });
2754
+ void dispatchA2aRequest(f, cfg, send, { dedupeStore });
2791
2755
  }
2792
2756
  });
2793
2757
  ws.on("close", (code, reason) => {
@@ -2870,10 +2834,10 @@ function listenCommand() {
2870
2834
  "Deprecated/ignored: writeback now mints ACN agent JWT from api-key"
2871
2835
  ).option(
2872
2836
  "--chat-complete-url <url>",
2873
- '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)"
2874
2838
  ).option(
2875
2839
  "--chat-complete-exec <cmd>",
2876
- '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.'
2877
2841
  ).option(
2878
2842
  "--chat-complete-timeout <ms>",
2879
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.8",
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": {