@acnlabs/acn-cli 1.0.10 → 1.0.12

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 (2) hide show
  1. package/dist/index.js +240 -23
  2. package/package.json +1 -1
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.10",
34
+ version: "1.0.12",
35
35
  description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -1761,6 +1761,144 @@ function canCompleteOfficialHop(opts) {
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
+ function isSseContentType(contentType) {
1780
+ return contentType.toLowerCase().includes("text/event-stream");
1781
+ }
1782
+ async function pipeWebStream(body, res) {
1783
+ const reader = body.getReader();
1784
+ try {
1785
+ while (true) {
1786
+ const { done, value } = await reader.read();
1787
+ if (done) break;
1788
+ if (!value?.length) continue;
1789
+ const ok = res.write(Buffer.from(value));
1790
+ if (!ok) {
1791
+ await new Promise((resolve) => res.once("drain", resolve));
1792
+ }
1793
+ }
1794
+ res.end();
1795
+ } catch (err) {
1796
+ if (!res.writableEnded) res.destroy(err instanceof Error ? err : void 0);
1797
+ } finally {
1798
+ try {
1799
+ reader.releaseLock();
1800
+ } catch {
1801
+ }
1802
+ }
1803
+ }
1804
+ async function handleDoorRequest(req, res, opts) {
1805
+ const path = (req.url ?? "").split("?")[0];
1806
+ if (req.method !== "POST" || path !== "/v1/chat/completions" && path !== "/chat/completions") {
1807
+ send(res, 404, Buffer.from('{"error":"not_found"}'));
1808
+ return;
1809
+ }
1810
+ let payload;
1811
+ try {
1812
+ const raw = await readBody(req);
1813
+ payload = JSON.parse(raw.length ? raw.toString("utf-8") : "{}");
1814
+ } catch {
1815
+ send(res, 400, Buffer.from('{"error":"invalid_json"}'));
1816
+ return;
1817
+ }
1818
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
1819
+ send(res, 400, Buffer.from('{"error":"invalid_json"}'));
1820
+ return;
1821
+ }
1822
+ const body = { ...payload };
1823
+ delete body.agent_id;
1824
+ body.hop_id = opts.hopId;
1825
+ const headers = {
1826
+ authorization: `Bearer ${opts.jwt}`,
1827
+ "content-type": "application/json",
1828
+ "X-Hop-Id": opts.hopId
1829
+ };
1830
+ if (opts.agentId) headers["X-Agent-Id"] = opts.agentId;
1831
+ try {
1832
+ const upstream = await opts.fetchFn(opts.upstream, {
1833
+ method: "POST",
1834
+ headers,
1835
+ body: JSON.stringify(body)
1836
+ });
1837
+ const ct = upstream.headers.get("content-type") || "application/json";
1838
+ const sse = isSseContentType(ct) && upstream.body != null;
1839
+ const streamOk = body.stream === true && upstream.body != null && upstream.status < 400;
1840
+ if (sse || streamOk) {
1841
+ res.writeHead(upstream.status, {
1842
+ "content-type": isSseContentType(ct) ? ct : "text/event-stream",
1843
+ "cache-control": "no-cache",
1844
+ connection: "keep-alive",
1845
+ "x-accel-buffering": "no"
1846
+ });
1847
+ await pipeWebStream(upstream.body, res);
1848
+ return;
1849
+ }
1850
+ const out = Buffer.from(await upstream.arrayBuffer());
1851
+ send(res, upstream.status, out, ct);
1852
+ } catch (err) {
1853
+ const msg = err instanceof Error ? err.message : String(err);
1854
+ send(
1855
+ res,
1856
+ 502,
1857
+ Buffer.from(JSON.stringify({ error: `upstream_unreachable:${msg.slice(0, 120)}` }))
1858
+ );
1859
+ }
1860
+ }
1861
+ function closeServer(server) {
1862
+ return new Promise((resolve) => {
1863
+ server.closeAllConnections?.();
1864
+ server.close(() => resolve());
1865
+ });
1866
+ }
1867
+ async function startOfficialHopDoor(opts) {
1868
+ const dest = asHostInferenceUrl(opts.hostInferenceUrl);
1869
+ const hopId = opts.hopId.trim();
1870
+ const jwt = opts.jwt.trim();
1871
+ if (!dest || !hopId || !jwt) return null;
1872
+ const fetchFn = opts.fetchFn ?? fetch;
1873
+ const upstream = `${dest}/chat/completions`;
1874
+ const server = import_node_http.default.createServer((req, res) => {
1875
+ void handleDoorRequest(req, res, {
1876
+ upstream,
1877
+ hopId,
1878
+ agentId: opts.agentId,
1879
+ jwt,
1880
+ fetchFn
1881
+ });
1882
+ });
1883
+ try {
1884
+ await new Promise((resolve, reject) => {
1885
+ server.once("error", reject);
1886
+ server.listen(0, "127.0.0.1", () => resolve());
1887
+ });
1888
+ } catch {
1889
+ return null;
1890
+ }
1891
+ const addr = server.address();
1892
+ const port = typeof addr === "object" && addr ? addr.port : 0;
1893
+ if (!port) {
1894
+ await closeServer(server);
1895
+ return null;
1896
+ }
1897
+ return {
1898
+ baseUrl: `http://127.0.0.1:${port}/v1`,
1899
+ close: () => closeServer(server)
1900
+ };
1901
+ }
1764
1902
 
1765
1903
  // src/commands/chat-writeback.ts
1766
1904
  var DEFAULT_COMPLETE_TIMEOUT_MS = 12e4;
@@ -1976,7 +2114,7 @@ function completeInferenceEnv(event, opts, jwt, door) {
1976
2114
  }
1977
2115
  return { ...process.env, ...extra };
1978
2116
  }
1979
- function completeInferenceHeaders(event, opts) {
2117
+ function completeInferenceHeaders(event, opts, jwt, door) {
1980
2118
  const headers = {
1981
2119
  "content-type": "application/json"
1982
2120
  };
@@ -1987,9 +2125,14 @@ function completeInferenceHeaders(event, opts) {
1987
2125
  if (chat?.host_inference_url) {
1988
2126
  headers["X-ACN-Host-Inference-Url"] = chat.host_inference_url;
1989
2127
  }
2128
+ if (jwt) headers["X-ACN-Agent-Jwt"] = jwt;
2129
+ if (door?.baseUrl && jwt) {
2130
+ headers["X-ACN-OpenAI-Base-Url"] = door.baseUrl;
2131
+ headers["X-ACN-OpenAI-Api-Key"] = jwt;
2132
+ }
1990
2133
  return headers;
1991
2134
  }
1992
- async function completeViaHttp(event, opts, deps) {
2135
+ async function completeViaHttp(event, opts, deps, jwt, door) {
1993
2136
  const fetchFn = deps.fetchFn ?? fetch;
1994
2137
  const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
1995
2138
  const controller = new AbortController();
@@ -1997,7 +2140,7 @@ async function completeViaHttp(event, opts, deps) {
1997
2140
  try {
1998
2141
  const res = await fetchFn(opts.completeUrl, {
1999
2142
  method: "POST",
2000
- headers: completeInferenceHeaders(event, opts),
2143
+ headers: completeInferenceHeaders(event, opts, jwt, door),
2001
2144
  body: JSON.stringify(event),
2002
2145
  signal: controller.signal
2003
2146
  });
@@ -2085,6 +2228,9 @@ function officialCompleteFailureContent(reason, model) {
2085
2228
  if (mid && !officialV0SupportsModel(mid)) {
2086
2229
  return `Official v0 is a single completion and cannot run thinking/reasoning models (${mid}). Switch to a chat model such as kimi-k2.5.`;
2087
2230
  }
2231
+ if (reason === "official_host_unseen") {
2232
+ return "Official hop failed: Host did not see this hop. Complete must call OPENAI_BASE_URL (Host), not a BYO / TokenHub key.";
2233
+ }
2088
2234
  return `Official hop failed (${reason}). Try kimi or another chat model.`;
2089
2235
  }
2090
2236
  async function completeOfficialViaHost(event, opts, deps, jwt) {
@@ -2153,6 +2299,73 @@ async function completeOfficialViaHost(event, opts, deps, jwt) {
2153
2299
  clearTimeout(timer);
2154
2300
  }
2155
2301
  }
2302
+ async function loadOfficialHostMeter(event, opts, deps, jwt) {
2303
+ const chat = event.chat;
2304
+ const base = asHostInferenceUrl(chat?.host_inference_url);
2305
+ const hopId = chat?.hop_id?.trim();
2306
+ if (!base || !hopId) return { ok: false, reason: "official_complete_skipped" };
2307
+ const fetchFn = deps.fetchFn ?? fetch;
2308
+ try {
2309
+ const res = await fetchFn(`${base}/hops/${encodeURIComponent(hopId)}`, {
2310
+ headers: {
2311
+ authorization: `Bearer ${jwt}`,
2312
+ "X-Hop-Id": hopId,
2313
+ "X-Agent-Id": opts.agentId
2314
+ }
2315
+ });
2316
+ if (res.status < 200 || res.status >= 300) {
2317
+ return { ok: false, reason: `official_meter_http_${res.status}` };
2318
+ }
2319
+ let payload;
2320
+ try {
2321
+ payload = JSON.parse(await res.text());
2322
+ } catch {
2323
+ return { ok: false, reason: "official_meter_invalid_json" };
2324
+ }
2325
+ return { ok: true, seen: payload.seen === true };
2326
+ } catch (err) {
2327
+ const msg = err instanceof Error ? err.message : String(err);
2328
+ return { ok: false, reason: `official_meter_unreachable:${msg.slice(0, 80)}` };
2329
+ }
2330
+ }
2331
+ async function completeOfficialViaAgent(event, opts, deps, jwt) {
2332
+ const chat = event.chat;
2333
+ if (!chat) return { ok: false, reason: "no_chat_envelope" };
2334
+ if (!canCompleteOfficialHop({
2335
+ inferencePath: chat.inference_path,
2336
+ hopId: chat.hop_id,
2337
+ hostInferenceUrl: chat.host_inference_url,
2338
+ jwt
2339
+ })) {
2340
+ return { ok: false, reason: "official_complete_skipped" };
2341
+ }
2342
+ const model = chat.requested_model?.trim();
2343
+ if (model && !officialV0SupportsModel(model)) {
2344
+ return { ok: false, reason: "official_complete_unsupported_model" };
2345
+ }
2346
+ const logFn = deps.logFn ?? ((line) => console.error(line));
2347
+ logFn(
2348
+ `[acn listen] official_complete_via_agent chat_id=${chat.chat_id}` + (model ? ` model=${model}` : "")
2349
+ );
2350
+ const door = await startOfficialHopDoor({
2351
+ hostInferenceUrl: chat.host_inference_url,
2352
+ hopId: chat.hop_id,
2353
+ agentId: opts.agentId,
2354
+ jwt,
2355
+ fetchFn: deps.fetchFn
2356
+ });
2357
+ if (!door) return { ok: false, reason: "official_door_failed" };
2358
+ try {
2359
+ const completed = opts.completeUrl ? await completeViaHttp(event, opts, deps, jwt, door) : await spawnCompleteExec(event, opts, deps, jwt, door);
2360
+ if (!completed.ok) return completed;
2361
+ const meter = await loadOfficialHostMeter(event, opts, deps, jwt);
2362
+ if (!meter.ok) return meter;
2363
+ if (!meter.seen) return { ok: false, reason: "official_host_unseen" };
2364
+ return { ok: true, result: { content: completed.result.content } };
2365
+ } finally {
2366
+ await door.close();
2367
+ }
2368
+ }
2156
2369
  async function completeViaExec(event, opts, deps, jwt) {
2157
2370
  return spawnCompleteExec(event, opts, deps, jwt);
2158
2371
  }
@@ -2299,7 +2512,11 @@ async function handleChatWriteback(event, opts, deps = {}) {
2299
2512
  }
2300
2513
  let completed;
2301
2514
  if (event.chat.inference_path === "official") {
2302
- completed = await completeOfficialViaHost(event, opts, deps, jwt);
2515
+ if (opts.completeUrl || opts.completeExec) {
2516
+ completed = await completeOfficialViaAgent(event, opts, deps, jwt);
2517
+ } else {
2518
+ completed = await completeOfficialViaHost(event, opts, deps, jwt);
2519
+ }
2303
2520
  } else if (opts.completeUrl) {
2304
2521
  completed = await completeViaHttp(event, opts, deps);
2305
2522
  } else if (opts.completeExec) {
@@ -2545,7 +2762,7 @@ function formatWakeFailed(event, reason) {
2545
2762
  function formatDeduped(event) {
2546
2763
  return `[acn listen] deduped key=${dedupeKey(event)}`;
2547
2764
  }
2548
- function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send, deps = {}) {
2765
+ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send2, deps = {}) {
2549
2766
  const logFn = deps.logFn ?? ((line) => console.error(line));
2550
2767
  const result = processIncomingRequest(
2551
2768
  correlationId,
@@ -2554,7 +2771,7 @@ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send,
2554
2771
  dedupeStore,
2555
2772
  deps
2556
2773
  );
2557
- send(result.response);
2774
+ send2(result.response);
2558
2775
  if (result.dedupeHit && result.event) {
2559
2776
  logFn(formatDeduped(result.event));
2560
2777
  return;
@@ -2615,7 +2832,7 @@ function errorResponse(id, status, detail) {
2615
2832
  body: JSON.stringify({ error: detail })
2616
2833
  };
2617
2834
  }
2618
- async function dispatchA2aRequest(frame, opts, send, deps = {}) {
2835
+ async function dispatchA2aRequest(frame, opts, send2, deps = {}) {
2619
2836
  const bodyBuf = decodeBody(frame);
2620
2837
  try {
2621
2838
  if (opts.runtime) {
@@ -2625,7 +2842,7 @@ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
2625
2842
  bodyBuf.toString("utf-8"),
2626
2843
  opts.runtime,
2627
2844
  store,
2628
- send,
2845
+ send2,
2629
2846
  {
2630
2847
  fetchFn: deps.fetchFn,
2631
2848
  spawnFn: deps.spawnFn,
@@ -2635,17 +2852,17 @@ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
2635
2852
  return;
2636
2853
  }
2637
2854
  if (opts.forward) {
2638
- await forwardToHttp(frame, bodyBuf, opts.forward, deps.fetchFn ?? fetch, send);
2855
+ await forwardToHttp(frame, bodyBuf, opts.forward, deps.fetchFn ?? fetch, send2);
2639
2856
  return;
2640
2857
  }
2641
2858
  if (opts.exec) {
2642
- send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
2859
+ send2(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
2643
2860
  return;
2644
2861
  }
2645
- send(errorResponse(frame.id, 500, "no handler configured"));
2862
+ send2(errorResponse(frame.id, 500, "no handler configured"));
2646
2863
  } catch (err) {
2647
2864
  const msg = err instanceof Error ? err.message : String(err);
2648
- send(errorResponse(frame.id, 502, `handler failed: ${msg}`));
2865
+ send2(errorResponse(frame.id, 502, `handler failed: ${msg}`));
2649
2866
  }
2650
2867
  }
2651
2868
  function buildForwardHeaders(frame) {
@@ -2655,7 +2872,7 @@ function buildForwardHeaders(frame) {
2655
2872
  }
2656
2873
  return headers;
2657
2874
  }
2658
- async function forwardToHttp(frame, bodyBuf, base, fetchFn, send) {
2875
+ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send2) {
2659
2876
  const suffix = frame.path && frame.path !== "/" ? "/" + frame.path.replace(/^\//, "") : "";
2660
2877
  const targetUrl = base.replace(/\/$/, "") + suffix;
2661
2878
  const method = (frame.method ?? "POST").toUpperCase();
@@ -2673,7 +2890,7 @@ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send) {
2673
2890
  const { done, value } = await reader.read();
2674
2891
  if (done) break;
2675
2892
  if (value && value.length > 0) {
2676
- send({
2893
+ send2({
2677
2894
  type: "a2a_stream_chunk",
2678
2895
  id: frame.id,
2679
2896
  seq: seq++,
@@ -2682,17 +2899,17 @@ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send) {
2682
2899
  });
2683
2900
  }
2684
2901
  }
2685
- send({ type: "a2a_stream_end", id: frame.id, status: res.status });
2902
+ send2({ type: "a2a_stream_end", id: frame.id, status: res.status });
2686
2903
  } catch (err) {
2687
2904
  const msg = err instanceof Error ? err.message : String(err);
2688
- send({ type: "a2a_stream_end", id: frame.id, error: msg });
2905
+ send2({ type: "a2a_stream_end", id: frame.id, error: msg });
2689
2906
  } finally {
2690
2907
  reader.releaseLock();
2691
2908
  }
2692
2909
  return;
2693
2910
  }
2694
2911
  const respText = await res.text();
2695
- send({
2912
+ send2({
2696
2913
  type: "a2a_response",
2697
2914
  id: frame.id,
2698
2915
  status: res.status,
@@ -2803,12 +3020,12 @@ function runListener(cfg) {
2803
3020
  if (!frame || typeof frame !== "object") return;
2804
3021
  const f = frame;
2805
3022
  if (f.type === "a2a_request" && typeof f.id === "string") {
2806
- const send = (out) => {
3023
+ const send2 = (out) => {
2807
3024
  if (ws.readyState === import_ws.default.OPEN) {
2808
3025
  ws.send(JSON.stringify(out));
2809
3026
  }
2810
3027
  };
2811
- void dispatchA2aRequest(f, cfg, send, { dedupeStore });
3028
+ void dispatchA2aRequest(f, cfg, send2, { dedupeStore });
2812
3029
  }
2813
3030
  });
2814
3031
  ws.on("close", (code, reason) => {
@@ -2891,13 +3108,13 @@ function listenCommand() {
2891
3108
  "Deprecated/ignored: writeback now mints ACN agent JWT from api-key"
2892
3109
  ).option(
2893
3110
  "--chat-complete-url <url>",
2894
- "BYO complete URL (optional if official-only; mutually exclusive with --chat-complete-exec)"
3111
+ "Complete URL (optional if official-only; mutually exclusive with --chat-complete-exec). Official hops open a Host door and require a Host invoice."
2895
3112
  ).option(
2896
3113
  "--chat-complete-exec <cmd>",
2897
- 'BYO only (optional if official-only): stdin event \u2192 stdout {"content"}. Official hops complete via Host.'
3114
+ 'Complete exec (optional if official-only): stdin event \u2192 stdout {"content"}. Official hops with this flag open a Host door and require a Host invoice.'
2898
3115
  ).option(
2899
3116
  "--chat-complete-timeout <ms>",
2900
- `Complete timeout in ms (official default ${DEFAULT_OFFICIAL_COMPLETE_TIMEOUT_MS}; BYO default ${DEFAULT_COMPLETE_TIMEOUT_MS})`
3117
+ `Complete timeout in ms (CLI-owned official default ${DEFAULT_OFFICIAL_COMPLETE_TIMEOUT_MS}; agent/BYO default ${DEFAULT_COMPLETE_TIMEOUT_MS})`
2901
3118
  ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").option(
2902
3119
  "-m, --model <modelId>",
2903
3120
  "Declare runtime model (Host Catalog id) on connect + every 15m via REST heartbeat (self-reported; env: ACN_PREFERRED_MODEL)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {