@agent-play/cli 3.0.1 → 3.1.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 CHANGED
@@ -1,11 +1,13 @@
1
1
  # @agent-play/cli
2
2
 
3
- Command-line tool for **Agent Play**: sign in to the web app, create account API keys, and register agents (up to two per account) when the server uses a Redis-backed repository.
3
+ Command-line tool for **Agent Play**: create a **main developer node** (platform signup), add **agent nodes**, **validate** those identities against the server, and manage agent registrations. The server must use a **Redis**-backed agent repository (`REDIS_URL` on the server).
4
+
5
+ Authentication uses **`x-node-id`** and **`x-node-passw`** on every request **except** main-node creation, which sends hashed passphrase material in the JSON body to **`POST /api/nodes**.
4
6
 
5
7
  ## Documentation
6
8
 
7
9
  - **[Repository](https://github.com/wilforlan/agent-play)**
8
- - **[CLI guide](https://github.com/wilforlan/agent-play/blob/main/docs/cli.md)** — `login`, `create-key`, `create`, `delete`
10
+ - **[CLI guide](https://github.com/wilforlan/agent-play/blob/main/docs/cli.md)** — full **Node setup** and **Node validation** sections
9
11
  - **[API reference](https://wilforlan.github.io/agent-play/)** — TypeDoc
10
12
 
11
13
  ## Install
@@ -15,3 +17,62 @@ npm install -g @agent-play/cli
15
17
  ```
16
18
 
17
19
  Binary name: **`agent-play`**.
20
+
21
+ Default server URL: **`http://127.0.0.1:3000`**, or override with **`AGENT_PLAY_SERVER_URL`**.
22
+
23
+ Root key for derivation: **`--root-file`**, **`AGENT_PLAY_ROOT_FILE_PATH`**, or a **`.root`** file under **`~/.agent-play/`** or the current working directory (must match the server’s `.root`).
24
+
25
+ ## Node setup
26
+
27
+ 1. **Server:** Redis (`REDIS_URL`) and a deployed web UI/API the CLI can reach (`AGENT_PLAY_SERVER_URL`).
28
+ 2. **Local `.root`:** Must match the server genesis root key (see resolution order above).
29
+ 3. **`create-main-node`** (`bootstrap-node`): prompts for server URL, generates a passphrase, registers **`POST /api/nodes`**, writes **`~/.agent-play/credentials.json`** with **`serverUrl`**, **`nodeId`**, and the human passphrase.
30
+ 4. **`create-agent-node`**: derives an agent node under your main node, **`POST /api/nodes/agent-node`**, appends to **`credentials.json` → `agentNodes`**.
31
+ 5. **`inspect-node`**, **`list-agent-nodes`**, **`delete-*`**, **`clear-node-credentials`**: inspect or tear down registrations; see **`docs/cli.md`** for the full table.
32
+
33
+ > **@deprecated** **`POST /api/agents`** does not create agent **node** identity. Use **`POST /api/nodes/agent-node`**, then attach runtime data with **`world.addPlayer`**.
34
+
35
+ ## Node validation
36
+
37
+ - **`validate-main-node`** — calls **`POST /api/nodes/validate`** for your main node id (uses **`credentials.json`** + **`.root`**).
38
+ - **`validate-agent-node --all`** — validates every id in **`credentials.json` → `agentNodes`** (includes **`mainNodeId`** in the validate body).
39
+ - **`validate-agent-node --agent-node-ids id1,id2`** — same for explicit ids.
40
+
41
+ For Redis-direct checks (ops/CI), use **`node-tools`** script **`scripts/validate-node-derivative.mjs`**; details in **`docs/cli.md`** and **`docs/notes/node-id-v1-migration.md`**.
42
+
43
+ ## Commands
44
+
45
+ | Command | Aliases | What it does |
46
+ |--------|---------|----------------|
47
+ | **`create-main-node`** | `bootstrap-node` | Sign up a **main** node: **`POST /api/nodes`** (no node headers), save **`~/.agent-play/credentials.json`**. Optional **`--root-file`**. |
48
+ | **`inspect-node`** | — | **GET /api/nodes** — genesis id, main node, **agent node ids** (`create-agent-node`), and **runtime** agent rows (SDK metadata) if present. |
49
+ | **`create-agent-node`** | `create` | **POST /api/nodes/agent-node** — new agent node under your main node. |
50
+ | **`list-agent-nodes`** | `list` | **GET /api/agents** — lists registered agents. |
51
+ | **`delete-agent-node`** | `delete`, `remove` | **DELETE /api/agents** — optional **`[agent-id]`**; if omitted, prompts. |
52
+ | **`delete-main-node`** | — | **DELETE /api/nodes** — confirm by typing main node id; cascades. |
53
+ | **`validate-main-node`** | — | **POST /api/nodes/validate** for main node id. |
54
+ | **`validate-agent-node`** | — | **`--all`** or **`--agent-node-ids id1,id2,...`** — validate agent node ids. |
55
+ | **`clear-node-credentials`** | — | Removes **`~/.agent-play/credentials.json`**. |
56
+
57
+ ## Genesis and main node
58
+
59
+ Every **main node id** is derived from passphrase material and the platform **root key** (the **genesis** identity). **`inspect-node`** and **`create-main-node`** output should agree with **`.root`** when both sides use the same key.
60
+
61
+ Node kinds: **`root` → `main` → `agent`**. Root has no passphrase; main and agent persist hashed material server-side.
62
+
63
+ ## Usage examples
64
+
65
+ ```bash
66
+ npx agent-play create-main-node
67
+ npx agent-play validate-main-node
68
+ npx agent-play inspect-node
69
+ npx agent-play create-agent-node
70
+ npx agent-play validate-agent-node --all
71
+ npx agent-play list-agent-nodes
72
+ npx agent-play delete-agent-node
73
+ npx agent-play delete-agent-node <agent-uuid>
74
+ npx agent-play delete-main-node
75
+ npx agent-play clear-node-credentials
76
+ ```
77
+
78
+ For SDK usage after bootstrap, use **`RemotePlayWorld`** and register players with **`mainNodeId`** and **`agentId`** from the CLI output.
package/dist/.root ADDED
@@ -0,0 +1 @@
1
+ 87b6637b010478e48a83a8d445041ae4df5d607df7932153cdfee5c601e8e39e
package/dist/cli.js CHANGED
@@ -1,134 +1,227 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { mkdir, readFile, unlink, writeFile } from "fs/promises";
4
+ import { existsSync } from "fs";
5
+ import { mkdir, unlink, writeFile } from "fs/promises";
5
6
  import { homedir } from "os";
6
- import { join } from "path";
7
+ import { join, resolve } from "path";
7
8
  import { createInterface } from "readline/promises";
8
9
  import { stdin as input, stdout as output } from "process";
10
+ import {
11
+ createNodeCredentialFromPassw,
12
+ deriveNodeIdFromPassword,
13
+ generateNodePassw,
14
+ hashNodePassword,
15
+ loadAgentPlayCredentialsFileFromPath,
16
+ loadRootKey
17
+ } from "@agent-play/node-tools";
18
+ function nodeAuthHeaders(cred) {
19
+ return {
20
+ "x-node-id": cred.nodeId,
21
+ "x-node-passw": hashNodePassword(cred.passw)
22
+ };
23
+ }
24
+ function parseAgentRows(agentsRaw) {
25
+ if (!Array.isArray(agentsRaw)) {
26
+ return [];
27
+ }
28
+ const agents = [];
29
+ for (const a of agentsRaw) {
30
+ if (typeof a !== "object" || a === null) continue;
31
+ const o = a;
32
+ if (typeof o.agentId === "string" && typeof o.name === "string") {
33
+ agents.push({ agentId: o.agentId, name: o.name });
34
+ }
35
+ }
36
+ return agents;
37
+ }
9
38
  function credentialsPath() {
10
39
  return join(homedir(), ".agent-play", "credentials.json");
11
40
  }
12
41
  async function loadCredentials() {
13
- try {
14
- const raw = await readFile(credentialsPath(), "utf8");
15
- const json = JSON.parse(raw);
16
- if (typeof json !== "object" || json === null) return null;
17
- const o = json;
18
- if (typeof o.serverUrl !== "string" || typeof o.token !== "string") {
19
- return null;
20
- }
21
- return { serverUrl: o.serverUrl.replace(/\/$/, ""), token: o.token };
22
- } catch {
23
- return null;
24
- }
42
+ return loadAgentPlayCredentialsFileFromPath(credentialsPath());
25
43
  }
26
44
  async function saveCredentials(c) {
27
45
  const dir = join(homedir(), ".agent-play");
28
46
  await mkdir(dir, { recursive: true });
29
47
  await writeFile(
30
48
  credentialsPath(),
31
- JSON.stringify({ serverUrl: c.serverUrl, token: c.token }, null, 2),
49
+ JSON.stringify(c, null, 2),
32
50
  "utf8"
33
51
  );
34
52
  }
35
- function defaultServerUrl() {
36
- return process.env.AGENT_PLAY_SERVER_URL ?? "http://127.0.0.1:3000";
37
- }
38
- async function cmdLogin() {
39
- const rl = createInterface({ input, output });
40
- const serverUrl = ((await rl.question(
41
- `Server URL [${defaultServerUrl()}]: `
42
- )).trim() || defaultServerUrl()).replace(/\/$/, "");
43
- const email = (await rl.question("Email: ")).trim();
44
- if (email.length === 0) {
45
- rl.close();
46
- console.error("Email is required.");
47
- process.exitCode = 1;
48
- return;
53
+ var BOOTSTRAP_ENVIRONMENTS = [
54
+ { id: "local-server", url: "http://127.0.0.1:3000" },
55
+ { id: "test-server", url: "https://test-agent-play.com" },
56
+ { id: "main-server", url: "https://agent-play.com" }
57
+ ];
58
+ function parseBootstrapEnvironmentAnswer(raw) {
59
+ const t = raw.trim().toLowerCase();
60
+ if (t === "" || t === "1") {
61
+ return BOOTSTRAP_ENVIRONMENTS[0].url;
49
62
  }
50
- const lookupRes = await fetch(`${serverUrl}/api/auth/lookup`, {
51
- method: "POST",
52
- headers: { "content-type": "application/json" },
53
- body: JSON.stringify({ email })
54
- });
55
- const lookupText = await lookupRes.text();
56
- if (!lookupRes.ok) {
57
- rl.close();
58
- console.error(`Lookup failed (${lookupRes.status}): ${lookupText}`);
59
- process.exitCode = 1;
60
- return;
63
+ if (t === "2") {
64
+ return BOOTSTRAP_ENVIRONMENTS[1].url;
61
65
  }
62
- let lookupJson;
63
- try {
64
- lookupJson = JSON.parse(lookupText);
65
- } catch {
66
- rl.close();
67
- console.error("Invalid JSON from server.");
68
- process.exitCode = 1;
69
- return;
66
+ if (t === "3") {
67
+ return BOOTSTRAP_ENVIRONMENTS[2].url;
70
68
  }
71
- const exists = typeof lookupJson === "object" && lookupJson !== null && lookupJson.exists === true;
72
- let token;
73
- if (exists) {
74
- const password = (await rl.question("Password: ")).trim();
75
- rl.close();
76
- const loginRes = await fetch(`${serverUrl}/api/auth/login`, {
77
- method: "POST",
78
- headers: { "content-type": "application/json" },
79
- body: JSON.stringify({ email, password })
80
- });
81
- const loginText = await loginRes.text();
82
- if (!loginRes.ok) {
83
- console.error(`Login failed (${loginRes.status}): ${loginText}`);
84
- process.exitCode = 1;
85
- return;
69
+ for (const e of BOOTSTRAP_ENVIRONMENTS) {
70
+ if (t === e.id) {
71
+ return e.url;
86
72
  }
87
- const loginJson = JSON.parse(loginText);
88
- if (typeof loginJson.token !== "string") {
89
- console.error("Missing token in response.");
90
- process.exitCode = 1;
91
- return;
73
+ }
74
+ if (t === "local") {
75
+ return BOOTSTRAP_ENVIRONMENTS[0].url;
76
+ }
77
+ if (t === "test") {
78
+ return BOOTSTRAP_ENVIRONMENTS[1].url;
79
+ }
80
+ if (t === "main") {
81
+ return BOOTSTRAP_ENVIRONMENTS[2].url;
82
+ }
83
+ return null;
84
+ }
85
+ async function promptBootstrapEnvironment(rl) {
86
+ const lines = [
87
+ "Choose environment (sets server URL):",
88
+ ` 1) ${BOOTSTRAP_ENVIRONMENTS[0].id} \u2192 ${BOOTSTRAP_ENVIRONMENTS[0].url}`,
89
+ ` 2) ${BOOTSTRAP_ENVIRONMENTS[1].id} \u2192 ${BOOTSTRAP_ENVIRONMENTS[1].url}`,
90
+ ` 3) ${BOOTSTRAP_ENVIRONMENTS[2].id} \u2192 ${BOOTSTRAP_ENVIRONMENTS[2].url}`,
91
+ "Enter 1\u20133, or local-server / test-server / main-server [1]: "
92
+ ].join("\n");
93
+ for (; ; ) {
94
+ const answer = await rl.question(lines);
95
+ const url = parseBootstrapEnvironmentAnswer(answer);
96
+ if (url !== null) {
97
+ return url.replace(/\/$/, "");
92
98
  }
93
- token = loginJson.token;
94
- } else {
95
- const name = (await rl.question("Your name: ")).trim() || "User";
96
- const password = (await rl.question("Choose a password (min 8 chars): ")).trim();
97
- if (password.length < 8) {
98
- rl.close();
99
- console.error("Password must be at least 8 characters.");
100
- process.exitCode = 1;
101
- return;
99
+ console.log(
100
+ "Invalid choice. Enter 1, 2, or 3, or one of: local-server, test-server, main-server."
101
+ );
102
+ }
103
+ }
104
+ function parseBootstrapNodeArgs(argv) {
105
+ const out = {};
106
+ for (let i = 0; i < argv.length; i++) {
107
+ const a = argv[i];
108
+ if (a === "--root-file" && typeof argv[i + 1] === "string") {
109
+ out.rootFilePath = argv[++i];
102
110
  }
103
- rl.close();
104
- const regRes = await fetch(`${serverUrl}/api/auth/register`, {
105
- method: "POST",
106
- headers: { "content-type": "application/json" },
107
- body: JSON.stringify({ email, name, password })
108
- });
109
- const regText = await regRes.text();
110
- if (!regRes.ok) {
111
- console.error(`Sign up failed (${regRes.status}): ${regText}`);
112
- process.exitCode = 1;
113
- return;
111
+ }
112
+ return out;
113
+ }
114
+ function parseValidateAgentNodeArgs(argv) {
115
+ let wantsAll = false;
116
+ let ids = [];
117
+ for (let i = 0; i < argv.length; i++) {
118
+ const a = argv[i];
119
+ if (a === "--all") {
120
+ wantsAll = true;
121
+ continue;
114
122
  }
115
- const regJson = JSON.parse(regText);
116
- if (typeof regJson.token !== "string") {
117
- console.error("Missing token in response.");
118
- process.exitCode = 1;
119
- return;
123
+ if (a === "--agent-node-ids" && typeof argv[i + 1] === "string") {
124
+ const raw = argv[++i].trim();
125
+ ids = raw.split(",").map((x) => x.trim()).filter((x) => x.length > 0);
126
+ continue;
127
+ }
128
+ return null;
129
+ }
130
+ if (wantsAll) {
131
+ return { mode: "all" };
132
+ }
133
+ if (ids.length > 0) {
134
+ return { mode: "ids", agentNodeIds: ids };
135
+ }
136
+ return null;
137
+ }
138
+ function resolveAgentPlayRootPath(options) {
139
+ if (typeof options.rootFilePath === "string" && options.rootFilePath.trim().length > 0) {
140
+ return resolve(options.rootFilePath.trim());
141
+ }
142
+ const fromEnv = process.env.AGENT_PLAY_ROOT_FILE_PATH;
143
+ if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
144
+ return resolve(fromEnv.trim());
145
+ }
146
+ const homeRoot = join(homedir(), ".agent-play", ".root");
147
+ if (existsSync(homeRoot)) {
148
+ return homeRoot;
149
+ }
150
+ const cwdRoot = resolve(process.cwd(), ".root");
151
+ if (existsSync(cwdRoot)) {
152
+ return cwdRoot;
153
+ }
154
+ throw new Error(
155
+ "Agent Play root key not found. Pass --root-file <path>, set AGENT_PLAY_ROOT_FILE_PATH, or place .root in ~/.agent-play/ or the project directory."
156
+ );
157
+ }
158
+ async function registerNodeOnServer(serverUrl, passw, expectedNodeId) {
159
+ const res = await fetch(`${serverUrl}/api/nodes`, {
160
+ method: "POST",
161
+ headers: { "content-type": "application/json" },
162
+ body: JSON.stringify({ kind: "main", passw })
163
+ });
164
+ const text = await res.text();
165
+ if (res.status === 409) {
166
+ return;
167
+ }
168
+ if (!res.ok) {
169
+ let msg = text;
170
+ try {
171
+ const err = JSON.parse(text);
172
+ if (typeof err.error === "string") {
173
+ msg = err.error;
174
+ }
175
+ } catch {
120
176
  }
121
- token = regJson.token;
177
+ throw new Error(
178
+ `Node registration failed (${String(res.status)}): ${msg}`
179
+ );
180
+ }
181
+ const json = JSON.parse(text);
182
+ if (typeof json.nodeId !== "string") {
183
+ throw new Error("Invalid response from server.");
184
+ }
185
+ if (json.nodeId !== expectedNodeId) {
186
+ console.log("json", json);
187
+ console.log("expectedNodeId", expectedNodeId);
188
+ throw new Error(
189
+ "Server node id does not match local derivation; check root file and server configuration."
190
+ );
122
191
  }
123
- await saveCredentials({ serverUrl, token });
124
- console.log(`Signed in. Credentials saved to ${credentialsPath()}`);
125
192
  }
126
- async function cmdLogout() {
193
+ async function cmdBootstrapNode(argv) {
194
+ const opts = parseBootstrapNodeArgs(argv);
195
+ const rl = createInterface({ input, output });
196
+ const serverUrl = await promptBootstrapEnvironment(rl);
197
+ rl.close();
198
+ console.log(`Using server: ${serverUrl}`);
199
+ const rootPath = resolveAgentPlayRootPath(opts);
200
+ const rootKey = loadRootKey(rootPath);
201
+ const dir = join(homedir(), ".agent-play");
202
+ await mkdir(dir, { recursive: true });
203
+ const generatedPassw = generateNodePassw();
204
+ const hashedPassw = hashNodePassword(generatedPassw);
205
+ const credential = createNodeCredentialFromPassw({ passw: hashedPassw, rootKey });
206
+ await registerNodeOnServer(serverUrl, hashedPassw, credential.nodeId);
207
+ await saveCredentials({
208
+ serverUrl,
209
+ nodeId: credential.nodeId,
210
+ passw: generatedPassw
211
+ });
212
+ console.log(
213
+ `genesisNodeId (platform root key from .root; all main nodes derive under this): ${rootKey}`
214
+ );
215
+ console.log(`mainNodeId (your developer node): ${credential.nodeId}`);
216
+ console.log(`passw: ${generatedPassw}`);
217
+ console.log("Keep this material safe. Losing it means losing access.");
218
+ }
219
+ async function cmdClearNodeCredentials() {
127
220
  try {
128
221
  await unlink(credentialsPath());
129
- console.log("Logged out.");
222
+ console.log("Credentials removed.");
130
223
  } catch {
131
- console.log("No saved session.");
224
+ console.log("No saved credentials.");
132
225
  }
133
226
  }
134
227
  function printAgentPlayIntegrationGuide() {
@@ -136,7 +229,7 @@ function printAgentPlayIntegrationGuide() {
136
229
  console.log("How your agent appears on the play world");
137
230
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
138
231
  console.log(
139
- " \u2022 One account API key: run `agent-play create-key` (after login) if you do not have one yet."
232
+ " \u2022 Use ~/.agent-play/credentials.json + .root with RemotePlayWorld({ nodeCredentials })."
140
233
  );
141
234
  console.log(
142
235
  " \u2022 LangChain: use langchainRegistration(agent) and pass agent.toolNames to addPlayer."
@@ -148,22 +241,38 @@ function printAgentPlayIntegrationGuide() {
148
241
  " \u2022 Structures on the map are derived from those tool names \u2014 keep them aligned with your real tools."
149
242
  );
150
243
  console.log(
151
- " \u2022 RemotePlayWorld({ apiKey: <account key> }) and addPlayer({ ..., agentId: <id below> })."
244
+ " \u2022 RemotePlayWorld({ nodeCredentials: { rootKey, passw } }) and addAgent({ nodeId, ... })."
152
245
  );
153
246
  console.log("");
154
247
  }
155
- async function cmdCreateKey() {
248
+ async function cmdCreateAgentNode() {
156
249
  const cred = await loadCredentials();
157
250
  if (cred === null) {
158
- console.error("Run `agent-play login` first.");
251
+ console.error(
252
+ "Run `agent-play create-main-node` (or `bootstrap-node`) first."
253
+ );
159
254
  process.exitCode = 1;
160
255
  return;
161
256
  }
162
- const res = await fetch(`${cred.serverUrl}/api/agents/api-key`, {
257
+ const rootKey = loadRootKey(resolveAgentPlayRootPath({}));
258
+ const agentPassw = generateNodePassw();
259
+ const hashedAgentPassw = hashNodePassword(agentPassw);
260
+ const agentNodeId = deriveNodeIdFromPassword({
261
+ password: hashedAgentPassw,
262
+ rootKey
263
+ });
264
+ const res = await fetch(`${cred.serverUrl}/api/nodes/agent-node`, {
163
265
  method: "POST",
164
266
  headers: {
165
- authorization: `Bearer ${cred.token}`
166
- }
267
+ "content-type": "application/json",
268
+ ...nodeAuthHeaders(cred)
269
+ },
270
+ body: JSON.stringify({
271
+ kind: "agent",
272
+ parentNodeId: cred.nodeId,
273
+ agentNodeId,
274
+ agentNodePassw: hashedAgentPassw
275
+ })
167
276
  });
168
277
  const text = await res.text();
169
278
  if (!res.ok) {
@@ -173,65 +282,55 @@ async function cmdCreateKey() {
173
282
  if (typeof err.error === "string") msg = err.error;
174
283
  } catch {
175
284
  }
176
- console.error(`create-key failed (${res.status}): ${msg}`);
285
+ console.error(`Create failed (${res.status}): ${msg}`);
177
286
  process.exitCode = 1;
178
287
  return;
179
288
  }
180
289
  const json = JSON.parse(text);
181
- if (typeof json.plainApiKey !== "string") {
290
+ if (typeof json.agentId !== "string") {
182
291
  console.error("Invalid response from server.");
183
292
  process.exitCode = 1;
184
293
  return;
185
294
  }
186
- console.log("API key (store securely; shown once):");
187
- console.log(json.plainApiKey);
188
- console.log("");
189
- }
190
- async function cmdViewKeys() {
191
- const cred = await loadCredentials();
192
- if (cred === null) {
193
- console.error("Run `agent-play login` first.");
295
+ if (json.agentId !== agentNodeId) {
296
+ console.error(
297
+ "Server returned a different agent node id than the locally derived one."
298
+ );
194
299
  process.exitCode = 1;
195
300
  return;
196
301
  }
197
- const res = await fetch(`${cred.serverUrl}/api/agents/api-key`, {
198
- headers: { authorization: `Bearer ${cred.token}` }
302
+ const nextAgentNodes = [
303
+ ...(cred.agentNodes ?? []).filter((n) => n.nodeId !== agentNodeId),
304
+ {
305
+ nodeId: agentNodeId,
306
+ passw: agentPassw,
307
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
308
+ }
309
+ ];
310
+ await saveCredentials({
311
+ ...cred,
312
+ agentNodes: nextAgentNodes
199
313
  });
200
- const text = await res.text();
201
- if (!res.ok) {
202
- console.error(`view-keys failed (${res.status}): ${text}`);
203
- process.exitCode = 1;
204
- return;
205
- }
206
- const json = JSON.parse(text);
207
- if (json.hasKey === true) {
208
- const when = typeof json.createdAt === "string" ? json.createdAt : "unknown time";
209
- console.log(`Account API key: active (created ${when}).`);
210
- console.log(
211
- "The secret value cannot be shown again. Use the key you saved when you ran `agent-play create-key`."
212
- );
213
- } else {
214
- console.log("No API key for this account.");
215
- console.log("Run `agent-play create-key` to generate one (shown once).");
216
- }
314
+ printAgentPlayIntegrationGuide();
315
+ console.log(`Created agent node id: ${json.agentId}`);
316
+ console.log(`Agent node passw: ${agentPassw}`);
317
+ console.log(
318
+ `Saved agent node credentials to ${credentialsPath()} (agentNodes).`
319
+ );
320
+ console.log("Keep this material safe. Losing it means losing access.");
321
+ console.log("");
217
322
  }
218
- async function cmdCreate() {
323
+ async function cmdInspectNode() {
219
324
  const cred = await loadCredentials();
220
325
  if (cred === null) {
221
- console.error("Run `agent-play login` first.");
326
+ console.error(
327
+ "Run `agent-play create-main-node` (or `bootstrap-node`) first."
328
+ );
222
329
  process.exitCode = 1;
223
330
  return;
224
331
  }
225
- const rl = createInterface({ input, output });
226
- const name = (await rl.question("Agent name: ")).trim() || "agent";
227
- rl.close();
228
- const res = await fetch(`${cred.serverUrl}/api/agents`, {
229
- method: "POST",
230
- headers: {
231
- "content-type": "application/json",
232
- authorization: `Bearer ${cred.token}`
233
- },
234
- body: JSON.stringify({ name })
332
+ const res = await fetch(`${cred.serverUrl}/api/nodes`, {
333
+ headers: nodeAuthHeaders(cred)
235
334
  });
236
335
  const text = await res.text();
237
336
  if (!res.ok) {
@@ -241,29 +340,71 @@ async function cmdCreate() {
241
340
  if (typeof err.error === "string") msg = err.error;
242
341
  } catch {
243
342
  }
244
- console.error(`Create failed (${res.status}): ${msg}`);
343
+ console.error(`Inspect failed (${res.status}): ${msg}`);
245
344
  process.exitCode = 1;
246
345
  return;
247
346
  }
248
347
  const json = JSON.parse(text);
249
- if (typeof json.agentId !== "string") {
250
- console.error("Invalid response from server.");
348
+ if (typeof json.genesisNodeId !== "string") {
349
+ console.error("Invalid inspect response.");
251
350
  process.exitCode = 1;
252
351
  return;
253
352
  }
254
- printAgentPlayIntegrationGuide();
255
- console.log(`Created agent id: ${json.agentId}`);
353
+ const main2 = json.mainNode;
354
+ if (typeof main2 !== "object" || main2 === null) {
355
+ console.error("Invalid inspect response.");
356
+ process.exitCode = 1;
357
+ return;
358
+ }
359
+ const mn = main2;
360
+ if (typeof mn.nodeId !== "string" || typeof mn.createdAt !== "string") {
361
+ console.error("Invalid inspect response.");
362
+ process.exitCode = 1;
363
+ return;
364
+ }
365
+ const agentNodeIdsFromMain = Array.isArray(mn.agentNodeIds) ? mn.agentNodeIds.filter((x) => typeof x === "string") : [];
366
+ const runtimeAgents = parseAgentRows(json.agentNodes);
367
+ console.log("Platform genesis node id (from server .root / root key):");
368
+ console.log(` ${json.genesisNodeId}`);
369
+ console.log("");
370
+ console.log("Your main developer node:");
371
+ console.log(` nodeId: ${mn.nodeId}`);
372
+ console.log(` createdAt: ${mn.createdAt}`);
373
+ console.log("");
374
+ console.log(
375
+ `Agent node identities (create-agent-node) (${String(agentNodeIdsFromMain.length)}):`
376
+ );
377
+ if (agentNodeIdsFromMain.length === 0) {
378
+ console.log(" (none)");
379
+ } else {
380
+ agentNodeIdsFromMain.forEach((id, i) => {
381
+ console.log(` ${String(i + 1)}. ${id}`);
382
+ });
383
+ }
384
+ console.log("");
385
+ console.log(
386
+ `Runtime agents \u2014 SDK metadata (${String(runtimeAgents.length)}):`
387
+ );
388
+ if (runtimeAgents.length === 0) {
389
+ console.log(" (none)");
390
+ } else {
391
+ runtimeAgents.forEach((a, i) => {
392
+ console.log(` ${String(i + 1)}. ${a.agentId} \u2014 ${a.name}`);
393
+ });
394
+ }
256
395
  console.log("");
257
396
  }
258
- async function cmdDelete() {
397
+ async function cmdListAgentNodes() {
259
398
  const cred = await loadCredentials();
260
399
  if (cred === null) {
261
- console.error("Run `agent-play login` first.");
400
+ console.error(
401
+ "Run `agent-play create-main-node` (or `bootstrap-node`) first."
402
+ );
262
403
  process.exitCode = 1;
263
404
  return;
264
405
  }
265
406
  const listRes = await fetch(`${cred.serverUrl}/api/agents`, {
266
- headers: { authorization: `Bearer ${cred.token}` }
407
+ headers: nodeAuthHeaders(cred)
267
408
  });
268
409
  const listText = await listRes.text();
269
410
  if (!listRes.ok) {
@@ -272,30 +413,48 @@ async function cmdDelete() {
272
413
  return;
273
414
  }
274
415
  const listJson = JSON.parse(listText);
275
- const agentsRaw = listJson.agents;
276
- if (!Array.isArray(agentsRaw)) {
277
- console.error("Invalid list response.");
278
- process.exitCode = 1;
279
- return;
280
- }
281
- const agents = [];
282
- for (const a of agentsRaw) {
283
- if (typeof a !== "object" || a === null) continue;
284
- const o = a;
285
- if (typeof o.agentId === "string" && typeof o.name === "string") {
286
- agents.push({ agentId: o.agentId, name: o.name });
287
- }
288
- }
416
+ const agents = parseAgentRows(listJson.agents);
289
417
  if (agents.length === 0) {
290
- console.log("No agents.");
418
+ console.log("No agent nodes.");
291
419
  return;
292
420
  }
293
421
  agents.forEach((a, i) => {
294
- console.log(`${i + 1}. ${a.agentId} (${a.name})`);
422
+ console.log(`${String(i + 1)}. ${a.agentId} (${a.name})`);
295
423
  });
296
- const rl = createInterface({ input, output });
297
- const pick = (await rl.question("Agent id to delete (empty = cancel): ")).trim();
298
- rl.close();
424
+ }
425
+ async function cmdDeleteAgentNode(argv) {
426
+ const cred = await loadCredentials();
427
+ if (cred === null) {
428
+ console.error(
429
+ "Run `agent-play create-main-node` (or `bootstrap-node`) first."
430
+ );
431
+ process.exitCode = 1;
432
+ return;
433
+ }
434
+ let pick = argv[0]?.trim() ?? "";
435
+ if (pick.length === 0) {
436
+ const listRes = await fetch(`${cred.serverUrl}/api/agents`, {
437
+ headers: nodeAuthHeaders(cred)
438
+ });
439
+ const listText = await listRes.text();
440
+ if (!listRes.ok) {
441
+ console.error(`List failed (${listRes.status}): ${listText}`);
442
+ process.exitCode = 1;
443
+ return;
444
+ }
445
+ const listJson = JSON.parse(listText);
446
+ const agents = parseAgentRows(listJson.agents);
447
+ if (agents.length === 0) {
448
+ console.log("No agents.");
449
+ return;
450
+ }
451
+ agents.forEach((a, i) => {
452
+ console.log(`${i + 1}. ${a.agentId} (${a.name})`);
453
+ });
454
+ const rl = createInterface({ input, output });
455
+ pick = (await rl.question("Agent id to delete (empty = cancel): ")).trim();
456
+ rl.close();
457
+ }
299
458
  if (pick.length === 0) {
300
459
  console.log("Cancelled.");
301
460
  return;
@@ -304,7 +463,7 @@ async function cmdDelete() {
304
463
  `${cred.serverUrl}/api/agents?id=${encodeURIComponent(pick)}`,
305
464
  {
306
465
  method: "DELETE",
307
- headers: { authorization: `Bearer ${cred.token}` }
466
+ headers: nodeAuthHeaders(cred)
308
467
  }
309
468
  );
310
469
  const delText = await delRes.text();
@@ -316,34 +475,225 @@ async function cmdDelete() {
316
475
  const delJson = JSON.parse(delText);
317
476
  console.log(delJson.ok === true ? "Deleted." : "Not found.");
318
477
  }
478
+ async function cmdDeleteMainNode() {
479
+ const cred = await loadCredentials();
480
+ if (cred === null) {
481
+ console.error(
482
+ "Run `agent-play create-main-node` (or `bootstrap-node`) first."
483
+ );
484
+ process.exitCode = 1;
485
+ return;
486
+ }
487
+ console.error("");
488
+ console.error("WARNING: You are about to delete your main developer node.");
489
+ console.error(
490
+ "The server will remove this node and cascade-delete every registered"
491
+ );
492
+ console.error(
493
+ "agent node (SDK agent registration) that belongs to it. This cannot be undone."
494
+ );
495
+ console.error(
496
+ "You will need a new passphrase and secret file to join the platform again."
497
+ );
498
+ console.error("");
499
+ const rl = createInterface({ input, output });
500
+ const typed = (await rl.question(
501
+ `Type your main node id exactly to confirm (${cred.nodeId}): `
502
+ )).trim();
503
+ rl.close();
504
+ if (typed !== cred.nodeId) {
505
+ console.log("Confirmation did not match. Cancelled.");
506
+ return;
507
+ }
508
+ const res = await fetch(`${cred.serverUrl}/api/nodes`, {
509
+ method: "DELETE",
510
+ headers: {
511
+ "content-type": "application/json",
512
+ ...nodeAuthHeaders(cred)
513
+ },
514
+ body: JSON.stringify({ confirmNodeId: cred.nodeId })
515
+ });
516
+ const text = await res.text();
517
+ if (!res.ok) {
518
+ let msg = text;
519
+ try {
520
+ const err = JSON.parse(text);
521
+ if (typeof err.error === "string") msg = err.error;
522
+ } catch {
523
+ }
524
+ console.error(`Delete main node failed (${res.status}): ${msg}`);
525
+ process.exitCode = 1;
526
+ return;
527
+ }
528
+ const json = JSON.parse(text);
529
+ if (json.ok !== true) {
530
+ console.error("Unexpected response from server.");
531
+ process.exitCode = 1;
532
+ return;
533
+ }
534
+ const n = typeof json.deletedAgentCount === "number" ? json.deletedAgentCount : 0;
535
+ console.log(
536
+ `Main node removed. Cascaded agent nodes deleted: ${String(n)}.`
537
+ );
538
+ console.log("Run `agent-play clear-node-credentials` to forget local creds.");
539
+ }
540
+ async function validateNodeIdentityOnServer(options) {
541
+ const res = await fetch(`${options.cred.serverUrl}/api/nodes/validate`, {
542
+ method: "POST",
543
+ headers: {
544
+ "content-type": "application/json",
545
+ ...nodeAuthHeaders(options.cred)
546
+ },
547
+ body: JSON.stringify({
548
+ nodeId: options.nodeId,
549
+ rootKey: options.rootKey,
550
+ mainNodeId: options.mainNodeId
551
+ })
552
+ });
553
+ const text = await res.text();
554
+ let json;
555
+ try {
556
+ json = JSON.parse(text);
557
+ } catch {
558
+ throw new Error(`Validate failed (${String(res.status)}): ${text}`);
559
+ }
560
+ if (typeof json !== "object" || json === null) {
561
+ throw new Error(`Validate failed (${String(res.status)}): invalid response`);
562
+ }
563
+ const obj = json;
564
+ if (typeof obj.ok !== "boolean") {
565
+ const err = typeof obj.error === "string" ? obj.error : text;
566
+ throw new Error(`Validate failed (${String(res.status)}): ${err}`);
567
+ }
568
+ return {
569
+ ok: obj.ok,
570
+ reason: typeof obj.reason === "string" ? obj.reason : void 0,
571
+ nodeKind: typeof obj.nodeKind === "string" ? obj.nodeKind : void 0
572
+ };
573
+ }
574
+ async function cmdValidateMainNode() {
575
+ const cred = await loadCredentials();
576
+ if (cred === null) {
577
+ console.error(
578
+ "Run `agent-play create-main-node` (or `bootstrap-node`) first."
579
+ );
580
+ process.exitCode = 1;
581
+ return;
582
+ }
583
+ const rootKey = loadRootKey(resolveAgentPlayRootPath({}));
584
+ const result = await validateNodeIdentityOnServer({
585
+ cred,
586
+ rootKey,
587
+ nodeId: cred.nodeId
588
+ });
589
+ if (!result.ok) {
590
+ console.error(
591
+ `Main node validation failed: ${result.reason ?? "unknown reason"}`
592
+ );
593
+ process.exitCode = 1;
594
+ return;
595
+ }
596
+ console.log(`Main node validation passed: ${cred.nodeId}`);
597
+ }
598
+ async function cmdValidateAgentNode(argv) {
599
+ const cred = await loadCredentials();
600
+ if (cred === null) {
601
+ console.error(
602
+ "Run `agent-play create-main-node` (or `bootstrap-node`) first."
603
+ );
604
+ process.exitCode = 1;
605
+ return;
606
+ }
607
+ const opts = parseValidateAgentNodeArgs(argv);
608
+ if (opts === null) {
609
+ console.error(
610
+ "Usage: agent-play validate-agent-node --all | --agent-node-ids <id1,id2,...>"
611
+ );
612
+ process.exitCode = 1;
613
+ return;
614
+ }
615
+ const rootKey = loadRootKey(resolveAgentPlayRootPath({}));
616
+ const targetIds = opts.mode === "all" ? (cred.agentNodes ?? []).map((n) => n.nodeId) : opts.agentNodeIds;
617
+ const dedupedIds = Array.from(new Set(targetIds.filter((id) => id.length > 0)));
618
+ if (dedupedIds.length === 0) {
619
+ console.log("No agent node ids to validate.");
620
+ return;
621
+ }
622
+ let failures = 0;
623
+ for (const nodeId of dedupedIds) {
624
+ const result = await validateNodeIdentityOnServer({
625
+ cred,
626
+ rootKey,
627
+ nodeId,
628
+ mainNodeId: cred.nodeId
629
+ });
630
+ if (!result.ok) {
631
+ failures += 1;
632
+ console.error(
633
+ `FAIL ${nodeId}: ${result.reason ?? "unknown reason"}`
634
+ );
635
+ continue;
636
+ }
637
+ console.log(`PASS ${nodeId}`);
638
+ }
639
+ if (failures > 0) {
640
+ process.exitCode = 1;
641
+ return;
642
+ }
643
+ console.log(`Validated ${String(dedupedIds.length)} agent node(s) successfully.`);
644
+ }
319
645
  async function main() {
320
646
  const cmd = process.argv[2];
321
- if (cmd === "login") {
322
- await cmdLogin();
647
+ if (cmd === "bootstrap-node" || cmd === "create-main-node") {
648
+ await cmdBootstrapNode(process.argv.slice(3));
649
+ return;
650
+ }
651
+ if (cmd === "clear-node-credentials") {
652
+ await cmdClearNodeCredentials();
653
+ return;
654
+ }
655
+ if (cmd === "inspect-node") {
656
+ await cmdInspectNode();
657
+ return;
658
+ }
659
+ if (cmd === "create-agent-node" || cmd === "create") {
660
+ await cmdCreateAgentNode();
323
661
  return;
324
662
  }
325
- if (cmd === "logout") {
326
- await cmdLogout();
663
+ if (cmd === "list-agent-nodes" || cmd === "list") {
664
+ await cmdListAgentNodes();
327
665
  return;
328
666
  }
329
- if (cmd === "create") {
330
- await cmdCreate();
667
+ if (cmd === "delete-agent-node" || cmd === "delete" || cmd === "remove") {
668
+ await cmdDeleteAgentNode(process.argv.slice(3));
331
669
  return;
332
670
  }
333
- if (cmd === "create-key" || cmd === "generate-key") {
334
- await cmdCreateKey();
671
+ if (cmd === "delete-main-node") {
672
+ await cmdDeleteMainNode();
335
673
  return;
336
674
  }
337
- if (cmd === "view-keys") {
338
- await cmdViewKeys();
675
+ if (cmd === "validate-main-node") {
676
+ await cmdValidateMainNode();
339
677
  return;
340
678
  }
341
- if (cmd === "delete" || cmd === "remove") {
342
- await cmdDelete();
679
+ if (cmd === "validate-agent-node") {
680
+ await cmdValidateAgentNode(process.argv.slice(3));
343
681
  return;
344
682
  }
345
683
  console.error(
346
- "Usage: agent-play login | logout | create-key | view-keys | create | delete"
684
+ [
685
+ "Usage:",
686
+ " agent-play create-main-node | bootstrap-node [--root-file <path>]",
687
+ " agent-play inspect-node",
688
+ " agent-play create-agent-node | create",
689
+ " agent-play list-agent-nodes | list",
690
+ " agent-play delete-agent-node | delete [agent-id]",
691
+ " agent-play delete-main-node",
692
+ " agent-play validate-main-node",
693
+ " agent-play validate-agent-node --all",
694
+ " agent-play validate-agent-node --agent-node-ids <id1,id2,...>",
695
+ " agent-play clear-node-credentials"
696
+ ].join("\n")
347
697
  );
348
698
  process.exitCode = 1;
349
699
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-play/cli",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Command-line tool for Agent Play: login, API keys, and agent registration against the web UI.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -20,9 +20,11 @@
20
20
  "access": "public"
21
21
  },
22
22
  "scripts": {
23
- "build": "tsup src/cli.ts --format esm --platform node --target node20 --out-dir dist --clean"
23
+ "build": "tsup src/cli.ts --format esm --platform node --target node20 --out-dir dist --clean --external @agent-play/node-tools && node ../../scripts/copy-root-file.mjs cli"
24
+ },
25
+ "dependencies": {
26
+ "@agent-play/node-tools": "1.0.0"
24
27
  },
25
- "dependencies": {},
26
28
  "devDependencies": {
27
29
  "@types/node": "^22.10.0",
28
30
  "tsup": "^8.5.1",