@agent-play/cli 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/README.md +13 -0
  2. package/dist/README.md +91 -0
  3. package/dist/cli.js +449 -15
  4. package/package.json +8 -4
  5. package/templates/agent-starter/langchain/.env.example +6 -0
  6. package/templates/agent-starter/langchain/README.md +24 -0
  7. package/templates/agent-starter/langchain/package.json +25 -0
  8. package/templates/agent-starter/langchain/src/bare-server.ts +12 -0
  9. package/templates/agent-starter/langchain/src/builtins/definitions.ts +66 -0
  10. package/templates/agent-starter/langchain/src/builtins/toolkits/starter-tools.ts +84 -0
  11. package/templates/agent-starter/langchain/src/express-server.ts +27 -0
  12. package/templates/agent-starter/langchain/src/index.ts +6 -0
  13. package/templates/agent-starter/langchain/src/load-env.ts +5 -0
  14. package/templates/agent-starter/langchain/src/register/register-builtins.ts +62 -0
  15. package/templates/agent-starter/langchain/src/register-agents.ts +1 -0
  16. package/templates/agent-starter/langchain/src/tool-handlers/assist-brainstorm.ts +8 -0
  17. package/templates/agent-starter/langchain/src/tool-handlers/assist-calculate-coefficient.ts +35 -0
  18. package/templates/agent-starter/langchain/src/tool-handlers/assist-collect-scene-details.ts +32 -0
  19. package/templates/agent-starter/langchain/src/tool-handlers/chat-tool.ts +6 -0
  20. package/templates/agent-starter/langchain/src/tool-handlers/execute-tool-capability.ts +12 -0
  21. package/templates/agent-starter/langchain/src/tool-handlers/tool-capability-registry.ts +49 -0
  22. package/templates/agent-starter/langchain/tsconfig.json +13 -0
package/README.md CHANGED
@@ -52,6 +52,7 @@ For Redis-direct checks (ops/CI), use **`node-tools`** script **`scripts/validat
52
52
  | **`delete-main-node`** | — | **DELETE /api/nodes** — confirm by typing main node id; cascades. |
53
53
  | **`validate-main-node`** | — | **POST /api/nodes/validate** for main node id. |
54
54
  | **`validate-agent-node`** | — | **`--all`** or **`--agent-node-ids id1,id2,...`** — validate agent node ids. |
55
+ | **`initialize`** | `init` | Interactive scaffold for a starter agent codebase, optional node bootstrap, asks for agent count (1-2), and writes env-variable node ids into `.env` when bootstrapped. |
55
56
  | **`clear-node-credentials`** | — | Removes **`~/.agent-play/credentials.json`**. |
56
57
 
57
58
  ## Genesis and main node
@@ -68,6 +69,7 @@ npx agent-play validate-main-node
68
69
  npx agent-play inspect-node
69
70
  npx agent-play create-agent-node
70
71
  npx agent-play validate-agent-node --all
72
+ npx agent-play initialize
71
73
  npx agent-play list-agent-nodes
72
74
  npx agent-play delete-agent-node
73
75
  npx agent-play delete-agent-node <agent-uuid>
@@ -76,3 +78,14 @@ npx agent-play clear-node-credentials
76
78
  ```
77
79
 
78
80
  For SDK usage after bootstrap, use **`RemotePlayWorld`** and register players with **`mainNodeId`** and **`agentId`** from the CLI output.
81
+
82
+ ## Initialize quick start
83
+
84
+ - `npx agent-play initialize` (or `npx agent-play init`) scaffolds starter files.
85
+ - The flow asks whether to create nodes now and how many agent nodes to provision (max `2`).
86
+ - If bootstrap is selected, it writes:
87
+ - `AGENT_PLAY_MAIN_NODE_ID`
88
+ - `AGENT_PLAY_AGENT_NODE_ID_1`
89
+ - `AGENT_PLAY_AGENT_NODE_ID_2` (when requested)
90
+ into generated `.env`.
91
+ - Generated code references these env vars directly (no hardcoded ids).
package/dist/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @agent-play/cli
2
+
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**.
6
+
7
+ ## Documentation
8
+
9
+ - **[Repository](https://github.com/wilforlan/agent-play)**
10
+ - **[CLI guide](https://github.com/wilforlan/agent-play/blob/main/docs/cli.md)** — full **Node setup** and **Node validation** sections
11
+ - **[API reference](https://wilforlan.github.io/agent-play/)** — TypeDoc
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g @agent-play/cli
17
+ ```
18
+
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
+ | **`initialize`** | `init` | Interactive scaffold for a starter agent codebase, optional node bootstrap, asks for agent count (1-2), and writes env-variable node ids into `.env` when bootstrapped. |
56
+ | **`clear-node-credentials`** | — | Removes **`~/.agent-play/credentials.json`**. |
57
+
58
+ ## Genesis and main node
59
+
60
+ 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.
61
+
62
+ Node kinds: **`root` → `main` → `agent`**. Root has no passphrase; main and agent persist hashed material server-side.
63
+
64
+ ## Usage examples
65
+
66
+ ```bash
67
+ npx agent-play create-main-node
68
+ npx agent-play validate-main-node
69
+ npx agent-play inspect-node
70
+ npx agent-play create-agent-node
71
+ npx agent-play validate-agent-node --all
72
+ npx agent-play initialize
73
+ npx agent-play list-agent-nodes
74
+ npx agent-play delete-agent-node
75
+ npx agent-play delete-agent-node <agent-uuid>
76
+ npx agent-play delete-main-node
77
+ npx agent-play clear-node-credentials
78
+ ```
79
+
80
+ For SDK usage after bootstrap, use **`RemotePlayWorld`** and register players with **`mainNodeId`** and **`agentId`** from the CLI output.
81
+
82
+ ## Initialize quick start
83
+
84
+ - `npx agent-play initialize` (or `npx agent-play init`) scaffolds starter files.
85
+ - The flow asks whether to create nodes now and how many agent nodes to provision (max `2`).
86
+ - If bootstrap is selected, it writes:
87
+ - `AGENT_PLAY_MAIN_NODE_ID`
88
+ - `AGENT_PLAY_AGENT_NODE_ID_1`
89
+ - `AGENT_PLAY_AGENT_NODE_ID_2` (when requested)
90
+ into generated `.env`.
91
+ - Generated code references these env vars directly (no hardcoded ids).
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync } from "fs";
5
- import { mkdir, unlink, writeFile } from "fs/promises";
4
+ import { existsSync as existsSync2 } from "fs";
5
+ import { mkdir as mkdir2, unlink, writeFile as writeFile2 } from "fs/promises";
6
6
  import { homedir } from "os";
7
- import { join, resolve } from "path";
7
+ import { join as join2, resolve as resolve2 } from "path";
8
8
  import { createInterface } from "readline/promises";
9
9
  import { stdin as input, stdout as output } from "process";
10
10
  import {
@@ -15,6 +15,247 @@ import {
15
15
  loadAgentPlayCredentialsFileFromPath,
16
16
  loadRootKey
17
17
  } from "@agent-play/node-tools";
18
+
19
+ // src/initialize.ts
20
+ import { existsSync } from "fs";
21
+ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
22
+ import { dirname, join, resolve } from "path";
23
+ import { fileURLToPath } from "url";
24
+ function resolveServerUrlForEnvironment(environment) {
25
+ if (environment === "test") {
26
+ return "https://test-agent-play.com";
27
+ }
28
+ if (environment === "production") {
29
+ return "https://agent-play.com";
30
+ }
31
+ return "http://127.0.0.1:3000";
32
+ }
33
+ var TEMPLATE_ROOT = fileURLToPath(
34
+ new URL("../templates/agent-starter/langchain", import.meta.url)
35
+ );
36
+ function parseInitializeArgs(argv) {
37
+ const out = {
38
+ template: "langchain",
39
+ yes: false,
40
+ force: false
41
+ };
42
+ for (let i = 0; i < argv.length; i++) {
43
+ const token = argv[i];
44
+ if (token === "--dir" && typeof argv[i + 1] === "string") {
45
+ out.dir = argv[++i];
46
+ continue;
47
+ }
48
+ if (token === "--name" && typeof argv[i + 1] === "string") {
49
+ out.name = argv[++i];
50
+ continue;
51
+ }
52
+ if (token === "--template" && typeof argv[i + 1] === "string") {
53
+ const template = argv[++i];
54
+ if (template !== "langchain") {
55
+ return null;
56
+ }
57
+ out.template = template;
58
+ continue;
59
+ }
60
+ if (token === "--yes") {
61
+ out.yes = true;
62
+ continue;
63
+ }
64
+ if (token === "--force") {
65
+ out.force = true;
66
+ continue;
67
+ }
68
+ if (token === "--bootstrap-nodes") {
69
+ out.bootstrapNodes = true;
70
+ continue;
71
+ }
72
+ if (token === "--agent-count" && typeof argv[i + 1] === "string") {
73
+ const raw = Number(argv[++i]);
74
+ if (raw !== 1 && raw !== 2) {
75
+ return null;
76
+ }
77
+ out.agentCount = raw;
78
+ continue;
79
+ }
80
+ if (token === "--environment" && typeof argv[i + 1] === "string") {
81
+ const value = argv[++i].trim().toLowerCase();
82
+ if (value === "development" || value === "test" || value === "production") {
83
+ out.environment = value;
84
+ continue;
85
+ }
86
+ return null;
87
+ }
88
+ if (token === "--server-type" && typeof argv[i + 1] === "string") {
89
+ const value = argv[++i].trim().toLowerCase();
90
+ if (value === "bare" || value === "express") {
91
+ out.serverType = value;
92
+ continue;
93
+ }
94
+ return null;
95
+ }
96
+ return null;
97
+ }
98
+ return out;
99
+ }
100
+ function normalizeProjectName(raw) {
101
+ if (typeof raw !== "string") {
102
+ return "agent-play-agent-starter";
103
+ }
104
+ const normalized = raw.trim().toLowerCase().replace(/[^a-z0-9-_]+/g, "-");
105
+ if (normalized.length === 0) {
106
+ return "agent-play-agent-starter";
107
+ }
108
+ return normalized;
109
+ }
110
+ async function listTemplateFiles(dir) {
111
+ const entries = await readdir(dir, { withFileTypes: true });
112
+ const files = [];
113
+ for (const entry of entries) {
114
+ const full = join(dir, entry.name);
115
+ if (entry.isDirectory()) {
116
+ const nested = await listTemplateFiles(full);
117
+ files.push(...nested.map((path) => join(entry.name, path)));
118
+ continue;
119
+ }
120
+ files.push(entry.name);
121
+ }
122
+ return files;
123
+ }
124
+ async function ensureSafeTarget(options) {
125
+ const targetExists = existsSync(options.targetDir);
126
+ if (!targetExists) {
127
+ await mkdir(options.targetDir, { recursive: true });
128
+ return;
129
+ }
130
+ if (options.force) {
131
+ return;
132
+ }
133
+ const existing = await readdir(options.targetDir);
134
+ if (existing.length > 0) {
135
+ throw new Error(
136
+ `Target directory is not empty: ${options.targetDir}. Re-run with --force to overwrite scaffold-managed files.`
137
+ );
138
+ }
139
+ }
140
+ function patchEnvContent(options) {
141
+ const lines = options.envContent.split(/\r?\n/);
142
+ const updates = /* @__PURE__ */ new Map([
143
+ ["AGENT_PLAY_WEB_UI_URL", options.serverUrl],
144
+ ["AGENT_PLAY_MAIN_NODE_ID", options.mainNodeId],
145
+ ["AGENT_PLAY_AGENT_NODE_ID_1", options.agentNodeIds[0] ?? ""],
146
+ ["AGENT_PLAY_AGENT_NODE_ID_2", options.agentNodeIds[1] ?? ""]
147
+ ]);
148
+ const seen = /* @__PURE__ */ new Set();
149
+ const next = lines.map((line) => {
150
+ const eqIndex = line.indexOf("=");
151
+ if (eqIndex <= 0) {
152
+ return line;
153
+ }
154
+ const key = line.slice(0, eqIndex);
155
+ const update = updates.get(key);
156
+ if (update === void 0) {
157
+ return line;
158
+ }
159
+ seen.add(key);
160
+ return `${key}=${update}`;
161
+ });
162
+ for (const [key, value] of updates.entries()) {
163
+ if (!seen.has(key)) {
164
+ next.push(`${key}=${value}`);
165
+ }
166
+ }
167
+ return next.join("\n");
168
+ }
169
+ async function renderTemplate(options) {
170
+ const files = await listTemplateFiles(TEMPLATE_ROOT);
171
+ for (const relativePath of files) {
172
+ const templatePath = join(TEMPLATE_ROOT, relativePath);
173
+ const targetPath = join(options.targetDir, relativePath);
174
+ if (!options.force && existsSync(targetPath)) {
175
+ continue;
176
+ }
177
+ await mkdir(dirname(targetPath), { recursive: true });
178
+ const source = await readFile(templatePath, "utf8");
179
+ const serverModule = options.serverType === "express" ? "./express-server.js" : "./bare-server.js";
180
+ const content = source.replaceAll("__PROJECT_NAME__", options.projectName).replaceAll("__AGENT_NAME__", "Starter Agent").replaceAll("__SERVER_MODULE__", serverModule);
181
+ await writeFile(targetPath, content, "utf8");
182
+ }
183
+ }
184
+ async function cmdInitialize(options) {
185
+ const parsed = parseInitializeArgs(options.argv);
186
+ if (parsed === null) {
187
+ throw new Error(
188
+ "Usage: agent-play initialize [--dir <path>] [--name <project-name>] [--template langchain] [--environment <development|test|production>] [--server-type <bare|express>] [--yes] [--force] [--bootstrap-nodes] [--agent-count <1|2>]"
189
+ );
190
+ }
191
+ const targetDir = resolve(parsed.dir ?? process.cwd());
192
+ const projectName = normalizeProjectName(parsed.name ?? basenameFromPath(targetDir));
193
+ const environment = parsed.environment ?? (parsed.yes ? "development" : await options.promptApi.askEnvironment());
194
+ const serverType = parsed.serverType ?? (parsed.yes ? "bare" : await options.promptApi.askServerType());
195
+ const serverUrl = resolveServerUrlForEnvironment(environment);
196
+ const agentCount = parsed.agentCount ?? (parsed.yes ? 1 : await options.promptApi.askAgentCount());
197
+ const bootstrapNodes = parsed.bootstrapNodes ?? (parsed.yes ? false : await options.promptApi.askBootstrapNodes());
198
+ await ensureSafeTarget({ targetDir, force: parsed.force });
199
+ await renderTemplate({
200
+ targetDir,
201
+ projectName,
202
+ serverType,
203
+ force: parsed.force
204
+ });
205
+ const envExamplePath = join(targetDir, ".env.example");
206
+ const envPath = join(targetDir, ".env");
207
+ if (!existsSync(envPath) && existsSync(envExamplePath)) {
208
+ await writeFile(envPath, await readFile(envExamplePath, "utf8"), "utf8");
209
+ }
210
+ if (bootstrapNodes) {
211
+ const bootstrapped = await options.runtimeApi.bootstrapNodeIds({
212
+ agentCount,
213
+ serverUrl
214
+ });
215
+ const envContent = existsSync(envPath) ? await readFile(envPath, "utf8") : "";
216
+ const nextEnv = patchEnvContent({
217
+ envContent,
218
+ serverUrl,
219
+ mainNodeId: bootstrapped.mainNodeId,
220
+ agentNodeIds: bootstrapped.agentNodeIds
221
+ });
222
+ await writeFile(envPath, nextEnv, "utf8");
223
+ console.log(`Bootstrapped main node id: ${bootstrapped.mainNodeId}`);
224
+ for (const [index, nodeId] of bootstrapped.agentNodeIds.entries()) {
225
+ console.log(`Bootstrapped agent node ${String(index + 1)} id: ${nodeId}`);
226
+ }
227
+ } else if (existsSync(envPath)) {
228
+ const envContent = await readFile(envPath, "utf8");
229
+ const nextEnv = patchEnvContent({
230
+ envContent,
231
+ serverUrl,
232
+ mainNodeId: "",
233
+ agentNodeIds: []
234
+ });
235
+ await writeFile(envPath, nextEnv, "utf8");
236
+ }
237
+ console.log("");
238
+ console.log("Agent starter scaffold created.");
239
+ console.log(`Location: ${targetDir}`);
240
+ console.log("Next steps:");
241
+ console.log(` cd "${targetDir}"`);
242
+ console.log(" npm install");
243
+ if (!bootstrapNodes) {
244
+ console.log(" npx agent-play create-main-node");
245
+ console.log(" npx agent-play create-agent-node");
246
+ if (agentCount === 2) {
247
+ console.log(" npx agent-play create-agent-node");
248
+ }
249
+ console.log(" copy node ids into .env");
250
+ }
251
+ console.log(" npm run dev");
252
+ }
253
+ function basenameFromPath(pathValue) {
254
+ const split = pathValue.split(/[\\/]/).filter((part) => part.length > 0);
255
+ return split[split.length - 1] ?? "agent-play-agent-starter";
256
+ }
257
+
258
+ // src/cli.ts
18
259
  function nodeAuthHeaders(cred) {
19
260
  return {
20
261
  "x-node-id": cred.nodeId,
@@ -36,15 +277,15 @@ function parseAgentRows(agentsRaw) {
36
277
  return agents;
37
278
  }
38
279
  function credentialsPath() {
39
- return join(homedir(), ".agent-play", "credentials.json");
280
+ return join2(homedir(), ".agent-play", "credentials.json");
40
281
  }
41
282
  async function loadCredentials() {
42
283
  return loadAgentPlayCredentialsFileFromPath(credentialsPath());
43
284
  }
44
285
  async function saveCredentials(c) {
45
- const dir = join(homedir(), ".agent-play");
46
- await mkdir(dir, { recursive: true });
47
- await writeFile(
286
+ const dir = join2(homedir(), ".agent-play");
287
+ await mkdir2(dir, { recursive: true });
288
+ await writeFile2(
48
289
  credentialsPath(),
49
290
  JSON.stringify(c, null, 2),
50
291
  "utf8"
@@ -101,6 +342,102 @@ async function promptBootstrapEnvironment(rl) {
101
342
  );
102
343
  }
103
344
  }
345
+ function parseInitializeBootstrapAnswer(raw) {
346
+ const trimmed = raw.trim().toLowerCase();
347
+ if (trimmed === "" || trimmed === "y" || trimmed === "yes") {
348
+ return true;
349
+ }
350
+ if (trimmed === "n" || trimmed === "no") {
351
+ return false;
352
+ }
353
+ return null;
354
+ }
355
+ async function promptInitializeBootstrapChoice(rl) {
356
+ for (; ; ) {
357
+ const answer = await rl.question("Create node identities now? [Y/n]: ");
358
+ const parsed = parseInitializeBootstrapAnswer(answer);
359
+ if (parsed !== null) {
360
+ return parsed;
361
+ }
362
+ console.log("Please answer yes or no.");
363
+ }
364
+ }
365
+ function parseInitializeAgentCount(raw) {
366
+ const trimmed = raw.trim();
367
+ if (trimmed === "" || trimmed === "1") {
368
+ return 1;
369
+ }
370
+ if (trimmed === "2") {
371
+ return 2;
372
+ }
373
+ return null;
374
+ }
375
+ async function promptInitializeAgentCount(rl) {
376
+ for (; ; ) {
377
+ const answer = await rl.question("How many agents do you want to deploy? (1-2) [1]: ");
378
+ const parsed = parseInitializeAgentCount(answer);
379
+ if (parsed !== null) {
380
+ return parsed;
381
+ }
382
+ console.log("Invalid value. Enter 1 or 2.");
383
+ }
384
+ }
385
+ function parseInitializeEnvironmentAnswer(raw) {
386
+ const trimmed = raw.trim().toLowerCase();
387
+ if (trimmed === "" || trimmed === "1" || trimmed === "development" || trimmed === "dev") {
388
+ return "development";
389
+ }
390
+ if (trimmed === "2" || trimmed === "test") {
391
+ return "test";
392
+ }
393
+ if (trimmed === "3" || trimmed === "production" || trimmed === "prod") {
394
+ return "production";
395
+ }
396
+ return null;
397
+ }
398
+ async function promptInitializeEnvironment(rl) {
399
+ const lines = [
400
+ "Choose environment for initialization:",
401
+ " 1) development \u2192 http://127.0.0.1:3000",
402
+ " 2) test \u2192 https://test-agent-play.com",
403
+ " 3) production \u2192 https://agent-play.com",
404
+ "Enter 1-3, or development/test/production [1]: "
405
+ ].join("\n");
406
+ for (; ; ) {
407
+ const answer = await rl.question(lines);
408
+ const parsed = parseInitializeEnvironmentAnswer(answer);
409
+ if (parsed !== null) {
410
+ return parsed;
411
+ }
412
+ console.log("Invalid choice. Enter 1, 2, 3, development, test, or production.");
413
+ }
414
+ }
415
+ function parseInitializeServerTypeAnswer(raw) {
416
+ const trimmed = raw.trim().toLowerCase();
417
+ if (trimmed === "" || trimmed === "1" || trimmed === "bare") {
418
+ return "bare";
419
+ }
420
+ if (trimmed === "2" || trimmed === "express") {
421
+ return "express";
422
+ }
423
+ return null;
424
+ }
425
+ async function promptInitializeServerType(rl) {
426
+ const lines = [
427
+ "Choose server runtime:",
428
+ " 1) bare \u2192 simple process entrypoint (minimal)",
429
+ " 2) express \u2192 deployable HTTP server with /health endpoint",
430
+ "Enter 1-2, or bare/express [1]: "
431
+ ].join("\n");
432
+ for (; ; ) {
433
+ const answer = await rl.question(lines);
434
+ const parsed = parseInitializeServerTypeAnswer(answer);
435
+ if (parsed !== null) {
436
+ return parsed;
437
+ }
438
+ console.log("Invalid choice. Enter 1, 2, bare, or express.");
439
+ }
440
+ }
104
441
  function parseBootstrapNodeArgs(argv) {
105
442
  const out = {};
106
443
  for (let i = 0; i < argv.length; i++) {
@@ -137,18 +474,18 @@ function parseValidateAgentNodeArgs(argv) {
137
474
  }
138
475
  function resolveAgentPlayRootPath(options) {
139
476
  if (typeof options.rootFilePath === "string" && options.rootFilePath.trim().length > 0) {
140
- return resolve(options.rootFilePath.trim());
477
+ return resolve2(options.rootFilePath.trim());
141
478
  }
142
479
  const fromEnv = process.env.AGENT_PLAY_ROOT_FILE_PATH;
143
480
  if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
144
- return resolve(fromEnv.trim());
481
+ return resolve2(fromEnv.trim());
145
482
  }
146
- const homeRoot = join(homedir(), ".agent-play", ".root");
147
- if (existsSync(homeRoot)) {
483
+ const homeRoot = join2(homedir(), ".agent-play", ".root");
484
+ if (existsSync2(homeRoot)) {
148
485
  return homeRoot;
149
486
  }
150
- const cwdRoot = resolve(process.cwd(), ".root");
151
- if (existsSync(cwdRoot)) {
487
+ const cwdRoot = resolve2(process.cwd(), ".root");
488
+ if (existsSync2(cwdRoot)) {
152
489
  return cwdRoot;
153
490
  }
154
491
  throw new Error(
@@ -198,8 +535,8 @@ async function cmdBootstrapNode(argv) {
198
535
  console.log(`Using server: ${serverUrl}`);
199
536
  const rootPath = resolveAgentPlayRootPath(opts);
200
537
  const rootKey = loadRootKey(rootPath);
201
- const dir = join(homedir(), ".agent-play");
202
- await mkdir(dir, { recursive: true });
538
+ const dir = join2(homedir(), ".agent-play");
539
+ await mkdir2(dir, { recursive: true });
203
540
  const generatedPassw = generateNodePassw();
204
541
  const hashedPassw = hashNodePassword(generatedPassw);
205
542
  const credential = createNodeCredentialFromPassw({ passw: hashedPassw, rootKey });
@@ -320,6 +657,67 @@ async function cmdCreateAgentNode() {
320
657
  console.log("Keep this material safe. Losing it means losing access.");
321
658
  console.log("");
322
659
  }
660
+ async function ensureMainCredentialsForInitialize(serverUrl) {
661
+ const existing = await loadCredentials();
662
+ if (existing !== null && existing.serverUrl.replace(/\/$/, "") === serverUrl.replace(/\/$/, "")) {
663
+ return existing;
664
+ }
665
+ const rootKey = loadRootKey(resolveAgentPlayRootPath({}));
666
+ const generatedPassw = generateNodePassw();
667
+ const hashedPassw = hashNodePassword(generatedPassw);
668
+ const credential = createNodeCredentialFromPassw({ passw: hashedPassw, rootKey });
669
+ await registerNodeOnServer(serverUrl, hashedPassw, credential.nodeId);
670
+ const created = {
671
+ serverUrl,
672
+ nodeId: credential.nodeId,
673
+ passw: generatedPassw
674
+ };
675
+ await saveCredentials(created);
676
+ return created;
677
+ }
678
+ async function createAgentNodeForInitialize(cred) {
679
+ const rootKey = loadRootKey(resolveAgentPlayRootPath({}));
680
+ const agentPassw = generateNodePassw();
681
+ const hashedAgentPassw = hashNodePassword(agentPassw);
682
+ const agentNodeId = deriveNodeIdFromPassword({
683
+ password: hashedAgentPassw,
684
+ rootKey
685
+ });
686
+ const res = await fetch(`${cred.serverUrl}/api/nodes/agent-node`, {
687
+ method: "POST",
688
+ headers: {
689
+ "content-type": "application/json",
690
+ ...nodeAuthHeaders(cred)
691
+ },
692
+ body: JSON.stringify({
693
+ kind: "agent",
694
+ parentNodeId: cred.nodeId,
695
+ agentNodeId,
696
+ agentNodePassw: hashedAgentPassw
697
+ })
698
+ });
699
+ const text = await res.text();
700
+ if (!res.ok) {
701
+ throw new Error(`Create failed (${String(res.status)}): ${text}`);
702
+ }
703
+ const json = JSON.parse(text);
704
+ if (typeof json.agentId !== "string" || json.agentId !== agentNodeId) {
705
+ throw new Error("Invalid agent creation response.");
706
+ }
707
+ const nextAgentNodes = [
708
+ ...(cred.agentNodes ?? []).filter((n) => n.nodeId !== agentNodeId),
709
+ {
710
+ nodeId: agentNodeId,
711
+ passw: agentPassw,
712
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
713
+ }
714
+ ];
715
+ await saveCredentials({
716
+ ...cred,
717
+ agentNodes: nextAgentNodes
718
+ });
719
+ return agentNodeId;
720
+ }
323
721
  async function cmdInspectNode() {
324
722
  const cred = await loadCredentials();
325
723
  if (cred === null) {
@@ -642,6 +1040,37 @@ async function cmdValidateAgentNode(argv) {
642
1040
  }
643
1041
  console.log(`Validated ${String(dedupedIds.length)} agent node(s) successfully.`);
644
1042
  }
1043
+ async function cmdInitialize2(argv) {
1044
+ const rl = createInterface({ input, output });
1045
+ try {
1046
+ await cmdInitialize({
1047
+ argv,
1048
+ promptApi: {
1049
+ askEnvironment: () => promptInitializeEnvironment(rl),
1050
+ askServerType: () => promptInitializeServerType(rl),
1051
+ askBootstrapNodes: () => promptInitializeBootstrapChoice(rl),
1052
+ askAgentCount: () => promptInitializeAgentCount(rl)
1053
+ },
1054
+ runtimeApi: {
1055
+ bootstrapNodeIds: async (options) => {
1056
+ const mainCred = await ensureMainCredentialsForInitialize(options.serverUrl);
1057
+ const agentNodeIds = [];
1058
+ for (let i = 0; i < options.agentCount; i++) {
1059
+ const refreshedCred = await loadCredentials() ?? mainCred;
1060
+ const nodeId = await createAgentNodeForInitialize(refreshedCred);
1061
+ agentNodeIds.push(nodeId);
1062
+ }
1063
+ return {
1064
+ mainNodeId: mainCred.nodeId,
1065
+ agentNodeIds
1066
+ };
1067
+ }
1068
+ }
1069
+ });
1070
+ } finally {
1071
+ rl.close();
1072
+ }
1073
+ }
645
1074
  async function main() {
646
1075
  const cmd = process.argv[2];
647
1076
  if (cmd === "bootstrap-node" || cmd === "create-main-node") {
@@ -680,6 +1109,10 @@ async function main() {
680
1109
  await cmdValidateAgentNode(process.argv.slice(3));
681
1110
  return;
682
1111
  }
1112
+ if (cmd === "initialize" || cmd === "init") {
1113
+ await cmdInitialize2(process.argv.slice(3));
1114
+ return;
1115
+ }
683
1116
  console.error(
684
1117
  [
685
1118
  "Usage:",
@@ -692,6 +1125,7 @@ async function main() {
692
1125
  " agent-play validate-main-node",
693
1126
  " agent-play validate-agent-node --all",
694
1127
  " agent-play validate-agent-node --agent-node-ids <id1,id2,...>",
1128
+ " agent-play initialize | init [--dir <path>] [--name <project-name>] [--template langchain] [--environment <development|test|production>] [--server-type <bare|express>] [--yes] [--force] [--bootstrap-nodes] [--agent-count <1|2>]",
695
1129
  " agent-play clear-node-credentials"
696
1130
  ].join("\n")
697
1131
  );
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agent-play/cli",
3
- "version": "3.1.0",
4
- "description": "Command-line tool for Agent Play: login, API keys, and agent registration against the web UI.",
3
+ "version": "3.2.0",
4
+ "description": "Command-line tool for Agent Play: node configurations, agent starter kit, and agent registration against the web UI.",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "bin": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "templates",
12
13
  "README.md"
13
14
  ],
14
15
  "repository": {
@@ -20,7 +21,9 @@
20
21
  "access": "public"
21
22
  },
22
23
  "scripts": {
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
+ "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 && cp README.md dist/README.md",
25
+ "test": "vitest run",
26
+ "smoke:initialize": "node ./scripts/smoke-initialize.mjs"
24
27
  },
25
28
  "dependencies": {
26
29
  "@agent-play/node-tools": "1.0.0"
@@ -28,6 +31,7 @@
28
31
  "devDependencies": {
29
32
  "@types/node": "^22.10.0",
30
33
  "tsup": "^8.5.1",
31
- "typescript": "~5.9.3"
34
+ "typescript": "~5.9.3",
35
+ "vitest": "^4.1.1"
32
36
  }
33
37
  }
@@ -0,0 +1,6 @@
1
+ AGENT_PLAY_WEB_UI_URL=https://agent-play.com
2
+ AGENT_PLAY_MAIN_NODE_ID=
3
+ AGENT_PLAY_AGENT_NODE_ID_1=
4
+ AGENT_PLAY_AGENT_NODE_ID_2=
5
+ OPENAI_API_KEY=
6
+ P2A_WEBRTC_ENABLED=1
@@ -0,0 +1,24 @@
1
+ # __PROJECT_NAME__
2
+
3
+ Starter scaffold generated by `agent-play initialize`.
4
+
5
+ ## Quick start
6
+
7
+ 1. Install dependencies:
8
+ - `npm install`
9
+ 2. Prepare local env:
10
+ - `cp .env.example .env`
11
+ 3. If you skipped bootstrap during initialize:
12
+ - `npx agent-play create-main-node`
13
+ - `npx agent-play create-agent-node` (once or twice)
14
+ - copy values from `~/.agent-play/credentials.json` into `.env`
15
+ 4. Run:
16
+ - `npm run dev`
17
+
18
+ ## Node identity env contract
19
+
20
+ - `AGENT_PLAY_MAIN_NODE_ID`: main developer node id.
21
+ - `AGENT_PLAY_AGENT_NODE_ID_1`: first agent node id.
22
+ - `AGENT_PLAY_AGENT_NODE_ID_2`: optional second agent node id.
23
+
24
+ The generated runtime uses env variables for node ids; no hardcoded identities are embedded.
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -p tsconfig.json",
8
+ "dev": "tsx src/index.ts",
9
+ "start": "node dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "@agent-play/sdk": "^3.2.1",
13
+ "@langchain/openai": "^1.3.0",
14
+ "dotenv": "^17.3.1",
15
+ "express": "^4.21.2",
16
+ "langchain": "^1.2.36",
17
+ "zod": "^3.24.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/express": "^4.17.21",
21
+ "@types/node": "^22.10.0",
22
+ "tsx": "^4.21.0",
23
+ "typescript": "~5.9.3"
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ import { loadEnv } from "./load-env.js";
2
+ import { registerAgents } from "./register-agents.js";
3
+
4
+ export async function startServer(): Promise<void> {
5
+ loadEnv();
6
+ const { world, registeredAgentIds } = await registerAgents();
7
+ console.log(`Registered ${String(registeredAgentIds.length)} agent(s):`);
8
+ for (const id of registeredAgentIds) {
9
+ console.log(` - ${id}`);
10
+ }
11
+ await world.hold().for(30 * 60);
12
+ }
@@ -0,0 +1,66 @@
1
+ import { ChatOpenAI } from "@langchain/openai";
2
+ import { createAgent } from "langchain";
3
+ import { calculatorTools, policeReportTools } from "./toolkits/starter-tools.js";
4
+
5
+ function randomAgentName(prefix: string): string {
6
+ return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
7
+ }
8
+
9
+ function randomSystemPrompt(prefix: string, rolePrompt: string): string {
10
+ const nonce = Math.random().toString(36).slice(2, 10);
11
+ return `${rolePrompt} Session nonce: ${nonce}. Agent label: ${prefix}.`;
12
+ }
13
+
14
+ function requiredEnv(name: string): string {
15
+ const value = process.env[name]?.trim();
16
+ if (value === undefined || value.length === 0) {
17
+ throw new Error(`Missing required env var: ${name}`);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ export type StarterAgentDefinition = {
23
+ nodeId: string;
24
+ name: string;
25
+ type: "langchain";
26
+ agent: ReturnType<typeof createAgent>;
27
+ };
28
+
29
+ export function getStarterAgentDefinitions(agentCount: 1 | 2): StarterAgentDefinition[] {
30
+ const model = new ChatOpenAI({
31
+ apiKey: requiredEnv("OPENAI_API_KEY"),
32
+ model: process.env.OPENAI_MODEL?.trim() || "gpt-4.1",
33
+ });
34
+ const first = {
35
+ nodeId: requiredEnv("AGENT_PLAY_AGENT_NODE_ID_1"),
36
+ name: randomAgentName("CalculatorAgent"),
37
+ type: "langchain" as const,
38
+ agent: createAgent({
39
+ name: randomAgentName("lc-calculator-agent"),
40
+ model,
41
+ tools: [...calculatorTools],
42
+ systemPrompt: randomSystemPrompt(
43
+ "Calculator Agent",
44
+ "You are a rigorous calculator agent for production operations. Always validate the equation format, show the extracted coefficient and brief reasoning, and ask clarifying questions when equation syntax is ambiguous. Use chat_tool for conversational explanations and assist_calculate_coefficient for structured extraction."
45
+ ),
46
+ }),
47
+ };
48
+ if (agentCount === 1) {
49
+ return [first];
50
+ }
51
+ const second = {
52
+ nodeId: requiredEnv("AGENT_PLAY_AGENT_NODE_ID_2"),
53
+ name: randomAgentName("PoliceReportAgent"),
54
+ type: "langchain" as const,
55
+ agent: createAgent({
56
+ name: randomAgentName("lc-police-report-agent"),
57
+ model,
58
+ tools: [...policeReportTools],
59
+ systemPrompt: randomSystemPrompt(
60
+ "Police Report Agent",
61
+ "You are a police report intake agent. Collect factual, time-stamped incident details, separate witness statements from assumptions, and maintain neutral language suitable for official records. Use chat_tool for guided conversation and assist_collect_scene_details to gather structured scene data."
62
+ ),
63
+ }),
64
+ };
65
+ return [first, second];
66
+ }
@@ -0,0 +1,84 @@
1
+ import { tool } from "langchain";
2
+ import { z } from "zod";
3
+
4
+ const calculatorChatTool = tool(
5
+ ({ message }: { message: string }) => `calculator:${message}`,
6
+ {
7
+ name: "chat_tool",
8
+ description: "Production-style calculator assistant chat tool.",
9
+ schema: z.object({
10
+ message: z.string(),
11
+ }),
12
+ }
13
+ );
14
+
15
+ const assistCalculateCoefficient = tool(
16
+ ({
17
+ equation,
18
+ variable,
19
+ }: {
20
+ equation: string;
21
+ variable: string;
22
+ }) => `coefficient:${equation}:${variable}`,
23
+ {
24
+ name: "assist_calculate_coefficient",
25
+ description:
26
+ "Extract the coefficient of a variable from a linear or polynomial equation string.",
27
+ schema: z.object({
28
+ equation: z.string(),
29
+ variable: z.string(),
30
+ }),
31
+ }
32
+ );
33
+
34
+ const policeChatTool = tool(
35
+ ({ message }: { message: string }) => `police:${message}`,
36
+ {
37
+ name: "chat_tool",
38
+ description: "Production-style police incident reporting chat tool.",
39
+ schema: z.object({
40
+ message: z.string(),
41
+ }),
42
+ }
43
+ );
44
+
45
+ const assistCollectSceneDetails = tool(
46
+ ({
47
+ location,
48
+ incidentType,
49
+ witnesses,
50
+ injuriesReported,
51
+ suspectDescription,
52
+ immediateRisk,
53
+ }: {
54
+ location: string;
55
+ incidentType: string;
56
+ witnesses?: string;
57
+ injuriesReported?: boolean;
58
+ suspectDescription?: string;
59
+ immediateRisk?: "low" | "medium" | "high";
60
+ }) =>
61
+ `scene:${location}:${incidentType}:${witnesses ?? ""}:${String(injuriesReported ?? false)}:${
62
+ suspectDescription ?? ""
63
+ }:${immediateRisk ?? "low"}`,
64
+ {
65
+ name: "assist_collect_scene_details",
66
+ description:
67
+ "Collect structured details from an incident scene report for police documentation.",
68
+ schema: z.object({
69
+ location: z.string(),
70
+ incidentType: z.string(),
71
+ witnesses: z.string().optional(),
72
+ injuriesReported: z.boolean().optional(),
73
+ suspectDescription: z.string().optional(),
74
+ immediateRisk: z.enum(["low", "medium", "high"]).optional(),
75
+ }),
76
+ }
77
+ );
78
+
79
+ export const calculatorTools = [
80
+ calculatorChatTool,
81
+ assistCalculateCoefficient,
82
+ ] as const;
83
+
84
+ export const policeReportTools = [policeChatTool, assistCollectSceneDetails] as const;
@@ -0,0 +1,27 @@
1
+ import express from "express";
2
+ import { loadEnv } from "./load-env.js";
3
+ import { registerAgents } from "./register-agents.js";
4
+
5
+ export async function startServer(): Promise<void> {
6
+ loadEnv();
7
+ const port = Number(process.env.PORT ?? "3100");
8
+ const host = process.env.HOST ?? "0.0.0.0";
9
+
10
+ const app = express();
11
+ app.use(express.json());
12
+ app.get("/health", (_req, res) => {
13
+ res.json({ ok: true });
14
+ });
15
+
16
+ const { world, registeredAgentIds } = await registerAgents();
17
+ console.log(`Registered ${String(registeredAgentIds.length)} agent(s):`);
18
+ for (const id of registeredAgentIds) {
19
+ console.log(` - ${id}`);
20
+ }
21
+
22
+ app.listen(port, host, () => {
23
+ console.log(`[starter-express] listening on http://${host}:${String(port)}`);
24
+ });
25
+
26
+ await world.hold().for(30 * 60);
27
+ }
@@ -0,0 +1,6 @@
1
+ import { startServer } from "__SERVER_MODULE__";
2
+
3
+ void startServer().catch((error: unknown) => {
4
+ console.error(error);
5
+ process.exitCode = 1;
6
+ });
@@ -0,0 +1,5 @@
1
+ import { config } from "dotenv";
2
+
3
+ export function loadEnv(): void {
4
+ config();
5
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ langchainRegistration,
3
+ RemotePlayWorld,
4
+ type RegisteredPlayer,
5
+ } from "@agent-play/sdk";
6
+ import { getStarterAgentDefinitions } from "../builtins/definitions.js";
7
+ import { executeToolCapability } from "../tool-handlers/execute-tool-capability.js";
8
+
9
+ type RegisterResult = {
10
+ world: RemotePlayWorld;
11
+ registeredAgentIds: string[];
12
+ };
13
+
14
+ function requiredEnv(name: string): string {
15
+ const value = process.env[name]?.trim();
16
+ if (value === undefined || value.length === 0) {
17
+ throw new Error(`Missing required env var: ${name}`);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ export async function registerBuiltinAgents(): Promise<RegisterResult> {
23
+ const mainNodeId = requiredEnv("AGENT_PLAY_MAIN_NODE_ID");
24
+ const world = new RemotePlayWorld({ logging: "on" });
25
+ const openAiApiKey = process.env.OPENAI_API_KEY?.trim();
26
+ if (openAiApiKey !== undefined && openAiApiKey.length > 0) {
27
+ world.initAudio({
28
+ openai: {
29
+ apiKey: openAiApiKey,
30
+ },
31
+ });
32
+ }
33
+ await world.connect({ mainNodeId });
34
+
35
+ const hasSecondAgent =
36
+ (process.env.AGENT_PLAY_AGENT_NODE_ID_2?.trim() ?? "").length > 0;
37
+ const definitions = getStarterAgentDefinitions(hasSecondAgent ? 2 : 1);
38
+ const registeredAgentIds: string[] = [];
39
+ const chatAgentsByPlayerId = new Map<string, unknown>();
40
+ const registeredPlayers: RegisteredPlayer[] = [];
41
+ for (const def of definitions) {
42
+ const registered = await world.addAgent({
43
+ name: def.name,
44
+ type: def.type,
45
+ agent: langchainRegistration(def.agent),
46
+ nodeId: def.nodeId,
47
+ enableP2a: "on",
48
+ });
49
+ registeredAgentIds.push(registered.id);
50
+ registeredPlayers.push(registered);
51
+ }
52
+ for (let i = 0; i < registeredPlayers.length; i++) {
53
+ chatAgentsByPlayerId.set(registeredPlayers[i].id, definitions[i].agent);
54
+ }
55
+ world.subscribeIntercomCommands({
56
+ playerIds: registeredPlayers.map((player) => player.id),
57
+ executeTool: executeToolCapability,
58
+ chatAgentsByPlayerId: chatAgentsByPlayerId as Map<string, never>,
59
+ });
60
+
61
+ return { world, registeredAgentIds };
62
+ }
@@ -0,0 +1 @@
1
+ export { registerBuiltinAgents as registerAgents } from "./register/register-builtins.js";
@@ -0,0 +1,8 @@
1
+ export function executeAssistBrainstorm(args: {
2
+ prompt?: string;
3
+ }): Record<string, unknown> {
4
+ const prompt = typeof args.prompt === "string" ? args.prompt : "";
5
+ return {
6
+ summary: `Brainstorm starter for: ${prompt}`,
7
+ };
8
+ }
@@ -0,0 +1,35 @@
1
+ function normalizeEquation(equation: string): string {
2
+ return equation.replace(/\s+/g, "");
3
+ }
4
+
5
+ export function executeAssistCalculateCoefficient(args: {
6
+ equation?: string;
7
+ variable?: string;
8
+ }): Record<string, unknown> {
9
+ const equation = typeof args.equation === "string" ? args.equation : "";
10
+ const variable = typeof args.variable === "string" ? args.variable : "";
11
+ if (equation.length === 0 || variable.length === 0) {
12
+ return {
13
+ ok: false,
14
+ error: "equation and variable are required.",
15
+ };
16
+ }
17
+ const normalized = normalizeEquation(equation);
18
+ const regex = new RegExp(`([+-]?\\d*\\.?\\d*)${variable}`);
19
+ const match = normalized.match(regex);
20
+ if (match === null) {
21
+ return {
22
+ ok: false,
23
+ error: `No coefficient found for variable ${variable}.`,
24
+ };
25
+ }
26
+ const raw = match[1];
27
+ const coefficient =
28
+ raw === "" || raw === "+" ? 1 : raw === "-" ? -1 : Number(raw);
29
+ return {
30
+ ok: Number.isFinite(coefficient),
31
+ variable,
32
+ coefficient,
33
+ equation,
34
+ };
35
+ }
@@ -0,0 +1,32 @@
1
+ export function executeAssistCollectSceneDetails(args: {
2
+ location?: string;
3
+ incidentType?: string;
4
+ witnesses?: string;
5
+ injuriesReported?: boolean;
6
+ suspectDescription?: string;
7
+ immediateRisk?: "low" | "medium" | "high";
8
+ }): Record<string, unknown> {
9
+ const location = typeof args.location === "string" ? args.location : "";
10
+ const incidentType =
11
+ typeof args.incidentType === "string" ? args.incidentType : "";
12
+ if (location.length === 0 || incidentType.length === 0) {
13
+ return {
14
+ ok: false,
15
+ error: "location and incidentType are required.",
16
+ };
17
+ }
18
+ return {
19
+ ok: true,
20
+ scene: {
21
+ location,
22
+ incidentType,
23
+ witnesses: typeof args.witnesses === "string" ? args.witnesses : "",
24
+ injuriesReported: args.injuriesReported === true,
25
+ suspectDescription:
26
+ typeof args.suspectDescription === "string"
27
+ ? args.suspectDescription
28
+ : "",
29
+ immediateRisk: args.immediateRisk ?? "low",
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,6 @@
1
+ export function executeChatTool(args: { message?: string }): Record<string, unknown> {
2
+ const message = typeof args.message === "string" ? args.message : "";
3
+ return {
4
+ reply: `Echo from __PROJECT_NAME__: ${message}`,
5
+ };
6
+ }
@@ -0,0 +1,12 @@
1
+ import { resolveToolCapabilityHandler } from "./tool-capability-registry.js";
2
+
3
+ export async function executeToolCapability(options: {
4
+ toolName: string;
5
+ args: Record<string, unknown>;
6
+ }): Promise<Record<string, unknown>> {
7
+ const handler = resolveToolCapabilityHandler(options.toolName);
8
+ if (handler === null) {
9
+ throw new Error(`unknown tool capability: ${options.toolName}`);
10
+ }
11
+ return await Promise.resolve(handler(options.args));
12
+ }
@@ -0,0 +1,49 @@
1
+ import { executeAssistCalculateCoefficient } from "./assist-calculate-coefficient.js";
2
+ import { executeAssistCollectSceneDetails } from "./assist-collect-scene-details.js";
3
+ import { executeChatTool } from "./chat-tool.js";
4
+
5
+ export type ToolCapabilityHandler = (
6
+ args: Record<string, unknown>
7
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
8
+
9
+ const registry = new Map<string, ToolCapabilityHandler>([
10
+ ["chat_tool", (args) => executeChatTool({ message: String(args.message ?? "") })],
11
+ [
12
+ "assist_calculate_coefficient",
13
+ (args) =>
14
+ executeAssistCalculateCoefficient({
15
+ equation: String(args.equation ?? ""),
16
+ variable: String(args.variable ?? ""),
17
+ }),
18
+ ],
19
+ [
20
+ "assist_collect_scene_details",
21
+ (args) =>
22
+ executeAssistCollectSceneDetails({
23
+ location: String(args.location ?? ""),
24
+ incidentType: String(args.incidentType ?? ""),
25
+ witnesses:
26
+ typeof args.witnesses === "string" ? args.witnesses : undefined,
27
+ injuriesReported:
28
+ typeof args.injuriesReported === "boolean"
29
+ ? args.injuriesReported
30
+ : undefined,
31
+ suspectDescription:
32
+ typeof args.suspectDescription === "string"
33
+ ? args.suspectDescription
34
+ : undefined,
35
+ immediateRisk:
36
+ args.immediateRisk === "low" ||
37
+ args.immediateRisk === "medium" ||
38
+ args.immediateRisk === "high"
39
+ ? args.immediateRisk
40
+ : undefined,
41
+ }),
42
+ ],
43
+ ]);
44
+
45
+ export function resolveToolCapabilityHandler(
46
+ toolName: string
47
+ ): ToolCapabilityHandler | null {
48
+ return registry.get(toolName) ?? null;
49
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "noEmit": false,
10
+ "outDir": "dist"
11
+ },
12
+ "include": ["src/**/*.ts"]
13
+ }