@acnlabs/acn-cli 0.12.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +464 -73
  2. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,16 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
9
  var __commonJS = (cb, mod) => function __require() {
5
10
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
11
  };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
7
28
 
8
29
  // package.json
9
30
  var require_package = __commonJS({
10
31
  "package.json"(exports2, module2) {
11
32
  module2.exports = {
12
33
  name: "@acnlabs/acn-cli",
13
- version: "0.12.0",
34
+ version: "0.13.2",
14
35
  description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
15
36
  main: "dist/index.js",
16
37
  bin: {
@@ -48,10 +69,12 @@ var require_package = __commonJS({
48
69
  url: "https://github.com/acnlabs/ACN/issues"
49
70
  },
50
71
  dependencies: {
51
- commander: "^12.0.0"
72
+ commander: "^12.0.0",
73
+ ws: "^8.18.0"
52
74
  },
53
75
  devDependencies: {
54
76
  "@types/node": "^20.0.0",
77
+ "@types/ws": "^8.5.10",
55
78
  tsup: "^8.0.0",
56
79
  typescript: "^5.0.0",
57
80
  vitest: "^3.0.5"
@@ -64,7 +87,7 @@ var require_package = __commonJS({
64
87
  });
65
88
 
66
89
  // src/index.ts
67
- var import_commander15 = require("commander");
90
+ var import_commander16 = require("commander");
68
91
 
69
92
  // src/output.ts
70
93
  var jsonMode = false;
@@ -101,32 +124,91 @@ var import_path = require("path");
101
124
  var import_fs = require("fs");
102
125
  var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".acn");
103
126
  var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
104
- var DEFAULT_BASE_URL = "https://api.acnlabs.dev";
105
- function loadConfig() {
106
- if (!(0, import_fs.existsSync)(CONFIG_FILE)) {
107
- return { base_url: DEFAULT_BASE_URL };
127
+ var REGION_BASE_URLS = {
128
+ global: "https://api.acnlabs.dev",
129
+ cn: "https://acn.acnlabs.cn"
130
+ };
131
+ var DEFAULT_BASE_URL = REGION_BASE_URLS.global;
132
+ function normalizeBaseUrl(url) {
133
+ let u = url.trim().replace(/\/+$/, "");
134
+ u = u.replace(/\/api\/v1$/i, "");
135
+ return u.replace(/\/+$/, "");
136
+ }
137
+ function baseUrlForRegion(region) {
138
+ const key = region.trim().toLowerCase();
139
+ if (key === "global" || key === "cn") {
140
+ return REGION_BASE_URLS[key];
108
141
  }
142
+ throw new Error(`Unknown region "${region}". Valid: global | cn`);
143
+ }
144
+ function inferRegion(baseUrl) {
145
+ const u = normalizeBaseUrl(baseUrl);
146
+ if (u === REGION_BASE_URLS.global) return "global";
147
+ if (u === REGION_BASE_URLS.cn) return "cn";
148
+ return void 0;
149
+ }
150
+ function readConfigFile() {
151
+ if (!(0, import_fs.existsSync)(CONFIG_FILE)) return {};
109
152
  try {
110
153
  const raw = (0, import_fs.readFileSync)(CONFIG_FILE, "utf-8");
111
- const parsed = JSON.parse(raw);
112
- return {
113
- base_url: parsed.base_url ?? DEFAULT_BASE_URL,
114
- api_key: parsed.api_key,
115
- agent_id: parsed.agent_id
116
- };
154
+ return JSON.parse(raw);
117
155
  } catch {
118
- return { base_url: DEFAULT_BASE_URL };
156
+ return {};
157
+ }
158
+ }
159
+ function resolveBaseUrl(overrides) {
160
+ if (overrides?.base_url) {
161
+ return normalizeBaseUrl(overrides.base_url);
162
+ }
163
+ if (overrides?.region) {
164
+ return baseUrlForRegion(overrides.region);
165
+ }
166
+ const env = process.env.ACN_BASE_URL?.trim();
167
+ if (env) {
168
+ return normalizeBaseUrl(env);
169
+ }
170
+ const file = readConfigFile();
171
+ if (file.base_url) {
172
+ return normalizeBaseUrl(file.base_url);
173
+ }
174
+ if (file.region) {
175
+ return baseUrlForRegion(file.region);
119
176
  }
177
+ return DEFAULT_BASE_URL;
178
+ }
179
+ function loadConfig() {
180
+ const file = readConfigFile();
181
+ const base_url = resolveBaseUrl();
182
+ return {
183
+ base_url,
184
+ api_key: file.api_key,
185
+ agent_id: file.agent_id,
186
+ region: inferRegion(base_url)
187
+ };
120
188
  }
121
189
  function saveConfig(updates) {
122
190
  if (!(0, import_fs.existsSync)(CONFIG_DIR)) {
123
191
  (0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
124
192
  }
125
- const current = loadConfig();
193
+ const file = readConfigFile();
194
+ const current = {
195
+ base_url: file.base_url ? normalizeBaseUrl(file.base_url) : DEFAULT_BASE_URL,
196
+ api_key: file.api_key,
197
+ agent_id: file.agent_id,
198
+ region: file.region === "global" || file.region === "cn" ? file.region : void 0
199
+ };
126
200
  const next = { ...current, ...updates };
127
- const clean = { base_url: next.base_url };
201
+ if (updates.region && !updates.base_url) {
202
+ next.base_url = baseUrlForRegion(updates.region);
203
+ }
204
+ if (updates.base_url && updates.region === void 0) {
205
+ next.region = inferRegion(updates.base_url);
206
+ }
207
+ const clean = { base_url: normalizeBaseUrl(next.base_url) };
128
208
  if (next.api_key !== void 0) clean.api_key = next.api_key;
129
209
  if (next.agent_id !== void 0) clean.agent_id = next.agent_id;
210
+ const region = next.region ?? inferRegion(clean.base_url);
211
+ if (region !== void 0) clean.region = region;
130
212
  (0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(clean, null, 2), "utf-8");
131
213
  }
132
214
  function getConfigPath() {
@@ -134,7 +216,7 @@ function getConfigPath() {
134
216
  }
135
217
 
136
218
  // src/commands/config.ts
137
- var VALID_KEYS = ["api-key", "agent-id", "base-url"];
219
+ var VALID_KEYS = ["api-key", "agent-id", "base-url", "region"];
138
220
  var KEY_MAP = {
139
221
  "api-key": "api_key",
140
222
  "agent-id": "agent_id",
@@ -142,11 +224,25 @@ var KEY_MAP = {
142
224
  };
143
225
  function configCommand() {
144
226
  const cmd = new import_commander.Command("config").description("Manage local ACN configuration");
145
- cmd.command("set <key> <value>").description(`Set a config value. Keys: ${VALID_KEYS.join(", ")}`).action((key, value) => {
227
+ cmd.command("set <key> <value>").description(
228
+ `Set a config value. Keys: ${VALID_KEYS.join(", ")}. region is global|cn (sets base-url). Env ACN_BASE_URL overrides base-url at runtime.`
229
+ ).action((key, value) => {
146
230
  if (!VALID_KEYS.includes(key)) {
147
231
  console.error(`Unknown key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
148
232
  process.exit(1);
149
233
  }
234
+ if (key === "region") {
235
+ try {
236
+ const base_url = baseUrlForRegion(value);
237
+ const region = value.trim().toLowerCase();
238
+ saveConfig({ region, base_url });
239
+ output({ key, value: region, base_url }, `Set region = ${region} (base-url = ${base_url})`);
240
+ } catch (err) {
241
+ console.error(err instanceof Error ? err.message : String(err));
242
+ process.exit(1);
243
+ }
244
+ return;
245
+ }
150
246
  saveConfig({ [KEY_MAP[key]]: value });
151
247
  output({ key, value }, `Set ${key} = ${value}`);
152
248
  });
@@ -156,19 +252,21 @@ function configCommand() {
156
252
  process.exit(1);
157
253
  }
158
254
  const config = loadConfig();
159
- const val = config[KEY_MAP[key]];
255
+ const val = key === "region" ? config.region : config[KEY_MAP[key]];
160
256
  if (val === void 0) {
161
257
  console.error(`Key "${key}" is not set.`);
162
258
  process.exit(1);
163
259
  }
164
- output({ key, value: val }, val);
260
+ output({ key, value: val }, String(val));
165
261
  });
166
262
  cmd.command("show").description("Show all config values").action(() => {
167
263
  const config = loadConfig();
168
264
  const path = getConfigPath();
265
+ const envOverride = process.env.ACN_BASE_URL?.trim() ? ` (ACN_BASE_URL override active)` : "";
169
266
  output(config, [
170
267
  `Config file: ${path}`,
171
- ` base-url : ${config.base_url}`,
268
+ ` region : ${config.region ?? "(custom / unknown)"}`,
269
+ ` base-url : ${config.base_url}${envOverride}`,
172
270
  ` api-key : ${config.api_key ? maskKey(config.api_key) : "(not set)"}`,
173
271
  ` agent-id : ${config.agent_id ?? "(not set)"}`
174
272
  ].join("\n"));
@@ -205,8 +303,9 @@ function extractDetail(body) {
205
303
  }
206
304
  async function acnFetch(path, options = {}) {
207
305
  const config = loadConfig();
208
- const { params, ...fetchOptions } = options;
209
- const url = new URL(`${config.base_url}/api/v1${path}`);
306
+ const { params, baseUrl: baseUrlOverride, ...fetchOptions } = options;
307
+ const origin = baseUrlOverride ? normalizeBaseUrl(baseUrlOverride) : config.base_url;
308
+ const url = new URL(`${origin}/api/v1${path}`);
210
309
  if (params) {
211
310
  for (const [k, v] of Object.entries(params)) {
212
311
  if (v !== void 0) url.searchParams.set(k, String(v));
@@ -234,54 +333,98 @@ async function acnFetch(path, options = {}) {
234
333
  }
235
334
  return res.json();
236
335
  }
237
- function acnGet(path, params) {
238
- return acnFetch(path, { method: "GET", params });
336
+ function acnGet(path, params, opts) {
337
+ return acnFetch(path, { method: "GET", params, baseUrl: opts?.baseUrl });
239
338
  }
240
- function acnPost(path, body) {
339
+ function acnPost(path, body, opts) {
241
340
  return acnFetch(path, {
242
341
  method: "POST",
243
- body: body !== void 0 ? JSON.stringify(body) : void 0
342
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
343
+ baseUrl: opts?.baseUrl
244
344
  });
245
345
  }
246
- function acnPatch(path, body) {
346
+ function acnPatch(path, body, opts) {
247
347
  return acnFetch(path, {
248
348
  method: "PATCH",
249
- body: body !== void 0 ? JSON.stringify(body) : void 0
349
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
350
+ baseUrl: opts?.baseUrl
250
351
  });
251
352
  }
252
- function acnDelete(path) {
253
- return acnFetch(path, { method: "DELETE" });
353
+ function acnDelete(path, opts) {
354
+ return acnFetch(path, { method: "DELETE", baseUrl: opts?.baseUrl });
254
355
  }
255
356
 
256
357
  // src/commands/join.ts
257
358
  function joinCommand() {
258
- return new import_commander2.Command("join").description("Register this agent with ACN and save credentials locally").requiredOption("-n, --name <name>", "Agent name").requiredOption("-t, --tags <tags>", "Comma-separated capability tags (e.g. coding,review)").option("-e, --endpoint <url>", "Public A2A endpoint URL of this agent").option("-d, --description <text>", "Agent description").action(async (opts) => {
259
- const tags = opts.tags.split(",").map((s) => s.trim()).filter(Boolean);
260
- const body = {
261
- name: opts.name,
262
- description: opts.description ?? `${opts.name} \u2014 registered via acn-cli`,
263
- tags,
264
- ...opts.endpoint ? { endpoint: opts.endpoint } : {}
265
- };
266
- try {
267
- const res = await acnPost("/agents/join", body);
268
- saveConfig({ api_key: res.api_key, agent_id: res.agent_id });
269
- const claimLine = res.claim_url ? `
359
+ return new import_commander2.Command("join").description("Register this agent with ACN and save credentials locally").requiredOption("-n, --name <name>", "Agent name").requiredOption("-t, --tags <tags>", "Comma-separated capability tags (e.g. coding,review)").option("-e, --endpoint <url>", "Public A2A endpoint URL of this agent").option("-d, --description <text>", "Agent description").option(
360
+ "--relay",
361
+ "Receive messages in real time over an outbound WebSocket (run `acn listen`) instead of hosting a public endpoint. Registers in open/push mode with no delivery URL."
362
+ ).option(
363
+ "--region <region>",
364
+ "ACN deployment to join: global (api.acnlabs.dev) or cn (acn.acnlabs.cn). Pick by where the agent is hosted \u2014 not by user nationality."
365
+ ).option(
366
+ "--base-url <url>",
367
+ "Override ACN origin (no /api/v1). Overrides --region and ACN_BASE_URL for this join."
368
+ ).action(
369
+ async (opts) => {
370
+ if (opts.region && opts.baseUrl) {
371
+ console.error("Use either --region or --base-url, not both.");
372
+ process.exit(1);
373
+ }
374
+ if (opts.region) {
375
+ const r = opts.region.trim().toLowerCase();
376
+ if (r !== "global" && r !== "cn") {
377
+ console.error(`Unknown --region "${opts.region}". Valid: global | cn`);
378
+ process.exit(1);
379
+ }
380
+ }
381
+ const base_url = resolveBaseUrl({
382
+ base_url: opts.baseUrl,
383
+ region: opts.region
384
+ });
385
+ const region = opts.region?.trim().toLowerCase() === "cn" || opts.region?.trim().toLowerCase() === "global" ? opts.region.trim().toLowerCase() : inferRegion(base_url);
386
+ const tags = opts.tags.split(",").map((s) => s.trim()).filter(Boolean);
387
+ const body = {
388
+ name: opts.name,
389
+ description: opts.description ?? `${opts.name} \u2014 registered via acn-cli`,
390
+ tags,
391
+ ...opts.endpoint ? { endpoint: opts.endpoint } : {},
392
+ // ADR-0012 Mode B: relay delivery needs a push mode (open) so the
393
+ // gateway actually pushes inbound messages down the WebSocket; the
394
+ // server-side validator then waives the public-URL requirement.
395
+ ...opts.relay ? { delivery: "relay", communication_policy: { mode: "open" } } : {}
396
+ };
397
+ try {
398
+ const res = await acnPost("/agents/join", body, {
399
+ baseUrl: base_url
400
+ });
401
+ saveConfig({
402
+ api_key: res.api_key,
403
+ agent_id: res.agent_id,
404
+ base_url,
405
+ ...region ? { region } : {}
406
+ });
407
+ const claimLine = res.claim_url ? `
270
408
  Claim URL: ${res.claim_url}` : "";
271
- const verifyLine = res.verification_code ? `
409
+ const verifyLine = res.verification_code ? `
272
410
  Verify : ${res.verification_code}` : "";
273
- output(res, [
274
- `Registered successfully!`,
275
- ` Agent ID : ${res.agent_id}`,
276
- ` API Key : ${res.api_key}`,
277
- ` Status : ${res.status}${claimLine}${verifyLine}`,
278
- ``,
279
- `Credentials saved to ~/.acn/config.json`
280
- ].join("\n"));
281
- } catch (err) {
282
- handleError(err);
411
+ const regionLine = region ? `
412
+ Region : ${region}` : "";
413
+ output(res, [
414
+ `Registered successfully!`,
415
+ ` Agent ID : ${res.agent_id}`,
416
+ ` API Key : ${res.api_key}`,
417
+ ` ACN : ${base_url}${regionLine}`,
418
+ ` Status : ${res.status}${claimLine}${verifyLine}`,
419
+ ``,
420
+ `Credentials saved to ~/.acn/config.json`,
421
+ `Do not reuse this api_key against another region \u2014 re-join instead.`
422
+ ].join("\n"));
423
+ } catch (err) {
424
+ handleError(err);
425
+ }
283
426
  }
284
- });
427
+ );
285
428
  }
286
429
 
287
430
  // src/commands/heartbeat.ts
@@ -1230,8 +1373,255 @@ ${formatPolicy(res)}`);
1230
1373
  return cmd;
1231
1374
  }
1232
1375
 
1233
- // src/commands/session.ts
1376
+ // src/commands/listen.ts
1234
1377
  var import_commander10 = require("commander");
1378
+ var import_child_process = require("child_process");
1379
+ var import_ws = __toESM(require("ws"));
1380
+ var STRIP_HEADERS = /* @__PURE__ */ new Set([
1381
+ "host",
1382
+ "content-length",
1383
+ "connection",
1384
+ "transfer-encoding"
1385
+ ]);
1386
+ function toWebsocketUrl(baseUrl, agentId) {
1387
+ const u = new URL(baseUrl);
1388
+ u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
1389
+ u.pathname = `/ws/${agentId}`;
1390
+ u.search = "";
1391
+ return u.toString();
1392
+ }
1393
+ function decodeBody(frame) {
1394
+ if (frame.body_encoding === "base64") {
1395
+ return Buffer.from(frame.body ?? "", "base64");
1396
+ }
1397
+ return Buffer.from(frame.body ?? "", "utf-8");
1398
+ }
1399
+ function errorResponse(id, status, detail) {
1400
+ return {
1401
+ type: "a2a_response",
1402
+ id,
1403
+ status,
1404
+ headers: { "content-type": "application/json" },
1405
+ body: JSON.stringify({ error: detail })
1406
+ };
1407
+ }
1408
+ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
1409
+ const bodyBuf = decodeBody(frame);
1410
+ try {
1411
+ if (opts.forward) {
1412
+ await forwardToHttp(frame, bodyBuf, opts.forward, deps.fetchFn ?? fetch, send);
1413
+ return;
1414
+ }
1415
+ if (opts.exec) {
1416
+ send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process.spawn));
1417
+ return;
1418
+ }
1419
+ send(errorResponse(frame.id, 500, "no handler configured"));
1420
+ } catch (err) {
1421
+ const msg = err instanceof Error ? err.message : String(err);
1422
+ send(errorResponse(frame.id, 502, `handler failed: ${msg}`));
1423
+ }
1424
+ }
1425
+ function buildForwardHeaders(frame) {
1426
+ const headers = {};
1427
+ for (const [k, v] of Object.entries(frame.headers ?? {})) {
1428
+ if (!STRIP_HEADERS.has(k.toLowerCase())) headers[k] = v;
1429
+ }
1430
+ return headers;
1431
+ }
1432
+ async function forwardToHttp(frame, bodyBuf, base, fetchFn, send) {
1433
+ const suffix = frame.path && frame.path !== "/" ? "/" + frame.path.replace(/^\//, "") : "";
1434
+ const targetUrl = base.replace(/\/$/, "") + suffix;
1435
+ const method = (frame.method ?? "POST").toUpperCase();
1436
+ const init = { method, headers: buildForwardHeaders(frame) };
1437
+ if (method !== "GET" && method !== "HEAD") {
1438
+ init.body = bodyBuf;
1439
+ }
1440
+ const res = await fetchFn(targetUrl, init);
1441
+ const contentType = res.headers.get("content-type") ?? "application/json";
1442
+ if (contentType.includes("text/event-stream") && res.body) {
1443
+ const reader = res.body.getReader();
1444
+ let seq = 0;
1445
+ try {
1446
+ for (; ; ) {
1447
+ const { done, value } = await reader.read();
1448
+ if (done) break;
1449
+ if (value && value.length > 0) {
1450
+ send({
1451
+ type: "a2a_stream_chunk",
1452
+ id: frame.id,
1453
+ seq: seq++,
1454
+ data: Buffer.from(value).toString("base64"),
1455
+ data_encoding: "base64"
1456
+ });
1457
+ }
1458
+ }
1459
+ send({ type: "a2a_stream_end", id: frame.id, status: res.status });
1460
+ } catch (err) {
1461
+ const msg = err instanceof Error ? err.message : String(err);
1462
+ send({ type: "a2a_stream_end", id: frame.id, error: msg });
1463
+ } finally {
1464
+ reader.releaseLock();
1465
+ }
1466
+ return;
1467
+ }
1468
+ const respText = await res.text();
1469
+ send({
1470
+ type: "a2a_response",
1471
+ id: frame.id,
1472
+ status: res.status,
1473
+ headers: { "content-type": contentType },
1474
+ body: respText
1475
+ });
1476
+ }
1477
+ function runExec(frame, bodyBuf, command, spawnFn) {
1478
+ return new Promise((resolve) => {
1479
+ const child = spawnFn(command, { shell: true });
1480
+ const out = [];
1481
+ const errOut = [];
1482
+ child.stdout?.on("data", (d) => out.push(Buffer.from(d)));
1483
+ child.stderr?.on("data", (d) => errOut.push(Buffer.from(d)));
1484
+ child.on(
1485
+ "error",
1486
+ (e) => resolve(errorResponse(frame.id, 502, `exec error: ${e.message}`))
1487
+ );
1488
+ child.on("close", (code) => {
1489
+ const stdout = Buffer.concat(out).toString("utf-8");
1490
+ if (code === 0) {
1491
+ resolve({
1492
+ type: "a2a_response",
1493
+ id: frame.id,
1494
+ status: 200,
1495
+ headers: { "content-type": "application/json" },
1496
+ body: stdout
1497
+ });
1498
+ } else {
1499
+ const stderr = Buffer.concat(errOut).toString("utf-8");
1500
+ resolve(
1501
+ errorResponse(frame.id, 500, `exec exited ${code}: ${stderr.slice(0, 500)}`)
1502
+ );
1503
+ }
1504
+ });
1505
+ child.stdin?.end(bodyBuf);
1506
+ });
1507
+ }
1508
+ function rawToString(data) {
1509
+ if (typeof data === "string") return data;
1510
+ if (Buffer.isBuffer(data)) return data.toString("utf-8");
1511
+ if (Array.isArray(data)) return Buffer.concat(data).toString("utf-8");
1512
+ return Buffer.from(data).toString("utf-8");
1513
+ }
1514
+ var KEEPALIVE_INTERVAL_MS = 3e4;
1515
+ var INITIAL_BACKOFF_MS = 1e3;
1516
+ var MAX_BACKOFF_MS = 3e4;
1517
+ function runListener(cfg) {
1518
+ const wsUrl = toWebsocketUrl(cfg.baseUrl, cfg.agentId);
1519
+ let backoff = INITIAL_BACKOFF_MS;
1520
+ let stopped = false;
1521
+ const connect = () => {
1522
+ const ws = new import_ws.default(wsUrl, {
1523
+ headers: { Authorization: `Bearer ${cfg.apiKey}` }
1524
+ });
1525
+ let keepalive;
1526
+ ws.on("open", () => {
1527
+ console.error(`[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl}`);
1528
+ backoff = INITIAL_BACKOFF_MS;
1529
+ keepalive = setInterval(() => {
1530
+ if (ws.readyState === import_ws.default.OPEN) {
1531
+ ws.send(JSON.stringify({ type: "ping" }));
1532
+ }
1533
+ }, KEEPALIVE_INTERVAL_MS);
1534
+ });
1535
+ ws.on("message", (data) => {
1536
+ let frame;
1537
+ try {
1538
+ frame = JSON.parse(rawToString(data));
1539
+ } catch {
1540
+ return;
1541
+ }
1542
+ if (!frame || typeof frame !== "object") return;
1543
+ const f = frame;
1544
+ if (f.type === "a2a_request" && typeof f.id === "string") {
1545
+ const send = (out) => {
1546
+ if (ws.readyState === import_ws.default.OPEN) {
1547
+ ws.send(JSON.stringify(out));
1548
+ }
1549
+ };
1550
+ void dispatchA2aRequest(f, cfg, send);
1551
+ }
1552
+ });
1553
+ ws.on("close", (code, reason) => {
1554
+ if (keepalive) clearInterval(keepalive);
1555
+ if (stopped) return;
1556
+ if (code === 4401 || code === 4403 || code === 4429) {
1557
+ console.error(
1558
+ `[acn listen] fatal close (code ${code}): ${reason.toString() || "see api_key / agent_id"}`
1559
+ );
1560
+ process.exit(1);
1561
+ }
1562
+ console.error(
1563
+ `[acn listen] disconnected (code ${code}); reconnecting in ${backoff / 1e3}s`
1564
+ );
1565
+ setTimeout(connect, backoff);
1566
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
1567
+ });
1568
+ ws.on("error", (err) => {
1569
+ console.error(`[acn listen] socket error: ${err.message}`);
1570
+ });
1571
+ };
1572
+ process.on("SIGINT", () => {
1573
+ stopped = true;
1574
+ console.error("\n[acn listen] stopping");
1575
+ process.exit(0);
1576
+ });
1577
+ connect();
1578
+ }
1579
+ function listenCommand() {
1580
+ 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."
1582
+ ).option(
1583
+ "--forward <url>",
1584
+ "Tunnel each relayed request to a local HTTP server (e.g. http://localhost:8080)"
1585
+ ).option(
1586
+ "--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>`."
1601
+ );
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);
1611
+ }
1612
+ runListener({
1613
+ agentId,
1614
+ apiKey,
1615
+ baseUrl: config.base_url,
1616
+ forward: opts.forward,
1617
+ exec: opts.exec
1618
+ });
1619
+ });
1620
+ return cmd;
1621
+ }
1622
+
1623
+ // src/commands/session.ts
1624
+ var import_commander11 = require("commander");
1235
1625
  function requireAgentId3() {
1236
1626
  const config = loadConfig();
1237
1627
  if (!config.api_key) {
@@ -1276,7 +1666,7 @@ function formatEntry2(s, index) {
1276
1666
  return lines.join("\n");
1277
1667
  }
1278
1668
  function sessionCommand() {
1279
- const cmd = new import_commander10.Command("session").description(
1669
+ const cmd = new import_commander11.Command("session").description(
1280
1670
  "Real-time session layer: bidirectional channel between two agents"
1281
1671
  );
1282
1672
  cmd.command("invite <target_agent_id>").description("Invite an agent to a real-time session").option("--ttl-seconds <s>", "Session TTL in seconds (60\u20131800, default 300)", parseInt).option("--metadata <json>", "Optional JSON object attached to the invitation (max 4KB)").action(
@@ -1354,7 +1744,7 @@ ${formatEntry2(res)}`);
1354
1744
  }
1355
1745
 
1356
1746
  // src/commands/subnet.ts
1357
- var import_commander11 = require("commander");
1747
+ var import_commander12 = require("commander");
1358
1748
  function requireAgentId4() {
1359
1749
  const config = loadConfig();
1360
1750
  if (!config.api_key) {
@@ -1442,7 +1832,7 @@ function formatSubnet(s, index) {
1442
1832
  return lines.join("\n");
1443
1833
  }
1444
1834
  function subnetCommand() {
1445
- const cmd = new import_commander11.Command("subnet").description("Manage ACN subnets");
1835
+ const cmd = new import_commander12.Command("subnet").description("Manage ACN subnets");
1446
1836
  cmd.command("list").description(
1447
1837
  "List subnets. Without --all/--parent shows only subnets you have joined."
1448
1838
  ).option("--all", "Show all public subnets on ACN (not just your own)").option(
@@ -1659,7 +2049,7 @@ ${JSON.stringify(res.agents, null, 2)}`);
1659
2049
  handleError(err);
1660
2050
  }
1661
2051
  });
1662
- const requests = new import_commander11.Command("requests").description(
2052
+ const requests = new import_commander12.Command("requests").description(
1663
2053
  "Manage join-requests for a subnet (ADR-0004)"
1664
2054
  );
1665
2055
  requests.command("list <subnet_id>").description(
@@ -1801,7 +2191,7 @@ ${JSON.stringify(res.agents, null, 2)}`);
1801
2191
  }
1802
2192
  );
1803
2193
  cmd.addCommand(requests);
1804
- const invitations = new import_commander11.Command("invitations").description(
2194
+ const invitations = new import_commander12.Command("invitations").description(
1805
2195
  "Manage invitations on a subnet (ADR-0004)"
1806
2196
  );
1807
2197
  invitations.command("send <subnet_id>").description(
@@ -1937,7 +2327,7 @@ ${JSON.stringify(res.agents, null, 2)}`);
1937
2327
  }
1938
2328
  );
1939
2329
  cmd.addCommand(invitations);
1940
- const allowlist = new import_commander11.Command("allowlist").description(
2330
+ const allowlist = new import_commander12.Command("allowlist").description(
1941
2331
  "Manage a subnet allowlist (ADR-0004)"
1942
2332
  );
1943
2333
  allowlist.command("list <subnet_id>").description("Owner-only: list allowlist entries for a subnet.").option("--limit <n>", "Page size (1-500, default 100)", "100").option("--offset <n>", "Page offset (default 0)", "0").action(
@@ -2005,7 +2395,7 @@ ${JSON.stringify(res.agents, null, 2)}`);
2005
2395
  }
2006
2396
  );
2007
2397
  cmd.addCommand(allowlist);
2008
- const harness = new import_commander11.Command("harness").description("Manage Org Harness webhook for a subnet");
2398
+ const harness = new import_commander12.Command("harness").description("Manage Org Harness webhook for a subnet");
2009
2399
  harness.command("set <subnet_id>").description("Register an Org Harness webhook on a subnet you own").requiredOption("--url <url>", "Harness webhook URL (HTTPS)").option("--secret <secret>", "HMAC-SHA256 signing secret (recommended)").action(async (subnetId, opts) => {
2010
2400
  const config = loadConfig();
2011
2401
  if (!config.api_key) {
@@ -2048,7 +2438,7 @@ ${JSON.stringify(res.agents, null, 2)}`);
2048
2438
  }
2049
2439
 
2050
2440
  // src/commands/follow.ts
2051
- var import_commander12 = require("commander");
2441
+ var import_commander13 = require("commander");
2052
2442
  function requireAgentId5() {
2053
2443
  const config = loadConfig();
2054
2444
  if (!config.api_key) {
@@ -2071,7 +2461,7 @@ function formatAgent2(a, i) {
2071
2461
  ].join("\n");
2072
2462
  }
2073
2463
  function followCommand() {
2074
- const cmd = new import_commander12.Command("follow").description("Follow/unfollow agents and inspect follow graph");
2464
+ const cmd = new import_commander13.Command("follow").description("Follow/unfollow agents and inspect follow graph");
2075
2465
  cmd.command("add <target_id>").description("Follow another agent").option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (targetId, opts) => {
2076
2466
  const agentId = opts.agentId ?? requireAgentId5();
2077
2467
  try {
@@ -2162,7 +2552,7 @@ function followCommand() {
2162
2552
  }
2163
2553
 
2164
2554
  // src/commands/wallet.ts
2165
- var import_commander13 = require("commander");
2555
+ var import_commander14 = require("commander");
2166
2556
  function requireAgentId6() {
2167
2557
  const config = loadConfig();
2168
2558
  if (!config.api_key) {
@@ -2205,7 +2595,7 @@ async function showWalletInfo(opts) {
2205
2595
  }
2206
2596
  }
2207
2597
  function walletCommand() {
2208
- const cmd = new import_commander13.Command("wallet").description("View and manage agent's wallet & payment info");
2598
+ const cmd = new import_commander14.Command("wallet").description("View and manage agent's wallet & payment info");
2209
2599
  cmd.command("info", { isDefault: true }).description("Show wallet, payment methods, pricing, and ERC-8004 status").option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(showWalletInfo);
2210
2600
  cmd.command("set-capability").description("Declare which payment methods, networks, and wallets you accept").requiredOption(
2211
2601
  "--methods <csv>",
@@ -2384,7 +2774,7 @@ function walletCommand() {
2384
2774
  }
2385
2775
 
2386
2776
  // src/commands/pay.ts
2387
- var import_commander14 = require("commander");
2777
+ var import_commander15 = require("commander");
2388
2778
  function requireAgentId7() {
2389
2779
  const config = loadConfig();
2390
2780
  if (!config.api_key) {
@@ -2398,8 +2788,8 @@ function requireAgentId7() {
2398
2788
  return config.agent_id;
2399
2789
  }
2400
2790
  function payCommand() {
2401
- const cmd = new import_commander14.Command("pay").description("Manage payment tasks between agents");
2402
- const createCmd = new import_commander14.Command("create").description("Create a payment task to another agent");
2791
+ const cmd = new import_commander15.Command("pay").description("Manage payment tasks between agents");
2792
+ const createCmd = new import_commander15.Command("create").description("Create a payment task to another agent");
2403
2793
  createCmd.requiredOption("--to <agent>", "Recipient agent ID").requiredOption("--amount <n>", "Payment amount (positive number)").requiredOption("--currency <c>", "Currency code, e.g. USD, USDC").requiredOption("--method <m>", "Payment method, e.g. usdc, eth, platform_credits").requiredOption("--network <n>", "Network, e.g. ethereum, base, solana").option("--description <text>", "Free-text description for the payment task").option("--metadata <json>", "Additional metadata as JSON object").action(
2404
2794
  async (opts) => {
2405
2795
  const fromAgent = requireAgentId7();
@@ -2445,7 +2835,7 @@ function payCommand() {
2445
2835
  }
2446
2836
  }
2447
2837
  );
2448
- const confirmCmd = new import_commander14.Command("confirm").description(
2838
+ const confirmCmd = new import_commander15.Command("confirm").description(
2449
2839
  "Confirm an external payment has been made (buyer only)"
2450
2840
  );
2451
2841
  confirmCmd.requiredOption("--task-id <id>", "Payment task ID to confirm").requiredOption(
@@ -2467,7 +2857,7 @@ function payCommand() {
2467
2857
  handleError(err);
2468
2858
  }
2469
2859
  });
2470
- const statusCmd = new import_commander14.Command("status").description(
2860
+ const statusCmd = new import_commander15.Command("status").description(
2471
2861
  "Show payment tasks for the authenticated agent"
2472
2862
  );
2473
2863
  statusCmd.option("--status <s>", "Filter by status (e.g. created, payment_confirmed)").option("--limit <n>", "Max results (default 50)", "50").action(async (opts) => {
@@ -2490,7 +2880,7 @@ function payCommand() {
2490
2880
 
2491
2881
  // src/index.ts
2492
2882
  var { version } = require_package();
2493
- var program = new import_commander15.Command();
2883
+ var program = new import_commander16.Command();
2494
2884
  program.name("acn").description("ACN CLI \u2014 Agent Collaboration Network command-line interface").version(version).option("--json", "Output raw JSON (useful for agent parsing)").hook("preAction", (thisCommand) => {
2495
2885
  const opts = thisCommand.opts();
2496
2886
  if (opts.json) setJsonMode(true);
@@ -2504,6 +2894,7 @@ program.addCommand(tasksCommand());
2504
2894
  program.addCommand(messageCommand());
2505
2895
  program.addCommand(notifyCommand());
2506
2896
  program.addCommand(inboxCommand());
2897
+ program.addCommand(listenCommand());
2507
2898
  program.addCommand(sessionCommand());
2508
2899
  program.addCommand(subnetCommand());
2509
2900
  program.addCommand(followCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.2",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) — zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -38,10 +38,12 @@
38
38
  "url": "https://github.com/acnlabs/ACN/issues"
39
39
  },
40
40
  "dependencies": {
41
- "commander": "^12.0.0"
41
+ "commander": "^12.0.0",
42
+ "ws": "^8.18.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/node": "^20.0.0",
46
+ "@types/ws": "^8.5.10",
45
47
  "tsup": "^8.0.0",
46
48
  "typescript": "^5.0.0",
47
49
  "vitest": "^3.0.5"