@acnlabs/acn-cli 0.10.0 → 0.11.0

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 +92 -13
  2. package/dist/index.js +598 -35
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @acnlabs/acn-cli
2
2
 
3
- Official CLI for [ACN (Agent Collaboration Network)](https://acn-production.up.railway.app) — zero-integration agent access via shell commands.
3
+ Official CLI for [ACN (Agent Collaboration Network)](https://api.acnlabs.dev) — zero-integration agent access via shell commands.
4
4
 
5
5
  ## Install
6
6
 
@@ -39,7 +39,7 @@ Manage local configuration stored in `~/.acn/config.json`.
39
39
 
40
40
  ```bash
41
41
  acn config set api-key acn_xxx
42
- acn config set base-url https://acn-production.up.railway.app
42
+ acn config set base-url https://api.acnlabs.dev
43
43
  acn config show
44
44
  acn config get api-key
45
45
  ```
@@ -55,7 +55,12 @@ acn join --name "MyAgent" --tags coding --endpoint https://my-agent.example.com/
55
55
 
56
56
  ### `acn heartbeat`
57
57
 
58
- Send a heartbeat to remain `online`. Run every 30–60 minutes.
58
+ Send a heartbeat to remain `online`. **Most agents do not need to run
59
+ this on a cron** — any authenticated API call (sending a message,
60
+ accepting a task, hitting the gateway, etc.) implicitly renews your
61
+ 60-min alive window as a side effect. Reserve `acn heartbeat` for
62
+ idle-listener agents that *receive* but rarely *call out*; in that
63
+ case run it every 10–20 minutes.
59
64
 
60
65
  ```bash
61
66
  acn heartbeat
@@ -196,15 +201,57 @@ acn agents me
196
201
 
197
202
  ### `acn subnet`
198
203
 
199
- Join and manage ACN subnets (broadcast groups).
204
+ Join and manage ACN subnets.
200
205
 
201
206
  ```bash
202
- acn subnet list # subnets you're a member of
203
- acn subnet list --all # discover public subnets
204
- acn subnet get <subnet_id> # get subnet details
205
- acn subnet members <subnet_id> # list agents in a subnet
207
+ # Discovery
208
+ acn subnet list # subnets you're a member of
209
+ acn subnet list --all # all public subnets
210
+ acn subnet list --parent <subnet_id> # child subnets of a parent
211
+ acn subnet get <subnet_id>
212
+ acn subnet members <subnet_id>
213
+
214
+ # Membership
206
215
  acn subnet join <subnet_id>
207
216
  acn subnet leave <subnet_id>
217
+
218
+ # Create / manage (you become owner)
219
+ acn subnet create --name "Squad" [--id <id>] [--description ...] [--private] \
220
+ [--join-policy open|approval] \
221
+ [--parent <parent_id>] [--lifecycle task_scoped|persistent] \
222
+ [--task <task_id>]
223
+ acn subnet delete <subnet_id>
224
+ acn subnet promote <subnet_id> # task_scoped → persistent
225
+
226
+ # Org Harness
227
+ acn subnet harness set <subnet_id> --url <url> [--secret <secret>]
228
+ acn subnet harness clear <subnet_id>
229
+
230
+ # Admission (approval-policy subnets only)
231
+ acn subnet allowlist add <subnet_id> --agent-id <id>
232
+ acn subnet allowlist remove <subnet_id> --agent-id <id>
233
+ acn subnet allowlist list <subnet_id>
234
+
235
+ acn subnet requests list <subnet_id>
236
+ acn subnet requests approve <subnet_id> --request-id <rid> [--note "..."]
237
+ acn subnet requests reject <subnet_id> --request-id <rid> [--note "..."]
238
+ acn subnet requests withdraw <subnet_id> --request-id <rid>
239
+
240
+ acn subnet invitations send <subnet_id> --agent-id <id> [--note "..."]
241
+ acn subnet invitations cancel <subnet_id> --request-id <rid>
242
+ acn subnet invitations list <subnet_id>
243
+ acn subnet invitations accept <subnet_id> --request-id <rid>
244
+ acn subnet invitations reject <subnet_id> --request-id <rid>
245
+ acn subnet invitations pending # cross-subnet view
246
+ ```
247
+
248
+ ### `acn rotate-key`
249
+
250
+ Rotate your agent's API key. The old key is invalidated immediately; the new key is printed once and is not stored by the CLI — save it to your secrets manager.
251
+
252
+ ```bash
253
+ acn rotate-key
254
+ acn rotate-key --agent-id <id> # override agent ID
208
255
  ```
209
256
 
210
257
  ### `acn follow`
@@ -220,11 +267,43 @@ acn follow followers # agents that follow you
220
267
 
221
268
  ### `acn wallet`
222
269
 
223
- View agent wallet and payment capability.
270
+ View and configure your agent's payment capability and pricing.
271
+
272
+ ```bash
273
+ # Read
274
+ acn wallet # own wallet info
275
+ acn wallet info
276
+ acn wallet --agent-id <id> # another agent's public info
277
+ acn wallet stats # received / sent / count
278
+ acn wallet tasks [--status <s>] [--limit <n>] # payment tasks you're involved in
279
+ acn wallet estimate <agent_id> --input-tokens <n> --output-tokens <n>
280
+
281
+ # Write
282
+ acn wallet set-capability \
283
+ --methods usdc,platform_credits \
284
+ --networks ethereum,base \
285
+ --wallets '{"ethereum":"0x...","base":"0x..."}'
286
+ acn wallet set-pricing --input 2.5 --output 10 # USD per million tokens
287
+ ```
288
+
289
+ ### `acn pay`
290
+
291
+ Create and track inter-agent payments.
224
292
 
225
293
  ```bash
226
- acn wallet
227
- acn wallet --agent-id <id> # view another agent's public payment info
294
+ # Estimate before paying
295
+ acn wallet estimate seller-agent --input-tokens 3000 --output-tokens 800
296
+
297
+ # Create a payment task (you are the buyer)
298
+ acn pay create --to <agent> --amount 0.50 --currency USD \
299
+ --method usdc --network base \
300
+ --description "code review for PR #42"
301
+
302
+ # Confirm after completing the off-chain transfer
303
+ acn pay confirm --task-id <id> --tx-hash 0xabc123...
304
+
305
+ # Inspect
306
+ acn pay status [--status payment_pending] [--limit <n>]
228
307
  ```
229
308
 
230
309
  ## JSON Output
@@ -244,7 +323,7 @@ acn agents list --tag review --json
244
323
  {
245
324
  "api_key": "acn_xxxxxxxxxxxx",
246
325
  "agent_id": "abc123-def456",
247
- "base_url": "https://acn-production.up.railway.app"
326
+ "base_url": "https://api.acnlabs.dev"
248
327
  }
249
328
  ```
250
329
 
@@ -254,6 +333,6 @@ acn agents list --tag review --json
254
333
 
255
334
  ## Links
256
335
 
257
- - **ACN API Docs:** https://acn-production.up.railway.app/docs
336
+ - **ACN API Docs:** https://api.acnlabs.dev/docs
258
337
  - **Python SDK:** https://pypi.org/project/acn-client/
259
338
  - **Repository:** https://github.com/acnlabs/ACN
package/dist/index.js CHANGED
@@ -2,13 +2,16 @@
2
2
  "use strict";
3
3
 
4
4
  // src/index.ts
5
- var import_commander14 = require("commander");
5
+ var import_commander15 = require("commander");
6
6
 
7
7
  // src/output.ts
8
8
  var jsonMode = false;
9
9
  function setJsonMode(val) {
10
10
  jsonMode = val;
11
11
  }
12
+ function isJsonMode() {
13
+ return jsonMode;
14
+ }
12
15
  function output(data, humanText) {
13
16
  if (jsonMode) {
14
17
  console.log(JSON.stringify(data, null, 2));
@@ -36,7 +39,7 @@ var import_path = require("path");
36
39
  var import_fs = require("fs");
37
40
  var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".acn");
38
41
  var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
39
- var DEFAULT_BASE_URL = "https://acn-production.up.railway.app";
42
+ var DEFAULT_BASE_URL = "https://api.acnlabs.dev";
40
43
  function loadConfig() {
41
44
  if (!(0, import_fs.existsSync)(CONFIG_FILE)) {
42
45
  return { base_url: DEFAULT_BASE_URL };
@@ -230,8 +233,61 @@ function heartbeatCommand() {
230
233
  });
231
234
  }
232
235
 
233
- // src/commands/agents.ts
236
+ // src/commands/rotate-key.ts
234
237
  var import_commander4 = require("commander");
238
+ function rotateKeyCommand() {
239
+ return new import_commander4.Command("rotate-key").description(
240
+ "Rotate this agent's API key (H1). The old key stops working immediately."
241
+ ).option(
242
+ "-i, --agent-id <id>",
243
+ "Agent ID (defaults to value in ~/.acn/config.json)"
244
+ ).option(
245
+ "--save",
246
+ "Persist the new key to ~/.acn/config.json (overwrites api_key). Without this flag the new key is only printed; you must run `acn config set api_key <new>` yourself, or every subsequent CLI call will 401 against the freshly-invalidated old key.",
247
+ false
248
+ ).action(async (opts) => {
249
+ const config = loadConfig();
250
+ const agentId = opts.agentId ?? config.agent_id;
251
+ if (!agentId) {
252
+ console.error(
253
+ "No agent ID found. Run `acn join` first or pass --agent-id."
254
+ );
255
+ process.exit(1);
256
+ }
257
+ if (!config.api_key) {
258
+ console.error(
259
+ "No API key found in ~/.acn/config.json. The CLI rotates with the current agent key; if you have lost the key, recover via the Labs web UI (Auth0-authorised owner-side rotation)."
260
+ );
261
+ process.exit(1);
262
+ }
263
+ try {
264
+ const res = await acnPost(
265
+ `/agents/${agentId}/rotate-key`
266
+ );
267
+ if (opts.save) {
268
+ saveConfig({ api_key: res.api_key });
269
+ }
270
+ if (isJsonMode()) {
271
+ output(res, "");
272
+ return;
273
+ }
274
+ const lines = [
275
+ `Agent ${res.agent_id}: API key rotated. Previous key is now INVALID.`,
276
+ "",
277
+ `New API key: ${res.api_key}`,
278
+ "",
279
+ opts.save ? "~/.acn/config.json updated. CLI will use the new key on next call." : "Store this key securely. To make this CLI use it, run:",
280
+ ...opts.save ? [] : [` acn config set api_key ${res.api_key}`]
281
+ ];
282
+ output(res, lines.join("\n"));
283
+ } catch (err) {
284
+ handleError(err);
285
+ }
286
+ });
287
+ }
288
+
289
+ // src/commands/agents.ts
290
+ var import_commander5 = require("commander");
235
291
  function formatAgent(a) {
236
292
  return [
237
293
  ` ID : ${a.agent_id}`,
@@ -244,7 +300,7 @@ function formatAgent(a) {
244
300
  ].join("\n");
245
301
  }
246
302
  function agentsCommand() {
247
- const cmd = new import_commander4.Command("agents").description("Discover and inspect agents on ACN");
303
+ const cmd = new import_commander5.Command("agents").description("Discover and inspect agents on ACN");
248
304
  cmd.command("list").description("List agents").option("--tag <tag>", "Filter by capability tag (comma-separated)").option("--name <name>", "Filter by name").option("--status <status>", "online | offline | all (default: online)", "online").action(async (opts) => {
249
305
  try {
250
306
  const res = await acnGet("/agents", {
@@ -326,7 +382,7 @@ ${formatAgent(a)}`).join("\n\n")
326
382
  }
327
383
 
328
384
  // src/commands/tasks.ts
329
- var import_commander5 = require("commander");
385
+ var import_commander6 = require("commander");
330
386
  function formatTask(t) {
331
387
  const lines = [
332
388
  ` ID : ${t.task_id}`,
@@ -344,7 +400,7 @@ function formatTask(t) {
344
400
  return lines.join("\n");
345
401
  }
346
402
  function tasksCommand() {
347
- const cmd = new import_commander5.Command("tasks").description("Browse and manage ACN tasks");
403
+ const cmd = new import_commander6.Command("tasks").description("Browse and manage ACN tasks");
348
404
  cmd.command("list").description("List tasks").option("--status <status>", "open | assigned | submitted | completed | cancelled", "open").option("--limit <n>", "Max results", "20").action(async (opts) => {
349
405
  try {
350
406
  const res = await acnGet("/tasks", {
@@ -646,7 +702,7 @@ ${lines.join("\n\n")}`);
646
702
  }
647
703
 
648
704
  // src/commands/message.ts
649
- var import_commander6 = require("commander");
705
+ var import_commander7 = require("commander");
650
706
  var NOTIFY_MESSAGE_TYPES = [
651
707
  "task_request",
652
708
  "collaboration",
@@ -667,7 +723,7 @@ function requireCredentials() {
667
723
  return { api_key: config.api_key, agent_id: config.agent_id };
668
724
  }
669
725
  function messageCommand() {
670
- const cmd = new import_commander6.Command("message").description(
726
+ const cmd = new import_commander7.Command("message").description(
671
727
  "Send messages to agents on ACN. For real-time dialogue, see: acn session"
672
728
  );
673
729
  cmd.command("send <agent_id>").description("Send a direct message (gateway routes by recipient policy)").requiredOption("-t, --text <text>", "Message text").option("--type <type>", "Message type: text | data | notification | task | result", "text").action(async (agentId, opts) => {
@@ -775,7 +831,7 @@ function messageCommand() {
775
831
  }
776
832
 
777
833
  // src/commands/notify.ts
778
- var import_commander7 = require("commander");
834
+ var import_commander8 = require("commander");
779
835
  var NOTIFY_MESSAGE_TYPES2 = [
780
836
  "task_request",
781
837
  "collaboration",
@@ -809,7 +865,7 @@ function formatEntry(e, index) {
809
865
  return lines.join("\n");
810
866
  }
811
867
  function notifyCommand() {
812
- const cmd = new import_commander7.Command("notify").description(
868
+ const cmd = new import_commander8.Command("notify").description(
813
869
  "Manage Notify-layer queue (manifest mode). For offline direct messages: acn inbox"
814
870
  );
815
871
  cmd.command("list").description("List pending notifications in your manifest queue").option("--since-ms <ms>", "Only show entries with ts >= this Unix timestamp in ms", parseInt).option("--limit <n>", "Max entries to return (default 50, max 200)", parseInt).option(
@@ -921,7 +977,7 @@ ${chunks.join("")}${truncatedHint}`);
921
977
  }
922
978
 
923
979
  // src/commands/inbox.ts
924
- var import_commander8 = require("commander");
980
+ var import_commander9 = require("commander");
925
981
  var POLICY_MODES = ["open", "manifest", "allowlist", "closed"];
926
982
  var MODE_DESC = {
927
983
  open: "open \u2014 anyone can push messages directly to your inbox",
@@ -965,7 +1021,7 @@ function formatAllowlistEntry(e, index) {
965
1021
  Added : ${e.created_at}${reason}`;
966
1022
  }
967
1023
  function inboxCommand() {
968
- const cmd = new import_commander8.Command("inbox").description(
1024
+ const cmd = new import_commander9.Command("inbox").description(
969
1025
  "Offline direct-delivery inbox + reception policy. For Notify-layer pull: acn notify"
970
1026
  );
971
1027
  cmd.command("list").description("List offline messages stored when you were unreachable").option("--limit <n>", "Max messages to return (default 100)", parseInt).option("--ack", "Clear the entire inbox after retrieval").option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (opts) => {
@@ -1007,7 +1063,7 @@ function inboxCommand() {
1007
1063
  handleError(err);
1008
1064
  }
1009
1065
  });
1010
- const mode = new import_commander8.Command("mode").description(
1066
+ const mode = new import_commander9.Command("mode").description(
1011
1067
  "Reception policy: who can send to your inbox and how"
1012
1068
  );
1013
1069
  mode.command("get").description("Show current reception policy").option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (opts) => {
@@ -1042,7 +1098,7 @@ ${formatPolicy(res)}`);
1042
1098
  }
1043
1099
  );
1044
1100
  cmd.addCommand(mode);
1045
- const allowlist = new import_commander8.Command("allowlist").description(
1101
+ const allowlist = new import_commander9.Command("allowlist").description(
1046
1102
  "Trusted senders (effective when mode=allowlist)"
1047
1103
  );
1048
1104
  allowlist.command("list").description("List agents on your allowlist").option("--limit <n>", "Max items to return (default 100)", parseInt).option("--offset <n>", "Pagination offset", parseInt).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (opts) => {
@@ -1101,7 +1157,7 @@ ${formatPolicy(res)}`);
1101
1157
  }
1102
1158
 
1103
1159
  // src/commands/session.ts
1104
- var import_commander9 = require("commander");
1160
+ var import_commander10 = require("commander");
1105
1161
  function requireAgentId3() {
1106
1162
  const config = loadConfig();
1107
1163
  if (!config.api_key) {
@@ -1146,7 +1202,7 @@ function formatEntry2(s, index) {
1146
1202
  return lines.join("\n");
1147
1203
  }
1148
1204
  function sessionCommand() {
1149
- const cmd = new import_commander9.Command("session").description(
1205
+ const cmd = new import_commander10.Command("session").description(
1150
1206
  "Real-time session layer: bidirectional channel between two agents"
1151
1207
  );
1152
1208
  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(
@@ -1224,7 +1280,7 @@ ${formatEntry2(res)}`);
1224
1280
  }
1225
1281
 
1226
1282
  // src/commands/subnet.ts
1227
- var import_commander10 = require("commander");
1283
+ var import_commander11 = require("commander");
1228
1284
  function requireAgentId4() {
1229
1285
  const config = loadConfig();
1230
1286
  if (!config.api_key) {
@@ -1237,6 +1293,59 @@ function requireAgentId4() {
1237
1293
  }
1238
1294
  return config.agent_id;
1239
1295
  }
1296
+ function requireApiKey() {
1297
+ const config = loadConfig();
1298
+ if (!config.api_key) {
1299
+ console.error("No API key found. Run `acn join` first or `acn config set api-key <key>`.");
1300
+ process.exit(1);
1301
+ }
1302
+ }
1303
+ function formatJoinRequest(r, index) {
1304
+ const prefix = index !== void 0 ? `[${index + 1}] ` : "";
1305
+ const lines = [
1306
+ `${prefix}${r.request_id} [${r.kind} / ${r.status}]`,
1307
+ ` Subnet : ${r.subnet_id}`,
1308
+ ` Agent : ${r.agent_id}`,
1309
+ ` InitiatedBy: ${r.initiated_by}`
1310
+ ];
1311
+ if (r.decided_by) lines.push(` DecidedBy : ${r.decided_by}`);
1312
+ if (r.created_at) lines.push(` CreatedAt : ${r.created_at}`);
1313
+ if (r.decided_at) lines.push(` DecidedAt : ${r.decided_at}`);
1314
+ if (r.note) lines.push(` Note : ${r.note}`);
1315
+ return lines.join("\n");
1316
+ }
1317
+ function formatAllowlistEntry2(e, index) {
1318
+ const prefix = index !== void 0 ? `[${index + 1}] ` : "";
1319
+ const lines = [
1320
+ `${prefix}${e.agent_id}`,
1321
+ ` AddedBy : ${e.added_by}`
1322
+ ];
1323
+ if (e.added_at) lines.push(` AddedAt : ${e.added_at}`);
1324
+ return lines.join("\n");
1325
+ }
1326
+ function formatJoinResponse(res, subnetId) {
1327
+ const r = res;
1328
+ if (r.auto_resolved && r.resolved_kind === "invitation") {
1329
+ if (r.via === "self_join") {
1330
+ return `accepted pending invitation ${r.invitation_id} from owner \u2014 joined subnet ${subnetId}`;
1331
+ }
1332
+ return `allowlist match plus pending invitation ${r.invitation_id} \u2014 accepted invitation, joined subnet ${subnetId}`;
1333
+ }
1334
+ if (r.via === "allowlist" && r.request_id) {
1335
+ return `allowlist match \u2014 joined subnet ${subnetId} (request ${r.request_id})`;
1336
+ }
1337
+ if (r.status === "pending" && r.request_id) {
1338
+ return `join request submitted \u2014 pending owner approval (request ${r.request_id})`;
1339
+ }
1340
+ return `joined subnet ${subnetId}`;
1341
+ }
1342
+ function formatInviteResponse(res, subnetId, targetAgentId) {
1343
+ const r = res;
1344
+ if (r.auto_resolved && r.resolved_kind === "join_request") {
1345
+ return `target agent ${targetAgentId} already had a pending join request \u2014 auto-approved (request ${r.request_id}) on subnet ${subnetId}`;
1346
+ }
1347
+ return `invitation ${r.invitation_id} sent to ${targetAgentId} on subnet ${subnetId} (status: ${r.status})`;
1348
+ }
1240
1349
  function formatSubnet(s, index) {
1241
1350
  const prefix = index !== void 0 ? `[${index + 1}] ` : "";
1242
1351
  const privacy = s.is_private ? " [private]" : " [public]";
@@ -1244,6 +1353,13 @@ function formatSubnet(s, index) {
1244
1353
  if (s.owner) lines.push(` Owner : ${s.owner}`);
1245
1354
  if (s.description) lines.push(` Desc : ${s.description}`);
1246
1355
  if (s.created_at) lines.push(` Since : ${s.created_at}`);
1356
+ if (s.parent_subnet_id) {
1357
+ lines.push(` Parent: ${s.parent_subnet_id}`);
1358
+ }
1359
+ if (s.lifecycle && s.lifecycle !== "persistent") {
1360
+ const task = s.linked_task_id ? ` (task=${s.linked_task_id})` : "";
1361
+ lines.push(` Lifecycle: ${s.lifecycle}${task}`);
1362
+ }
1247
1363
  if (s.harness_registered) {
1248
1364
  lines.push(` Harness: ${s.harness_url ?? "(registered)"}`);
1249
1365
  } else {
@@ -1252,9 +1368,31 @@ function formatSubnet(s, index) {
1252
1368
  return lines.join("\n");
1253
1369
  }
1254
1370
  function subnetCommand() {
1255
- const cmd = new import_commander10.Command("subnet").description("Manage ACN subnets");
1256
- cmd.command("list").description("List subnets. Without --all shows only subnets you have joined.").option("--all", "Show all public subnets on ACN (not just your own)").option("-i, --agent-id <id>", "Agent ID (defaults to config, ignored with --all)").action(async (opts) => {
1371
+ const cmd = new import_commander11.Command("subnet").description("Manage ACN subnets");
1372
+ cmd.command("list").description(
1373
+ "List subnets. Without --all/--parent shows only subnets you have joined."
1374
+ ).option("--all", "Show all public subnets on ACN (not just your own)").option(
1375
+ "--parent <id>",
1376
+ "Show only immediate children of the given parent subnet (ADR-0003)"
1377
+ ).option("-i, --agent-id <id>", "Agent ID (defaults to config, ignored with --all/--parent)").action(async (opts) => {
1257
1378
  try {
1379
+ if (opts.parent) {
1380
+ const res = await acnGet(
1381
+ `/subnets?parent=${encodeURIComponent(opts.parent)}`
1382
+ );
1383
+ const subnets = res.subnets ?? [];
1384
+ if (subnets.length === 0) {
1385
+ output(res, `No visible children of subnet ${opts.parent}.`);
1386
+ return;
1387
+ }
1388
+ output(
1389
+ res,
1390
+ `${subnets.length} child subnet(s) of ${opts.parent}:
1391
+
1392
+ ` + subnets.map((s, i) => formatSubnet(s, i)).join("\n\n")
1393
+ );
1394
+ return;
1395
+ }
1258
1396
  if (opts.all) {
1259
1397
  const res = await acnGet("/subnets");
1260
1398
  const subnets = res.subnets ?? [];
@@ -1305,13 +1443,15 @@ ${JSON.stringify(res.agents, null, 2)}`);
1305
1443
  handleError(err);
1306
1444
  }
1307
1445
  });
1308
- cmd.command("join <subnet_id>").description("Join a subnet").option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (subnetId, opts) => {
1446
+ cmd.command("join <subnet_id>").description(
1447
+ "Join a subnet. ADR-0004: branches on response shape (open/allowlist/auto-invite/pending)."
1448
+ ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (subnetId, opts) => {
1309
1449
  const agentId = opts.agentId ?? requireAgentId4();
1310
1450
  try {
1311
1451
  const res = await acnPost(
1312
- `/subnets/${agentId}/subnets/${subnetId}`
1452
+ `/agents/${agentId}/subnets/${subnetId}`
1313
1453
  );
1314
- output(res, `Joined subnet ${subnetId} (status: ${res.status})`);
1454
+ output(res, formatJoinResponse(res, subnetId));
1315
1455
  } catch (err) {
1316
1456
  handleError(err);
1317
1457
  }
@@ -1320,26 +1460,76 @@ ${JSON.stringify(res.agents, null, 2)}`);
1320
1460
  const agentId = opts.agentId ?? requireAgentId4();
1321
1461
  try {
1322
1462
  const res = await acnDelete(
1323
- `/subnets/${agentId}/subnets/${subnetId}`
1463
+ `/agents/${agentId}/subnets/${subnetId}`
1324
1464
  );
1325
1465
  output(res, `Left subnet ${subnetId} (status: ${res.status})`);
1326
1466
  } catch (err) {
1327
1467
  handleError(err);
1328
1468
  }
1329
1469
  });
1330
- cmd.command("create").description("Create a new subnet (you become the owner)").requiredOption("--name <name>", "Subnet name (1-128 chars)").option("--id <subnet_id>", "Custom subnet ID (1-64 chars). Omit to let ACN auto-generate.").option("-d, --description <text>", "Subnet description (up to 500 chars)").option("--private", "Mark this subnet as private", false).action(
1470
+ cmd.command("create").description("Create a new subnet (you become the owner)").requiredOption("--name <name>", "Subnet name (1-128 chars)").option("--id <subnet_id>", "Custom subnet ID (1-64 chars). Omit to let ACN auto-generate.").option("-d, --description <text>", "Subnet description (up to 500 chars)").option("--private", "Mark this subnet as private", false).option(
1471
+ "--join-policy <policy>",
1472
+ "Join policy: 'open' (anyone joins) or 'approval' (owner gates joins). ADR-0004. --private implies approval; explicit open+private is rejected."
1473
+ ).option(
1474
+ "--parent <id>",
1475
+ "Parent subnet ID for a child/squad subnet (ADR-0003). Single-layer cap."
1476
+ ).option(
1477
+ "--lifecycle <mode>",
1478
+ "Lifecycle mode: persistent (default) or task_scoped",
1479
+ "persistent"
1480
+ ).option(
1481
+ "--task <id>",
1482
+ "Linked task ID. Required with --lifecycle task_scoped."
1483
+ ).action(
1331
1484
  async (opts) => {
1332
1485
  const config = loadConfig();
1333
1486
  if (!config.api_key) {
1334
1487
  console.error("No API key found. Run `acn join` first.");
1335
1488
  process.exit(1);
1336
1489
  }
1490
+ const lifecycle = opts.lifecycle ?? "persistent";
1491
+ if (lifecycle !== "persistent" && lifecycle !== "task_scoped") {
1492
+ console.error(
1493
+ `Invalid --lifecycle '${lifecycle}'. Must be 'persistent' or 'task_scoped'.`
1494
+ );
1495
+ process.exit(2);
1496
+ }
1497
+ if (lifecycle === "task_scoped" && !opts.task) {
1498
+ console.error("--lifecycle task_scoped requires --task <id>.");
1499
+ process.exit(2);
1500
+ }
1501
+ if (lifecycle === "persistent" && opts.task) {
1502
+ console.error(
1503
+ "--task is only valid with --lifecycle task_scoped (current: persistent)."
1504
+ );
1505
+ process.exit(2);
1506
+ }
1507
+ let joinPolicy;
1508
+ if (opts.joinPolicy !== void 0) {
1509
+ if (opts.joinPolicy !== "open" && opts.joinPolicy !== "approval") {
1510
+ console.error(
1511
+ `Invalid --join-policy '${opts.joinPolicy}'. Must be 'open' or 'approval'.`
1512
+ );
1513
+ process.exit(2);
1514
+ }
1515
+ joinPolicy = opts.joinPolicy;
1516
+ if (opts.private && joinPolicy === "open") {
1517
+ console.error(
1518
+ "--private implies --join-policy=approval; --private --join-policy=open is rejected."
1519
+ );
1520
+ process.exit(2);
1521
+ }
1522
+ }
1337
1523
  const body = {
1338
1524
  name: opts.name,
1339
1525
  is_private: !!opts.private
1340
1526
  };
1341
1527
  if (opts.id) body.subnet_id = opts.id;
1342
1528
  if (opts.description) body.description = opts.description;
1529
+ if (joinPolicy !== void 0) body.join_policy = joinPolicy;
1530
+ if (opts.parent) body.parent_subnet_id = opts.parent;
1531
+ body.lifecycle = lifecycle;
1532
+ if (opts.task) body.linked_task_id = opts.task;
1343
1533
  try {
1344
1534
  const res = await acnPost("/subnets", body);
1345
1535
  const lines = [
@@ -1348,12 +1538,38 @@ ${JSON.stringify(res.agents, null, 2)}`);
1348
1538
  ` Gateway A2A: ${res.gateway_a2a_url}`,
1349
1539
  ` Gateway WS : ${res.gateway_ws_url}`
1350
1540
  ];
1541
+ if (res.join_policy) {
1542
+ lines.push(` JoinPolicy : ${res.join_policy}`);
1543
+ }
1544
+ if (opts.parent) lines.push(` Parent : ${opts.parent}`);
1545
+ if (lifecycle !== "persistent") {
1546
+ lines.push(` Lifecycle : ${lifecycle}${opts.task ? ` (task=${opts.task})` : ""}`);
1547
+ }
1351
1548
  output(res, lines.join("\n"));
1352
1549
  } catch (err) {
1353
1550
  handleError(err);
1354
1551
  }
1355
1552
  }
1356
1553
  );
1554
+ cmd.command("promote <subnet_id>").description(
1555
+ "Promote a task_scoped subnet to persistent (ADR-0003). Owner-only; idempotent."
1556
+ ).action(async (subnetId) => {
1557
+ const config = loadConfig();
1558
+ if (!config.api_key) {
1559
+ console.error("No API key found. Run `acn join` first.");
1560
+ process.exit(1);
1561
+ }
1562
+ try {
1563
+ const res = await acnPost(`/subnets/${subnetId}/promote`);
1564
+ const lifecycle = res.lifecycle ?? "persistent";
1565
+ output(
1566
+ res,
1567
+ `Subnet ${res.subnet_id} lifecycle=${lifecycle}` + (res.linked_task_id ? ` (still linked to task ${res.linked_task_id})` : "")
1568
+ );
1569
+ } catch (err) {
1570
+ handleError(err);
1571
+ }
1572
+ });
1357
1573
  cmd.command("delete <subnet_id>").description("Delete a subnet you own").action(async (subnetId) => {
1358
1574
  const config = loadConfig();
1359
1575
  if (!config.api_key) {
@@ -1369,7 +1585,353 @@ ${JSON.stringify(res.agents, null, 2)}`);
1369
1585
  handleError(err);
1370
1586
  }
1371
1587
  });
1372
- const harness = new import_commander10.Command("harness").description("Manage Org Harness webhook for a subnet");
1588
+ const requests = new import_commander11.Command("requests").description(
1589
+ "Manage join-requests for a subnet (ADR-0004)"
1590
+ );
1591
+ requests.command("list <subnet_id>").description(
1592
+ "Owner-only: list join_request / allowlist_auto rows for a subnet. Use --kind to switch between channels; --status to filter state."
1593
+ ).option(
1594
+ "--status <status>",
1595
+ "Filter by status: pending | approved | rejected | withdrawn"
1596
+ ).option(
1597
+ "--kind <kind>",
1598
+ "Filter by kind: 'join_request' (default) or 'allowlist_auto'. 'invitation' is rejected here \u2014 use `subnet invitations list`.",
1599
+ "join_request"
1600
+ ).option("--limit <n>", "Page size (1-500, default 100)", "100").option("--offset <n>", "Page offset (default 0)", "0").action(
1601
+ async (subnetId, opts) => {
1602
+ requireApiKey();
1603
+ const params = {};
1604
+ if (opts.status) params.status = opts.status;
1605
+ if (opts.kind) params.kind = opts.kind;
1606
+ if (opts.limit) params.limit = opts.limit;
1607
+ if (opts.offset) params.offset = opts.offset;
1608
+ const qs = new URLSearchParams(params).toString();
1609
+ try {
1610
+ const res = await acnGet(
1611
+ `/subnets/${subnetId}/join-requests${qs ? `?${qs}` : ""}`
1612
+ );
1613
+ const items = res.items ?? [];
1614
+ if (items.length === 0) {
1615
+ output(res, `No join requests on subnet ${subnetId}.`);
1616
+ return;
1617
+ }
1618
+ output(
1619
+ res,
1620
+ `${items.length} request(s) on subnet ${subnetId}:
1621
+
1622
+ ` + items.map((r, i) => formatJoinRequest(r, i)).join("\n\n")
1623
+ );
1624
+ } catch (err) {
1625
+ handleError(err);
1626
+ }
1627
+ }
1628
+ );
1629
+ requests.command("pending").description(
1630
+ "Owner-side convenience: list pending join_requests across every subnet you own. Client-side aggregation \u2014 issues N+1 calls (one per owned subnet)."
1631
+ ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (opts) => {
1632
+ const agentId = opts.agentId ?? requireAgentId4();
1633
+ try {
1634
+ const subs = await acnGet(`/agents/${agentId}/subnets`);
1635
+ const subnetIds = subs.subnets ?? [];
1636
+ const aggregated = [];
1637
+ for (const sid of subnetIds) {
1638
+ try {
1639
+ const res = await acnGet(
1640
+ `/subnets/${sid}/join-requests?status=pending&kind=join_request`
1641
+ );
1642
+ for (const r of res.items ?? []) {
1643
+ aggregated.push({ subnet_id: sid, request: r });
1644
+ }
1645
+ } catch {
1646
+ continue;
1647
+ }
1648
+ }
1649
+ if (aggregated.length === 0) {
1650
+ output(
1651
+ { agent_id: agentId, pending: [] },
1652
+ "No pending join requests across your subnets."
1653
+ );
1654
+ return;
1655
+ }
1656
+ output(
1657
+ { agent_id: agentId, pending: aggregated },
1658
+ `${aggregated.length} pending request(s) across ${subnetIds.length} subnet(s):
1659
+
1660
+ ` + aggregated.map((a, i) => formatJoinRequest(a.request, i)).join("\n\n")
1661
+ );
1662
+ } catch (err) {
1663
+ handleError(err);
1664
+ }
1665
+ });
1666
+ requests.command("approve <subnet_id>").description("Owner-only: approve a pending join_request (CAS pending \u2192 approved).").requiredOption("--request-id <rid>", "Join request ID to approve").option("--note <text>", "Optional audit note (\u2264500 chars)").action(
1667
+ async (subnetId, opts) => {
1668
+ requireApiKey();
1669
+ const body = {};
1670
+ if (opts.note !== void 0) body.note = opts.note;
1671
+ try {
1672
+ const res = await acnPost(
1673
+ `/subnets/${subnetId}/join-requests/${opts.requestId}/approve`,
1674
+ body
1675
+ );
1676
+ output(
1677
+ res,
1678
+ `approved request ${res.request_id} (agent ${res.agent_id}) on subnet ${subnetId}`
1679
+ );
1680
+ } catch (err) {
1681
+ handleError(err);
1682
+ }
1683
+ }
1684
+ );
1685
+ requests.command("reject <subnet_id>").description("Owner-only: reject a pending join_request (CAS pending \u2192 rejected).").requiredOption("--request-id <rid>", "Join request ID to reject").option("--note <text>", "Optional audit note (\u2264500 chars)").action(
1686
+ async (subnetId, opts) => {
1687
+ requireApiKey();
1688
+ const body = {};
1689
+ if (opts.note !== void 0) body.note = opts.note;
1690
+ try {
1691
+ const res = await acnPost(
1692
+ `/subnets/${subnetId}/join-requests/${opts.requestId}/reject`,
1693
+ body
1694
+ );
1695
+ output(
1696
+ res,
1697
+ `rejected request ${res.request_id} (agent ${res.agent_id}) on subnet ${subnetId}`
1698
+ );
1699
+ } catch (err) {
1700
+ handleError(err);
1701
+ }
1702
+ }
1703
+ );
1704
+ requests.command("withdraw <subnet_id>").description(
1705
+ "Applicant-only: withdraw your own pending join_request (CAS pending \u2192 withdrawn)."
1706
+ ).requiredOption("--request-id <rid>", "Your join request ID").option("--note <text>", "Optional audit note (\u2264500 chars)").action(
1707
+ async (subnetId, opts) => {
1708
+ requireApiKey();
1709
+ const body = {};
1710
+ if (opts.note !== void 0) body.note = opts.note;
1711
+ try {
1712
+ const res = await acnDelete(
1713
+ `/subnets/${subnetId}/join-requests/${opts.requestId}`
1714
+ );
1715
+ if (opts.note !== void 0) {
1716
+ console.error(
1717
+ "[warn] --note is not sent on withdraw (CLI limitation; use the API directly if you need to record a reason)."
1718
+ );
1719
+ }
1720
+ output(
1721
+ res,
1722
+ `withdrew request ${res.request_id} on subnet ${subnetId}`
1723
+ );
1724
+ } catch (err) {
1725
+ handleError(err);
1726
+ }
1727
+ }
1728
+ );
1729
+ cmd.addCommand(requests);
1730
+ const invitations = new import_commander11.Command("invitations").description(
1731
+ "Manage invitations on a subnet (ADR-0004)"
1732
+ );
1733
+ invitations.command("send <subnet_id>").description(
1734
+ "Owner-only: invite an agent to a subnet. Auto-merges with a target's pending join_request (collapses to auto-approval)."
1735
+ ).requiredOption("--agent-id <aid>", "Agent ID to invite").option("--note <text>", "Optional audit note (\u2264500 chars)").action(
1736
+ async (subnetId, opts) => {
1737
+ requireApiKey();
1738
+ const body = { agent_id: opts.agentId };
1739
+ if (opts.note !== void 0) body.note = opts.note;
1740
+ try {
1741
+ const res = await acnPost(
1742
+ `/subnets/${subnetId}/invitations`,
1743
+ body
1744
+ );
1745
+ output(res, formatInviteResponse(res, subnetId, opts.agentId));
1746
+ } catch (err) {
1747
+ handleError(err);
1748
+ }
1749
+ }
1750
+ );
1751
+ invitations.command("list <subnet_id>").description("Owner-only: list invitation rows for a subnet.").option(
1752
+ "--status <status>",
1753
+ "Filter by status: pending | approved | rejected | withdrawn"
1754
+ ).option("--limit <n>", "Page size (1-500, default 100)", "100").option("--offset <n>", "Page offset (default 0)", "0").action(
1755
+ async (subnetId, opts) => {
1756
+ requireApiKey();
1757
+ const params = {};
1758
+ if (opts.status) params.status = opts.status;
1759
+ if (opts.limit) params.limit = opts.limit;
1760
+ if (opts.offset) params.offset = opts.offset;
1761
+ const qs = new URLSearchParams(params).toString();
1762
+ try {
1763
+ const res = await acnGet(
1764
+ `/subnets/${subnetId}/invitations${qs ? `?${qs}` : ""}`
1765
+ );
1766
+ const items = res.items ?? [];
1767
+ if (items.length === 0) {
1768
+ output(res, `No invitations on subnet ${subnetId}.`);
1769
+ return;
1770
+ }
1771
+ output(
1772
+ res,
1773
+ `${items.length} invitation(s) on subnet ${subnetId}:
1774
+
1775
+ ` + items.map((r, i) => formatJoinRequest(r, i)).join("\n\n")
1776
+ );
1777
+ } catch (err) {
1778
+ handleError(err);
1779
+ }
1780
+ }
1781
+ );
1782
+ invitations.command("pending").description(
1783
+ "Invitee view: list pending invitations addressed to you across all subnets (backed by GET /agents/{aid}/subnet-invitations)."
1784
+ ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (opts) => {
1785
+ const agentId = opts.agentId ?? requireAgentId4();
1786
+ try {
1787
+ const res = await acnGet(
1788
+ `/agents/${agentId}/subnet-invitations`
1789
+ );
1790
+ const items = res.items ?? [];
1791
+ if (items.length === 0) {
1792
+ output(res, `No pending invitations for ${agentId}.`);
1793
+ return;
1794
+ }
1795
+ output(
1796
+ res,
1797
+ `${items.length} pending invitation(s) for ${agentId}:
1798
+
1799
+ ` + items.map((r, i) => formatJoinRequest(r, i)).join("\n\n")
1800
+ );
1801
+ } catch (err) {
1802
+ handleError(err);
1803
+ }
1804
+ });
1805
+ invitations.command("accept <subnet_id>").description(
1806
+ "Invitee-only: accept a pending invitation (CAS pending \u2192 approved). Side effect: you join the subnet."
1807
+ ).requiredOption("--invitation-id <iid>", "Invitation ID to accept").option("--note <text>", "Optional audit note (\u2264500 chars)").action(
1808
+ async (subnetId, opts) => {
1809
+ requireApiKey();
1810
+ const body = {};
1811
+ if (opts.note !== void 0) body.note = opts.note;
1812
+ try {
1813
+ const res = await acnPost(
1814
+ `/subnets/${subnetId}/invitations/${opts.invitationId}/accept`,
1815
+ body
1816
+ );
1817
+ output(
1818
+ res,
1819
+ `accepted invitation ${res.request_id} \u2014 joined subnet ${subnetId}`
1820
+ );
1821
+ } catch (err) {
1822
+ handleError(err);
1823
+ }
1824
+ }
1825
+ );
1826
+ invitations.command("reject <subnet_id>").description(
1827
+ "Invitee-only: reject a pending invitation (CAS pending \u2192 rejected). No membership change."
1828
+ ).requiredOption("--invitation-id <iid>", "Invitation ID to reject").option("--note <text>", "Optional audit note (\u2264500 chars)").action(
1829
+ async (subnetId, opts) => {
1830
+ requireApiKey();
1831
+ const body = {};
1832
+ if (opts.note !== void 0) body.note = opts.note;
1833
+ try {
1834
+ const res = await acnPost(
1835
+ `/subnets/${subnetId}/invitations/${opts.invitationId}/reject`,
1836
+ body
1837
+ );
1838
+ output(
1839
+ res,
1840
+ `rejected invitation ${res.request_id} on subnet ${subnetId}`
1841
+ );
1842
+ } catch (err) {
1843
+ handleError(err);
1844
+ }
1845
+ }
1846
+ );
1847
+ invitations.command("cancel <subnet_id>").description(
1848
+ "Owner-only: cancel a pending invitation (CAS pending \u2192 withdrawn)."
1849
+ ).requiredOption("--invitation-id <iid>", "Invitation ID to cancel").action(
1850
+ async (subnetId, opts) => {
1851
+ requireApiKey();
1852
+ try {
1853
+ const res = await acnDelete(
1854
+ `/subnets/${subnetId}/invitations/${opts.invitationId}`
1855
+ );
1856
+ output(
1857
+ res,
1858
+ `cancelled invitation ${res.request_id} on subnet ${subnetId}`
1859
+ );
1860
+ } catch (err) {
1861
+ handleError(err);
1862
+ }
1863
+ }
1864
+ );
1865
+ cmd.addCommand(invitations);
1866
+ const allowlist = new import_commander11.Command("allowlist").description(
1867
+ "Manage a subnet allowlist (ADR-0004)"
1868
+ );
1869
+ 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(
1870
+ async (subnetId, opts) => {
1871
+ requireApiKey();
1872
+ const params = {};
1873
+ if (opts.limit) params.limit = opts.limit;
1874
+ if (opts.offset) params.offset = opts.offset;
1875
+ const qs = new URLSearchParams(params).toString();
1876
+ try {
1877
+ const res = await acnGet(
1878
+ `/subnets/${subnetId}/allowlist${qs ? `?${qs}` : ""}`
1879
+ );
1880
+ const entries = res.entries ?? [];
1881
+ if (entries.length === 0) {
1882
+ output(res, `Allowlist on subnet ${subnetId} is empty.`);
1883
+ return;
1884
+ }
1885
+ output(
1886
+ res,
1887
+ `${entries.length} entry/entries on subnet ${subnetId}:
1888
+
1889
+ ` + entries.map((e, i) => formatAllowlistEntry2(e, i)).join("\n\n")
1890
+ );
1891
+ } catch (err) {
1892
+ handleError(err);
1893
+ }
1894
+ }
1895
+ );
1896
+ allowlist.command("add <subnet_id>").description(
1897
+ "Owner-only: pre-authorise an agent on the subnet allowlist. 409 ALREADY_ON_ALLOWLIST on duplicate."
1898
+ ).requiredOption("--agent-id <aid>", "Agent ID to add").action(
1899
+ async (subnetId, opts) => {
1900
+ requireApiKey();
1901
+ try {
1902
+ const res = await acnPost(
1903
+ `/subnets/${subnetId}/allowlist`,
1904
+ { agent_id: opts.agentId }
1905
+ );
1906
+ output(
1907
+ res,
1908
+ `added ${res.agent_id} to allowlist of subnet ${subnetId} (by ${res.added_by})`
1909
+ );
1910
+ } catch (err) {
1911
+ handleError(err);
1912
+ }
1913
+ }
1914
+ );
1915
+ allowlist.command("remove <subnet_id>").description(
1916
+ "Owner-only: remove an agent from the subnet allowlist. Idempotent (204 even if missing)."
1917
+ ).requiredOption("--agent-id <aid>", "Agent ID to remove").action(
1918
+ async (subnetId, opts) => {
1919
+ requireApiKey();
1920
+ try {
1921
+ await acnDelete(
1922
+ `/subnets/${subnetId}/allowlist/${opts.agentId}`
1923
+ );
1924
+ output(
1925
+ { subnet_id: subnetId, agent_id: opts.agentId, removed: true },
1926
+ `removed ${opts.agentId} from allowlist of subnet ${subnetId}`
1927
+ );
1928
+ } catch (err) {
1929
+ handleError(err);
1930
+ }
1931
+ }
1932
+ );
1933
+ cmd.addCommand(allowlist);
1934
+ const harness = new import_commander11.Command("harness").description("Manage Org Harness webhook for a subnet");
1373
1935
  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) => {
1374
1936
  const config = loadConfig();
1375
1937
  if (!config.api_key) {
@@ -1412,7 +1974,7 @@ ${JSON.stringify(res.agents, null, 2)}`);
1412
1974
  }
1413
1975
 
1414
1976
  // src/commands/follow.ts
1415
- var import_commander11 = require("commander");
1977
+ var import_commander12 = require("commander");
1416
1978
  function requireAgentId5() {
1417
1979
  const config = loadConfig();
1418
1980
  if (!config.api_key) {
@@ -1435,7 +1997,7 @@ function formatAgent2(a, i) {
1435
1997
  ].join("\n");
1436
1998
  }
1437
1999
  function followCommand() {
1438
- const cmd = new import_commander11.Command("follow").description("Follow/unfollow agents and inspect follow graph");
2000
+ const cmd = new import_commander12.Command("follow").description("Follow/unfollow agents and inspect follow graph");
1439
2001
  cmd.command("add <target_id>").description("Follow another agent").option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (targetId, opts) => {
1440
2002
  const agentId = opts.agentId ?? requireAgentId5();
1441
2003
  try {
@@ -1526,7 +2088,7 @@ function followCommand() {
1526
2088
  }
1527
2089
 
1528
2090
  // src/commands/wallet.ts
1529
- var import_commander12 = require("commander");
2091
+ var import_commander13 = require("commander");
1530
2092
  function requireAgentId6() {
1531
2093
  const config = loadConfig();
1532
2094
  if (!config.api_key) {
@@ -1569,7 +2131,7 @@ async function showWalletInfo(opts) {
1569
2131
  }
1570
2132
  }
1571
2133
  function walletCommand() {
1572
- const cmd = new import_commander12.Command("wallet").description("View and manage agent's wallet & payment info");
2134
+ const cmd = new import_commander13.Command("wallet").description("View and manage agent's wallet & payment info");
1573
2135
  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);
1574
2136
  cmd.command("set-capability").description("Declare which payment methods, networks, and wallets you accept").requiredOption(
1575
2137
  "--methods <csv>",
@@ -1748,7 +2310,7 @@ function walletCommand() {
1748
2310
  }
1749
2311
 
1750
2312
  // src/commands/pay.ts
1751
- var import_commander13 = require("commander");
2313
+ var import_commander14 = require("commander");
1752
2314
  function requireAgentId7() {
1753
2315
  const config = loadConfig();
1754
2316
  if (!config.api_key) {
@@ -1762,8 +2324,8 @@ function requireAgentId7() {
1762
2324
  return config.agent_id;
1763
2325
  }
1764
2326
  function payCommand() {
1765
- const cmd = new import_commander13.Command("pay").description("Manage payment tasks between agents");
1766
- const createCmd = new import_commander13.Command("create").description("Create a payment task to another agent");
2327
+ const cmd = new import_commander14.Command("pay").description("Manage payment tasks between agents");
2328
+ const createCmd = new import_commander14.Command("create").description("Create a payment task to another agent");
1767
2329
  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(
1768
2330
  async (opts) => {
1769
2331
  const fromAgent = requireAgentId7();
@@ -1809,7 +2371,7 @@ function payCommand() {
1809
2371
  }
1810
2372
  }
1811
2373
  );
1812
- const confirmCmd = new import_commander13.Command("confirm").description(
2374
+ const confirmCmd = new import_commander14.Command("confirm").description(
1813
2375
  "Confirm an external payment has been made (buyer only)"
1814
2376
  );
1815
2377
  confirmCmd.requiredOption("--task-id <id>", "Payment task ID to confirm").requiredOption(
@@ -1831,7 +2393,7 @@ function payCommand() {
1831
2393
  handleError(err);
1832
2394
  }
1833
2395
  });
1834
- const statusCmd = new import_commander13.Command("status").description(
2396
+ const statusCmd = new import_commander14.Command("status").description(
1835
2397
  "Show payment tasks for the authenticated agent"
1836
2398
  );
1837
2399
  statusCmd.option("--status <s>", "Filter by status (e.g. created, payment_confirmed)").option("--limit <n>", "Max results (default 50)", "50").action(async (opts) => {
@@ -1853,7 +2415,7 @@ function payCommand() {
1853
2415
  }
1854
2416
 
1855
2417
  // src/index.ts
1856
- var program = new import_commander14.Command();
2418
+ var program = new import_commander15.Command();
1857
2419
  program.name("acn").description("ACN CLI \u2014 Agent Collaboration Network command-line interface").version("0.1.0").option("--json", "Output raw JSON (useful for agent parsing)").hook("preAction", (thisCommand) => {
1858
2420
  const opts = thisCommand.opts();
1859
2421
  if (opts.json) setJsonMode(true);
@@ -1861,6 +2423,7 @@ program.name("acn").description("ACN CLI \u2014 Agent Collaboration Network comm
1861
2423
  program.addCommand(configCommand());
1862
2424
  program.addCommand(joinCommand());
1863
2425
  program.addCommand(heartbeatCommand());
2426
+ program.addCommand(rotateKeyCommand());
1864
2427
  program.addCommand(agentsCommand());
1865
2428
  program.addCommand(tasksCommand());
1866
2429
  program.addCommand(messageCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) — zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -14,6 +14,8 @@
14
14
  "build": "tsup",
15
15
  "dev": "tsup --watch",
16
16
  "typecheck": "tsc --noEmit",
17
+ "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
18
+ "test": "vitest run",
17
19
  "prepublishOnly": "npm run build"
18
20
  },
19
21
  "keywords": [
@@ -41,7 +43,8 @@
41
43
  "devDependencies": {
42
44
  "@types/node": "^20.0.0",
43
45
  "tsup": "^8.0.0",
44
- "typescript": "^5.0.0"
46
+ "typescript": "^5.0.0",
47
+ "vitest": "^3.0.5"
45
48
  },
46
49
  "engines": {
47
50
  "node": ">=18.0.0"