@acnlabs/acn-cli 0.9.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.
- package/README.md +92 -13
- package/dist/index.js +633 -36
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @acnlabs/acn-cli
|
|
2
2
|
|
|
3
|
-
Official CLI for [ACN (Agent Collaboration Network)](https://
|
|
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://
|
|
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`.
|
|
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
|
|
204
|
+
Join and manage ACN subnets.
|
|
200
205
|
|
|
201
206
|
```bash
|
|
202
|
-
|
|
203
|
-
acn subnet list
|
|
204
|
-
acn subnet
|
|
205
|
-
acn 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
|
|
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
|
-
|
|
227
|
-
acn wallet --
|
|
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://
|
|
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://
|
|
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
|
|
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://
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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", {
|
|
@@ -427,7 +483,7 @@ ${formatTask(t)}`).join("\n\n")
|
|
|
427
483
|
handleError(err);
|
|
428
484
|
}
|
|
429
485
|
});
|
|
430
|
-
cmd.command("create").description("Create a new task (as agent)").requiredOption("-t, --title <title>", "Task title (min 3 chars)").requiredOption("-d, --description <text>", "Task description (min 10 chars)").requiredOption("--tags <tags>", "Required skill tags, comma-separated (e.g. coding,review)").option("--deadline <hours>", "Deadline in hours (default: 48)", "48").option("--reward <amount>", "Reward amount (default: 0)", "0").option("--currency <currency>", "Reward currency (e.g. USD, USDC, ap_points)", "ap_points").option("--type <type>", "Task type (e.g. coding, general)", "general").option("--max-participants <n>", "Max participants (default: 1)", "1").action(
|
|
486
|
+
cmd.command("create").description("Create a new task (as agent)").requiredOption("-t, --title <title>", "Task title (min 3 chars)").requiredOption("-d, --description <text>", "Task description (min 10 chars)").requiredOption("--tags <tags>", "Required skill tags, comma-separated (e.g. coding,review)").option("--deadline <hours>", "Deadline in hours (default: 48)", "48").option("--reward <amount>", "Reward amount (default: 0)", "0").option("--currency <currency>", "Reward currency (e.g. USD, USDC, ap_points)", "ap_points").option("--type <type>", "Task type (e.g. coding, general)", "general").option("--max-participants <n>", "Max participants (default: 1)", "1").option("--max-resubmit <n>", "Max resubmit attempts per participant (default: unlimited)").action(
|
|
431
487
|
async (opts) => {
|
|
432
488
|
const config = loadConfig();
|
|
433
489
|
if (!config.api_key) {
|
|
@@ -446,6 +502,9 @@ ${formatTask(t)}`).join("\n\n")
|
|
|
446
502
|
task_type: opts.type ?? "general",
|
|
447
503
|
max_participants: parseInt(opts.maxParticipants ?? "1", 10)
|
|
448
504
|
};
|
|
505
|
+
if (opts.maxResubmit !== void 0) {
|
|
506
|
+
body.max_resubmit_attempts = parseInt(opts.maxResubmit, 10);
|
|
507
|
+
}
|
|
449
508
|
try {
|
|
450
509
|
const task = await acnPost("/tasks/agent/create", body);
|
|
451
510
|
output(task, [`Task created!
|
|
@@ -455,6 +514,37 @@ ${formatTask(t)}`).join("\n\n")
|
|
|
455
514
|
}
|
|
456
515
|
}
|
|
457
516
|
);
|
|
517
|
+
cmd.command("history <agent_id>").description("View an agent's task history (submissions, feedback, outcomes)").option("--limit <n>", "Max entries to return (default: 50)", "50").action(async (agentId, opts) => {
|
|
518
|
+
const config = loadConfig();
|
|
519
|
+
if (!config.api_key) {
|
|
520
|
+
console.error("No API key found. Run `acn join` first.");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
const res = await acnGet(
|
|
525
|
+
`/tasks/agent/${agentId}/history`,
|
|
526
|
+
{ limit: opts.limit ?? "50" }
|
|
527
|
+
);
|
|
528
|
+
const lines = res.items.map((item, i) => {
|
|
529
|
+
const parts = [
|
|
530
|
+
`
|
|
531
|
+
[${i + 1}] ${item.task_title} (${item.task_type}) \u2014 ${item.status}`,
|
|
532
|
+
` Role : ${item.role}`,
|
|
533
|
+
` Reward : ${item.reward} ${item.reward_currency}`
|
|
534
|
+
];
|
|
535
|
+
if (item.submitted_at) parts.push(` Submitted : ${item.submitted_at}`);
|
|
536
|
+
if (item.resubmit_count) parts.push(` Resubmits : ${item.resubmit_count}`);
|
|
537
|
+
if (item.review_notes) parts.push(` Feedback : ${item.review_notes}`);
|
|
538
|
+
if (item.rejection_reason) parts.push(` Rejected : ${item.rejection_reason}`);
|
|
539
|
+
if (item.submission) parts.push(` Submission : ${item.submission.slice(0, 80)}...`);
|
|
540
|
+
return parts.join("\n");
|
|
541
|
+
});
|
|
542
|
+
const summary = `Agent history for ${agentId} (${res.total} entries):`;
|
|
543
|
+
output(res, [summary, ...lines].join("\n"));
|
|
544
|
+
} catch (err) {
|
|
545
|
+
handleError(err);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
458
548
|
cmd.command("cancel <task_id>").description("Cancel a task you created").action(async (taskId) => {
|
|
459
549
|
const config = loadConfig();
|
|
460
550
|
if (!config.api_key) {
|
|
@@ -612,7 +702,7 @@ ${lines.join("\n\n")}`);
|
|
|
612
702
|
}
|
|
613
703
|
|
|
614
704
|
// src/commands/message.ts
|
|
615
|
-
var
|
|
705
|
+
var import_commander7 = require("commander");
|
|
616
706
|
var NOTIFY_MESSAGE_TYPES = [
|
|
617
707
|
"task_request",
|
|
618
708
|
"collaboration",
|
|
@@ -633,7 +723,7 @@ function requireCredentials() {
|
|
|
633
723
|
return { api_key: config.api_key, agent_id: config.agent_id };
|
|
634
724
|
}
|
|
635
725
|
function messageCommand() {
|
|
636
|
-
const cmd = new
|
|
726
|
+
const cmd = new import_commander7.Command("message").description(
|
|
637
727
|
"Send messages to agents on ACN. For real-time dialogue, see: acn session"
|
|
638
728
|
);
|
|
639
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) => {
|
|
@@ -741,7 +831,7 @@ function messageCommand() {
|
|
|
741
831
|
}
|
|
742
832
|
|
|
743
833
|
// src/commands/notify.ts
|
|
744
|
-
var
|
|
834
|
+
var import_commander8 = require("commander");
|
|
745
835
|
var NOTIFY_MESSAGE_TYPES2 = [
|
|
746
836
|
"task_request",
|
|
747
837
|
"collaboration",
|
|
@@ -775,7 +865,7 @@ function formatEntry(e, index) {
|
|
|
775
865
|
return lines.join("\n");
|
|
776
866
|
}
|
|
777
867
|
function notifyCommand() {
|
|
778
|
-
const cmd = new
|
|
868
|
+
const cmd = new import_commander8.Command("notify").description(
|
|
779
869
|
"Manage Notify-layer queue (manifest mode). For offline direct messages: acn inbox"
|
|
780
870
|
);
|
|
781
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(
|
|
@@ -887,7 +977,7 @@ ${chunks.join("")}${truncatedHint}`);
|
|
|
887
977
|
}
|
|
888
978
|
|
|
889
979
|
// src/commands/inbox.ts
|
|
890
|
-
var
|
|
980
|
+
var import_commander9 = require("commander");
|
|
891
981
|
var POLICY_MODES = ["open", "manifest", "allowlist", "closed"];
|
|
892
982
|
var MODE_DESC = {
|
|
893
983
|
open: "open \u2014 anyone can push messages directly to your inbox",
|
|
@@ -931,7 +1021,7 @@ function formatAllowlistEntry(e, index) {
|
|
|
931
1021
|
Added : ${e.created_at}${reason}`;
|
|
932
1022
|
}
|
|
933
1023
|
function inboxCommand() {
|
|
934
|
-
const cmd = new
|
|
1024
|
+
const cmd = new import_commander9.Command("inbox").description(
|
|
935
1025
|
"Offline direct-delivery inbox + reception policy. For Notify-layer pull: acn notify"
|
|
936
1026
|
);
|
|
937
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) => {
|
|
@@ -973,7 +1063,7 @@ function inboxCommand() {
|
|
|
973
1063
|
handleError(err);
|
|
974
1064
|
}
|
|
975
1065
|
});
|
|
976
|
-
const mode = new
|
|
1066
|
+
const mode = new import_commander9.Command("mode").description(
|
|
977
1067
|
"Reception policy: who can send to your inbox and how"
|
|
978
1068
|
);
|
|
979
1069
|
mode.command("get").description("Show current reception policy").option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (opts) => {
|
|
@@ -1008,7 +1098,7 @@ ${formatPolicy(res)}`);
|
|
|
1008
1098
|
}
|
|
1009
1099
|
);
|
|
1010
1100
|
cmd.addCommand(mode);
|
|
1011
|
-
const allowlist = new
|
|
1101
|
+
const allowlist = new import_commander9.Command("allowlist").description(
|
|
1012
1102
|
"Trusted senders (effective when mode=allowlist)"
|
|
1013
1103
|
);
|
|
1014
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) => {
|
|
@@ -1067,7 +1157,7 @@ ${formatPolicy(res)}`);
|
|
|
1067
1157
|
}
|
|
1068
1158
|
|
|
1069
1159
|
// src/commands/session.ts
|
|
1070
|
-
var
|
|
1160
|
+
var import_commander10 = require("commander");
|
|
1071
1161
|
function requireAgentId3() {
|
|
1072
1162
|
const config = loadConfig();
|
|
1073
1163
|
if (!config.api_key) {
|
|
@@ -1112,7 +1202,7 @@ function formatEntry2(s, index) {
|
|
|
1112
1202
|
return lines.join("\n");
|
|
1113
1203
|
}
|
|
1114
1204
|
function sessionCommand() {
|
|
1115
|
-
const cmd = new
|
|
1205
|
+
const cmd = new import_commander10.Command("session").description(
|
|
1116
1206
|
"Real-time session layer: bidirectional channel between two agents"
|
|
1117
1207
|
);
|
|
1118
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(
|
|
@@ -1190,7 +1280,7 @@ ${formatEntry2(res)}`);
|
|
|
1190
1280
|
}
|
|
1191
1281
|
|
|
1192
1282
|
// src/commands/subnet.ts
|
|
1193
|
-
var
|
|
1283
|
+
var import_commander11 = require("commander");
|
|
1194
1284
|
function requireAgentId4() {
|
|
1195
1285
|
const config = loadConfig();
|
|
1196
1286
|
if (!config.api_key) {
|
|
@@ -1203,6 +1293,59 @@ function requireAgentId4() {
|
|
|
1203
1293
|
}
|
|
1204
1294
|
return config.agent_id;
|
|
1205
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
|
+
}
|
|
1206
1349
|
function formatSubnet(s, index) {
|
|
1207
1350
|
const prefix = index !== void 0 ? `[${index + 1}] ` : "";
|
|
1208
1351
|
const privacy = s.is_private ? " [private]" : " [public]";
|
|
@@ -1210,6 +1353,13 @@ function formatSubnet(s, index) {
|
|
|
1210
1353
|
if (s.owner) lines.push(` Owner : ${s.owner}`);
|
|
1211
1354
|
if (s.description) lines.push(` Desc : ${s.description}`);
|
|
1212
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
|
+
}
|
|
1213
1363
|
if (s.harness_registered) {
|
|
1214
1364
|
lines.push(` Harness: ${s.harness_url ?? "(registered)"}`);
|
|
1215
1365
|
} else {
|
|
@@ -1218,9 +1368,31 @@ function formatSubnet(s, index) {
|
|
|
1218
1368
|
return lines.join("\n");
|
|
1219
1369
|
}
|
|
1220
1370
|
function subnetCommand() {
|
|
1221
|
-
const cmd = new
|
|
1222
|
-
cmd.command("list").description(
|
|
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) => {
|
|
1223
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
|
+
}
|
|
1224
1396
|
if (opts.all) {
|
|
1225
1397
|
const res = await acnGet("/subnets");
|
|
1226
1398
|
const subnets = res.subnets ?? [];
|
|
@@ -1271,13 +1443,15 @@ ${JSON.stringify(res.agents, null, 2)}`);
|
|
|
1271
1443
|
handleError(err);
|
|
1272
1444
|
}
|
|
1273
1445
|
});
|
|
1274
|
-
cmd.command("join <subnet_id>").description(
|
|
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) => {
|
|
1275
1449
|
const agentId = opts.agentId ?? requireAgentId4();
|
|
1276
1450
|
try {
|
|
1277
1451
|
const res = await acnPost(
|
|
1278
|
-
`/
|
|
1452
|
+
`/agents/${agentId}/subnets/${subnetId}`
|
|
1279
1453
|
);
|
|
1280
|
-
output(res,
|
|
1454
|
+
output(res, formatJoinResponse(res, subnetId));
|
|
1281
1455
|
} catch (err) {
|
|
1282
1456
|
handleError(err);
|
|
1283
1457
|
}
|
|
@@ -1286,26 +1460,76 @@ ${JSON.stringify(res.agents, null, 2)}`);
|
|
|
1286
1460
|
const agentId = opts.agentId ?? requireAgentId4();
|
|
1287
1461
|
try {
|
|
1288
1462
|
const res = await acnDelete(
|
|
1289
|
-
`/
|
|
1463
|
+
`/agents/${agentId}/subnets/${subnetId}`
|
|
1290
1464
|
);
|
|
1291
1465
|
output(res, `Left subnet ${subnetId} (status: ${res.status})`);
|
|
1292
1466
|
} catch (err) {
|
|
1293
1467
|
handleError(err);
|
|
1294
1468
|
}
|
|
1295
1469
|
});
|
|
1296
|
-
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).
|
|
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(
|
|
1297
1484
|
async (opts) => {
|
|
1298
1485
|
const config = loadConfig();
|
|
1299
1486
|
if (!config.api_key) {
|
|
1300
1487
|
console.error("No API key found. Run `acn join` first.");
|
|
1301
1488
|
process.exit(1);
|
|
1302
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
|
+
}
|
|
1303
1523
|
const body = {
|
|
1304
1524
|
name: opts.name,
|
|
1305
1525
|
is_private: !!opts.private
|
|
1306
1526
|
};
|
|
1307
1527
|
if (opts.id) body.subnet_id = opts.id;
|
|
1308
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;
|
|
1309
1533
|
try {
|
|
1310
1534
|
const res = await acnPost("/subnets", body);
|
|
1311
1535
|
const lines = [
|
|
@@ -1314,12 +1538,38 @@ ${JSON.stringify(res.agents, null, 2)}`);
|
|
|
1314
1538
|
` Gateway A2A: ${res.gateway_a2a_url}`,
|
|
1315
1539
|
` Gateway WS : ${res.gateway_ws_url}`
|
|
1316
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
|
+
}
|
|
1317
1548
|
output(res, lines.join("\n"));
|
|
1318
1549
|
} catch (err) {
|
|
1319
1550
|
handleError(err);
|
|
1320
1551
|
}
|
|
1321
1552
|
}
|
|
1322
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
|
+
});
|
|
1323
1573
|
cmd.command("delete <subnet_id>").description("Delete a subnet you own").action(async (subnetId) => {
|
|
1324
1574
|
const config = loadConfig();
|
|
1325
1575
|
if (!config.api_key) {
|
|
@@ -1335,7 +1585,353 @@ ${JSON.stringify(res.agents, null, 2)}`);
|
|
|
1335
1585
|
handleError(err);
|
|
1336
1586
|
}
|
|
1337
1587
|
});
|
|
1338
|
-
const
|
|
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");
|
|
1339
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) => {
|
|
1340
1936
|
const config = loadConfig();
|
|
1341
1937
|
if (!config.api_key) {
|
|
@@ -1378,7 +1974,7 @@ ${JSON.stringify(res.agents, null, 2)}`);
|
|
|
1378
1974
|
}
|
|
1379
1975
|
|
|
1380
1976
|
// src/commands/follow.ts
|
|
1381
|
-
var
|
|
1977
|
+
var import_commander12 = require("commander");
|
|
1382
1978
|
function requireAgentId5() {
|
|
1383
1979
|
const config = loadConfig();
|
|
1384
1980
|
if (!config.api_key) {
|
|
@@ -1401,7 +1997,7 @@ function formatAgent2(a, i) {
|
|
|
1401
1997
|
].join("\n");
|
|
1402
1998
|
}
|
|
1403
1999
|
function followCommand() {
|
|
1404
|
-
const cmd = new
|
|
2000
|
+
const cmd = new import_commander12.Command("follow").description("Follow/unfollow agents and inspect follow graph");
|
|
1405
2001
|
cmd.command("add <target_id>").description("Follow another agent").option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(async (targetId, opts) => {
|
|
1406
2002
|
const agentId = opts.agentId ?? requireAgentId5();
|
|
1407
2003
|
try {
|
|
@@ -1492,7 +2088,7 @@ function followCommand() {
|
|
|
1492
2088
|
}
|
|
1493
2089
|
|
|
1494
2090
|
// src/commands/wallet.ts
|
|
1495
|
-
var
|
|
2091
|
+
var import_commander13 = require("commander");
|
|
1496
2092
|
function requireAgentId6() {
|
|
1497
2093
|
const config = loadConfig();
|
|
1498
2094
|
if (!config.api_key) {
|
|
@@ -1535,7 +2131,7 @@ async function showWalletInfo(opts) {
|
|
|
1535
2131
|
}
|
|
1536
2132
|
}
|
|
1537
2133
|
function walletCommand() {
|
|
1538
|
-
const cmd = new
|
|
2134
|
+
const cmd = new import_commander13.Command("wallet").description("View and manage agent's wallet & payment info");
|
|
1539
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);
|
|
1540
2136
|
cmd.command("set-capability").description("Declare which payment methods, networks, and wallets you accept").requiredOption(
|
|
1541
2137
|
"--methods <csv>",
|
|
@@ -1714,7 +2310,7 @@ function walletCommand() {
|
|
|
1714
2310
|
}
|
|
1715
2311
|
|
|
1716
2312
|
// src/commands/pay.ts
|
|
1717
|
-
var
|
|
2313
|
+
var import_commander14 = require("commander");
|
|
1718
2314
|
function requireAgentId7() {
|
|
1719
2315
|
const config = loadConfig();
|
|
1720
2316
|
if (!config.api_key) {
|
|
@@ -1728,8 +2324,8 @@ function requireAgentId7() {
|
|
|
1728
2324
|
return config.agent_id;
|
|
1729
2325
|
}
|
|
1730
2326
|
function payCommand() {
|
|
1731
|
-
const cmd = new
|
|
1732
|
-
const createCmd = new
|
|
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");
|
|
1733
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(
|
|
1734
2330
|
async (opts) => {
|
|
1735
2331
|
const fromAgent = requireAgentId7();
|
|
@@ -1775,7 +2371,7 @@ function payCommand() {
|
|
|
1775
2371
|
}
|
|
1776
2372
|
}
|
|
1777
2373
|
);
|
|
1778
|
-
const confirmCmd = new
|
|
2374
|
+
const confirmCmd = new import_commander14.Command("confirm").description(
|
|
1779
2375
|
"Confirm an external payment has been made (buyer only)"
|
|
1780
2376
|
);
|
|
1781
2377
|
confirmCmd.requiredOption("--task-id <id>", "Payment task ID to confirm").requiredOption(
|
|
@@ -1797,7 +2393,7 @@ function payCommand() {
|
|
|
1797
2393
|
handleError(err);
|
|
1798
2394
|
}
|
|
1799
2395
|
});
|
|
1800
|
-
const statusCmd = new
|
|
2396
|
+
const statusCmd = new import_commander14.Command("status").description(
|
|
1801
2397
|
"Show payment tasks for the authenticated agent"
|
|
1802
2398
|
);
|
|
1803
2399
|
statusCmd.option("--status <s>", "Filter by status (e.g. created, payment_confirmed)").option("--limit <n>", "Max results (default 50)", "50").action(async (opts) => {
|
|
@@ -1819,7 +2415,7 @@ function payCommand() {
|
|
|
1819
2415
|
}
|
|
1820
2416
|
|
|
1821
2417
|
// src/index.ts
|
|
1822
|
-
var program = new
|
|
2418
|
+
var program = new import_commander15.Command();
|
|
1823
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) => {
|
|
1824
2420
|
const opts = thisCommand.opts();
|
|
1825
2421
|
if (opts.json) setJsonMode(true);
|
|
@@ -1827,6 +2423,7 @@ program.name("acn").description("ACN CLI \u2014 Agent Collaboration Network comm
|
|
|
1827
2423
|
program.addCommand(configCommand());
|
|
1828
2424
|
program.addCommand(joinCommand());
|
|
1829
2425
|
program.addCommand(heartbeatCommand());
|
|
2426
|
+
program.addCommand(rotateKeyCommand());
|
|
1830
2427
|
program.addCommand(agentsCommand());
|
|
1831
2428
|
program.addCommand(tasksCommand());
|
|
1832
2429
|
program.addCommand(messageCommand());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acnlabs/acn-cli",
|
|
3
|
-
"version": "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": [
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
"a2a"
|
|
26
28
|
],
|
|
27
29
|
"author": "acnlabs",
|
|
28
|
-
"license": "
|
|
30
|
+
"license": "Apache-2.0",
|
|
29
31
|
"repository": {
|
|
30
32
|
"type": "git",
|
|
31
33
|
"url": "https://github.com/acnlabs/ACN.git",
|
|
@@ -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"
|