@agentprojectcontext/apx 1.10.4 → 1.12.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.
@@ -0,0 +1,729 @@
1
+ // daemon/tools/registry.js
2
+ // Tool Registry on-demand for APX.
3
+ //
4
+ // Endpoints registered by api.js:
5
+ // GET /tools → lightweight list [{name, description, category, schema_url}]
6
+ // GET /tools/:name → full schema + examples
7
+ // POST /tools/:name/call → execute the tool (proxy to internal handler)
8
+ //
9
+ // Tools that already exist as HTTP endpoints are listed here with their
10
+ // endpoint targets — no code duplication.
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Tool definitions
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const TOOL_DEFINITIONS = [
17
+ // ── file ──────────────────────────────────────────────────────────────────
18
+ {
19
+ name: "read_file",
20
+ category: "file",
21
+ description: "Read the contents of a file inside the project.",
22
+ endpoint: { method: "GET", path: "/files", query: ["path", "project"] },
23
+ parameters: {
24
+ type: "object",
25
+ properties: {
26
+ path: { type: "string", description: "Relative path inside the project" },
27
+ project: { type: "string", description: "Project ID or path (optional)" },
28
+ },
29
+ required: ["path"],
30
+ },
31
+ examples: [{ path: "src/index.js" }],
32
+ },
33
+ {
34
+ name: "write_file",
35
+ category: "file",
36
+ description: "Write or overwrite a file inside the project.",
37
+ endpoint: { method: "POST", path: "/files" },
38
+ parameters: {
39
+ type: "object",
40
+ properties: {
41
+ path: { type: "string" },
42
+ content: { type: "string" },
43
+ project: { type: "string" },
44
+ },
45
+ required: ["path", "content"],
46
+ },
47
+ examples: [{ path: "notes.md", content: "# Hello" }],
48
+ },
49
+ {
50
+ name: "list_files",
51
+ category: "file",
52
+ description: "List files and directories inside a project path.",
53
+ endpoint: { method: "GET", path: "/files" },
54
+ parameters: {
55
+ type: "object",
56
+ properties: {
57
+ path: { type: "string", description: "Sub-path to list (optional)" },
58
+ project: { type: "string" },
59
+ },
60
+ },
61
+ examples: [{ path: "src" }],
62
+ },
63
+ {
64
+ name: "search_files",
65
+ category: "file",
66
+ description: "Search for files by name glob or content pattern in the project.",
67
+ endpoint: { method: "GET", path: "/files/search" },
68
+ parameters: {
69
+ type: "object",
70
+ properties: {
71
+ q: { type: "string", description: "Search query (filename or content)" },
72
+ project: { type: "string" },
73
+ },
74
+ required: ["q"],
75
+ },
76
+ examples: [{ q: "*.config.js" }],
77
+ },
78
+
79
+ // ── shell ─────────────────────────────────────────────────────────────────
80
+ {
81
+ name: "run_command",
82
+ category: "shell",
83
+ description: "Execute a shell command in the project directory. Returns stdout, stderr, exit_code.",
84
+ endpoint: { method: "POST", path: "/run" },
85
+ parameters: {
86
+ type: "object",
87
+ properties: {
88
+ cmd: { type: "string", description: "Shell command to run" },
89
+ cwd: { type: "string", description: "Working directory override" },
90
+ project: { type: "string" },
91
+ timeout_ms: { type: "integer", default: 30000 },
92
+ },
93
+ required: ["cmd"],
94
+ },
95
+ examples: [{ cmd: "ls -la" }, { cmd: "git log --oneline -5" }],
96
+ },
97
+
98
+ // ── memory ────────────────────────────────────────────────────────────────
99
+ {
100
+ name: "memory_get",
101
+ category: "memory",
102
+ description: "Read the memory.md of the default agent in a project.",
103
+ endpoint: { method: "GET", path: "/memory" },
104
+ parameters: {
105
+ type: "object",
106
+ properties: {
107
+ project: { type: "string" },
108
+ },
109
+ },
110
+ examples: [{}],
111
+ },
112
+ {
113
+ name: "memory_set",
114
+ category: "memory",
115
+ description: "Overwrite the memory.md of the default agent in a project.",
116
+ endpoint: { method: "POST", path: "/memory" },
117
+ parameters: {
118
+ type: "object",
119
+ properties: {
120
+ body: { type: "string", description: "Full content to write" },
121
+ project: { type: "string" },
122
+ },
123
+ required: ["body"],
124
+ },
125
+ examples: [{ body: "# Agent Memory\n\n- Remember to greet the user." }],
126
+ },
127
+ {
128
+ name: "memory_append",
129
+ category: "memory",
130
+ description: "Append text to the agent memory.md (read-modify-write).",
131
+ endpoint: null, // implemented inline in the call handler
132
+ parameters: {
133
+ type: "object",
134
+ properties: {
135
+ text: { type: "string" },
136
+ project: { type: "string" },
137
+ },
138
+ required: ["text"],
139
+ },
140
+ examples: [{ text: "\n- New fact to remember." }],
141
+ },
142
+ {
143
+ name: "memory_list",
144
+ category: "memory",
145
+ description: "List all agents that have memory files in a project.",
146
+ endpoint: null,
147
+ parameters: {
148
+ type: "object",
149
+ properties: { project: { type: "string" } },
150
+ },
151
+ examples: [{}],
152
+ },
153
+
154
+ // ── session ───────────────────────────────────────────────────────────────
155
+ {
156
+ name: "session_list",
157
+ category: "session",
158
+ description: "List sessions for an agent in a project.",
159
+ endpoint: { method: "GET", path: "/projects/:pid/agents/:slug/sessions" },
160
+ parameters: {
161
+ type: "object",
162
+ properties: {
163
+ project: { type: "string", description: "Project ID" },
164
+ agent: { type: "string", description: "Agent slug" },
165
+ },
166
+ required: ["project", "agent"],
167
+ },
168
+ examples: [{ project: "1", agent: "sofia" }],
169
+ },
170
+ {
171
+ name: "session_get",
172
+ category: "session",
173
+ description: "Get a session by filename.",
174
+ endpoint: { method: "GET", path: "/projects/:pid/sessions/:sid" },
175
+ parameters: {
176
+ type: "object",
177
+ properties: {
178
+ project: { type: "string" },
179
+ session_id: { type: "string" },
180
+ },
181
+ required: ["project", "session_id"],
182
+ },
183
+ examples: [{ project: "1", session_id: "2026-05-01-planning.md" }],
184
+ },
185
+ {
186
+ name: "session_search",
187
+ category: "session",
188
+ description: "Search session content by text query across all agents in a project.",
189
+ endpoint: { method: "GET", path: "/sessions/search" },
190
+ parameters: {
191
+ type: "object",
192
+ properties: {
193
+ q: { type: "string", description: "Search query" },
194
+ project: { type: "string" },
195
+ limit: { type: "integer", default: 20 },
196
+ },
197
+ required: ["q"],
198
+ },
199
+ examples: [{ q: "authentication bug" }],
200
+ },
201
+ {
202
+ name: "session_compact",
203
+ category: "session",
204
+ description: "Compact (summarise and compress) a session conversation.",
205
+ endpoint: { method: "POST", path: "/sessions/:id/compact" },
206
+ parameters: {
207
+ type: "object",
208
+ properties: {
209
+ session_id: { type: "string" },
210
+ project: { type: "string" },
211
+ agent: { type: "string" },
212
+ model: { type: "string" },
213
+ },
214
+ required: ["project", "agent", "session_id"],
215
+ },
216
+ examples: [{ project: "1", agent: "sofia", session_id: "2026-05-01-planning.md" }],
217
+ },
218
+
219
+ // ── mcp ───────────────────────────────────────────────────────────────────
220
+ {
221
+ name: "mcp_list",
222
+ category: "mcp",
223
+ description: "List all MCP servers registered in a project.",
224
+ endpoint: { method: "GET", path: "/mcp" },
225
+ parameters: {
226
+ type: "object",
227
+ properties: { project: { type: "string" } },
228
+ },
229
+ examples: [{}],
230
+ },
231
+ {
232
+ name: "mcp_run",
233
+ category: "mcp",
234
+ description: "Call a tool on an MCP server.",
235
+ endpoint: { method: "POST", path: "/mcp/run" },
236
+ parameters: {
237
+ type: "object",
238
+ properties: {
239
+ name: { type: "string", description: "MCP server name" },
240
+ tool: { type: "string", description: "Tool name on that server" },
241
+ params: { type: "object" },
242
+ project: { type: "string" },
243
+ },
244
+ required: ["name", "tool"],
245
+ },
246
+ examples: [{ name: "filesystem", tool: "list_directory", params: { path: "/tmp" } }],
247
+ },
248
+
249
+ // ── glob / grep ───────────────────────────────────────────────────────────
250
+ {
251
+ name: "glob",
252
+ category: "file",
253
+ description: "List files matching a glob pattern (e.g. **/*.js). Uses native Node.js glob.",
254
+ endpoint: { method: "POST", path: "/tools/glob" },
255
+ parameters: {
256
+ type: "object",
257
+ properties: {
258
+ pattern: { type: "string", description: "Glob pattern, e.g. src/**/*.ts" },
259
+ cwd: { type: "string", description: "Base directory (absolute path)" },
260
+ dot: { type: "boolean", default: false, description: "Include dotfiles" },
261
+ absolute: { type: "boolean", default: false },
262
+ limit: { type: "integer", default: 500 },
263
+ },
264
+ required: ["pattern"],
265
+ },
266
+ examples: [
267
+ { pattern: "**/*.js", cwd: "/my/project" },
268
+ { pattern: "src/**/*.ts", cwd: "/my/project", limit: 100 },
269
+ ],
270
+ },
271
+ {
272
+ name: "grep",
273
+ category: "file",
274
+ description: "Search file contents by regex pattern. Uses ripgrep when available, pure Node.js fallback.",
275
+ endpoint: { method: "POST", path: "/tools/grep" },
276
+ parameters: {
277
+ type: "object",
278
+ properties: {
279
+ pattern: { type: "string", description: "Regex to search for" },
280
+ path: { type: "string", description: "Directory or file to search in" },
281
+ glob: { type: "string", description: "Glob filter for files, e.g. *.ts" },
282
+ case_sensitive: { type: "boolean", default: false },
283
+ context: { type: "integer", default: 0, description: "Lines of context around matches" },
284
+ limit: { type: "integer", default: 100 },
285
+ },
286
+ required: ["pattern"],
287
+ },
288
+ examples: [
289
+ { pattern: "export default", path: "/my/project/src", glob: "*.js" },
290
+ { pattern: "TODO|FIXME", path: "/my/project", context: 2 },
291
+ ],
292
+ },
293
+
294
+ // ── fetch (native HTTP, no browser) ───────────────────────────────────────
295
+ {
296
+ name: "http_get",
297
+ category: "fetch",
298
+ description: "Native HTTP GET — fast, no headless browser. Use for REST APIs, raw HTML, JSON endpoints.",
299
+ endpoint: { method: "POST", path: "/tools/fetch/get" },
300
+ parameters: {
301
+ type: "object",
302
+ properties: {
303
+ url: { type: "string" },
304
+ headers: { type: "object" },
305
+ timeout_ms: { type: "number", default: 30000 },
306
+ },
307
+ required: ["url"],
308
+ },
309
+ examples: [{ url: "https://api.github.com/repos/anthropics/anthropic-sdk-typescript" }],
310
+ },
311
+ {
312
+ name: "http_post",
313
+ category: "fetch",
314
+ description: "Native HTTP POST — sends body as JSON when body is an object. Use for REST APIs.",
315
+ endpoint: { method: "POST", path: "/tools/fetch/post" },
316
+ parameters: {
317
+ type: "object",
318
+ properties: {
319
+ url: { type: "string" },
320
+ body: { description: "Object → JSON-stringified. String → sent as-is." },
321
+ headers: { type: "object" },
322
+ timeout_ms: { type: "number", default: 30000 },
323
+ json: { type: "boolean", description: "Force JSON parsing of response body." },
324
+ },
325
+ required: ["url"],
326
+ },
327
+ examples: [{ url: "https://api.example.com/items", body: { name: "foo" } }],
328
+ },
329
+ {
330
+ name: "http_request",
331
+ category: "fetch",
332
+ description: "Generic HTTP request with full control over method, headers, body, timeout.",
333
+ endpoint: { method: "POST", path: "/tools/fetch/request" },
334
+ parameters: {
335
+ type: "object",
336
+ properties: {
337
+ url: { type: "string" },
338
+ method: { type: "string", default: "GET" },
339
+ headers: { type: "object" },
340
+ body: {},
341
+ timeout_ms: { type: "number", default: 30000 },
342
+ json: { type: "boolean" },
343
+ },
344
+ required: ["url"],
345
+ },
346
+ examples: [{ url: "https://api.example.com/x", method: "DELETE" }],
347
+ },
348
+
349
+ // ── browser (Puppeteer-backed — heavier, launches Chromium lazily) ────────
350
+ {
351
+ name: "browser_navigate",
352
+ category: "browser",
353
+ description: "Navigate the headless browser to a URL. Launches Chromium lazily on first call.",
354
+ endpoint: { method: "POST", path: "/tools/browser/navigate" },
355
+ parameters: {
356
+ type: "object",
357
+ properties: {
358
+ url: { type: "string" },
359
+ launch_options: { type: "object", description: "Puppeteer launch overrides (headless, args, defaultViewport, etc.)." },
360
+ allow_dangerous: { type: "boolean", description: "Allow dangerous launch args (--no-sandbox, --single-process, etc.)." },
361
+ },
362
+ required: ["url"],
363
+ },
364
+ examples: [{ url: "https://example.com" }],
365
+ },
366
+ {
367
+ name: "browser_screenshot",
368
+ category: "browser",
369
+ description: "Take a screenshot of the current browser page (or a single element via selector). Returns base64 PNG.",
370
+ endpoint: { method: "POST", path: "/tools/browser/screenshot" },
371
+ parameters: {
372
+ type: "object",
373
+ properties: {
374
+ selector: { type: "string", description: "CSS selector of element to capture. Omit to capture full viewport/page." },
375
+ full_page: { type: "boolean", default: false },
376
+ width: { type: "number", description: "Viewport width (capped at 1920)." },
377
+ height: { type: "number", description: "Viewport height (capped at 1080)." },
378
+ encoded: { type: "boolean", description: "Also return a data: URI." },
379
+ },
380
+ },
381
+ examples: [{}, { selector: "#hero" }],
382
+ },
383
+ {
384
+ name: "browser_click",
385
+ category: "browser",
386
+ description: "Click a CSS selector on the current browser page.",
387
+ endpoint: { method: "POST", path: "/tools/browser/click" },
388
+ parameters: {
389
+ type: "object",
390
+ properties: { selector: { type: "string" } },
391
+ required: ["selector"],
392
+ },
393
+ examples: [{ selector: "button#submit" }],
394
+ },
395
+ {
396
+ name: "browser_type",
397
+ category: "browser",
398
+ description: "Type text into a CSS selector. Uses focus + Ctrl+A + Backspace to clear, then types with realistic delay.",
399
+ endpoint: { method: "POST", path: "/tools/browser/type" },
400
+ parameters: {
401
+ type: "object",
402
+ properties: {
403
+ selector: { type: "string" },
404
+ text: { type: "string" },
405
+ clear: { type: "boolean", default: true },
406
+ },
407
+ required: ["selector", "text"],
408
+ },
409
+ examples: [{ selector: "input#search", text: "hello world" }],
410
+ },
411
+ {
412
+ name: "browser_select",
413
+ category: "browser",
414
+ description: "Choose an option in a <select> element by its value.",
415
+ endpoint: { method: "POST", path: "/tools/browser/select" },
416
+ parameters: {
417
+ type: "object",
418
+ properties: {
419
+ selector: { type: "string" },
420
+ value: { type: "string" },
421
+ },
422
+ required: ["selector", "value"],
423
+ },
424
+ examples: [{ selector: "select#country", value: "AR" }],
425
+ },
426
+ {
427
+ name: "browser_hover",
428
+ category: "browser",
429
+ description: "Hover the cursor over an element (triggers tooltips, dropdowns, hover states).",
430
+ endpoint: { method: "POST", path: "/tools/browser/hover" },
431
+ parameters: {
432
+ type: "object",
433
+ properties: { selector: { type: "string" } },
434
+ required: ["selector"],
435
+ },
436
+ examples: [{ selector: "nav .menu-item" }],
437
+ },
438
+ {
439
+ name: "browser_evaluate",
440
+ category: "browser",
441
+ description: "Execute JavaScript in the page context. Captures the script's console.log/info/warn/error output and returns it alongside the result.",
442
+ endpoint: { method: "POST", path: "/tools/browser/evaluate" },
443
+ parameters: {
444
+ type: "object",
445
+ properties: { code: { type: "string", description: "JS code to eval (function body)." } },
446
+ required: ["code"],
447
+ },
448
+ examples: [{ code: "return document.title;" }],
449
+ },
450
+ {
451
+ name: "browser_get_text",
452
+ category: "browser",
453
+ description: "Extract readable text from the current page (or a single element). Strips script/style/nav/header/footer.",
454
+ endpoint: { method: "POST", path: "/tools/browser/get_text" },
455
+ parameters: {
456
+ type: "object",
457
+ properties: { selector: { type: "string", description: "Optional CSS selector." } },
458
+ },
459
+ examples: [{}, { selector: "article" }],
460
+ },
461
+ {
462
+ name: "browser_get_content",
463
+ category: "browser",
464
+ description: "Return raw innerHTML of the page or a single element (truncated at 1MB).",
465
+ endpoint: { method: "POST", path: "/tools/browser/get_content" },
466
+ parameters: {
467
+ type: "object",
468
+ properties: { selector: { type: "string" } },
469
+ },
470
+ examples: [{}, { selector: "main" }],
471
+ },
472
+ {
473
+ name: "browser_wait_for_selector",
474
+ category: "browser",
475
+ description: "Wait until a CSS selector appears on the page.",
476
+ endpoint: { method: "POST", path: "/tools/browser/wait_for_selector" },
477
+ parameters: {
478
+ type: "object",
479
+ properties: {
480
+ selector: { type: "string" },
481
+ timeout: { type: "number", default: 30000 },
482
+ },
483
+ required: ["selector"],
484
+ },
485
+ examples: [{ selector: ".results-loaded" }],
486
+ },
487
+ {
488
+ name: "browser_close",
489
+ category: "browser",
490
+ description: "Close the headless browser and free resources.",
491
+ endpoint: { method: "POST", path: "/tools/browser/close" },
492
+ parameters: { type: "object", properties: {} },
493
+ examples: [{}],
494
+ },
495
+
496
+ // ── search ────────────────────────────────────────────────────────────────
497
+ {
498
+ name: "web_search",
499
+ category: "search",
500
+ description: "Search the web. Modes: auto (tries DDG → Brave → Browser), ddg, brave, browser.",
501
+ endpoint: { method: "POST", path: "/tools/search" },
502
+ parameters: {
503
+ type: "object",
504
+ properties: {
505
+ query: { type: "string" },
506
+ mode: { type: "string", enum: ["auto", "ddg", "brave", "browser"], default: "auto" },
507
+ limit: { type: "integer", default: 5 },
508
+ },
509
+ required: ["query"],
510
+ },
511
+ examples: [
512
+ { query: "APC agent project context standard" },
513
+ { query: "site:github.com puppeteer examples", mode: "ddg" },
514
+ ],
515
+ },
516
+
517
+ // ── agents ────────────────────────────────────────────────────────────────
518
+ {
519
+ name: "agent_list",
520
+ category: "agents",
521
+ description: "List all agents in a project.",
522
+ endpoint: { method: "GET", path: "/projects/:pid/agents" },
523
+ parameters: {
524
+ type: "object",
525
+ properties: { project: { type: "string" } },
526
+ required: ["project"],
527
+ },
528
+ examples: [{ project: "1" }],
529
+ },
530
+ {
531
+ name: "agent_get",
532
+ category: "agents",
533
+ description: "Get details + memory for a specific agent.",
534
+ endpoint: { method: "GET", path: "/projects/:pid/agents/:slug" },
535
+ parameters: {
536
+ type: "object",
537
+ properties: {
538
+ project: { type: "string" },
539
+ agent: { type: "string" },
540
+ },
541
+ required: ["project", "agent"],
542
+ },
543
+ examples: [{ project: "1", agent: "sofia" }],
544
+ },
545
+
546
+ // ── project ───────────────────────────────────────────────────────────────
547
+ {
548
+ name: "project_info",
549
+ category: "project",
550
+ description: "List all registered projects and their metadata.",
551
+ endpoint: { method: "GET", path: "/projects" },
552
+ parameters: { type: "object", properties: {} },
553
+ examples: [{}],
554
+ },
555
+ ];
556
+
557
+ // ---------------------------------------------------------------------------
558
+ // Index for fast lookup
559
+ // ---------------------------------------------------------------------------
560
+
561
+ const TOOL_MAP = new Map(TOOL_DEFINITIONS.map((t) => [t.name, t]));
562
+
563
+ function listTools() {
564
+ return TOOL_DEFINITIONS.map(({ name, description, category, endpoint }) => ({
565
+ name,
566
+ description,
567
+ category,
568
+ schema_url: `/tools/${name}`,
569
+ endpoint_method: endpoint?.method || "inline",
570
+ endpoint_path: endpoint?.path || null,
571
+ }));
572
+ }
573
+
574
+ function getTool(name) {
575
+ const t = TOOL_MAP.get(name);
576
+ if (!t) return null;
577
+ return {
578
+ name: t.name,
579
+ description: t.description,
580
+ category: t.category,
581
+ parameters: t.parameters,
582
+ examples: t.examples || [],
583
+ endpoint: t.endpoint || null,
584
+ schema_url: `/tools/${name}`,
585
+ };
586
+ }
587
+
588
+ // ---------------------------------------------------------------------------
589
+ // Inline call handlers for tools without a dedicated HTTP endpoint
590
+ // ---------------------------------------------------------------------------
591
+
592
+ function makeInlineHandlers({ projects, registries }) {
593
+ return {
594
+ memory_append: async (body) => {
595
+ const { default: fetch } = await import("node-fetch");
596
+ const base = `http://localhost:${process.env.APX_PORT || 7430}`;
597
+ // GET current
598
+ const getRes = await fetch(`${base}/memory${body.project ? `?project=${body.project}` : ""}`);
599
+ if (!getRes.ok) throw new Error(`memory_get failed: ${getRes.status}`);
600
+ const { body: current } = await getRes.json();
601
+ // POST updated
602
+ const text = body.text || "";
603
+ const postRes = await fetch(`${base}/memory${body.project ? `?project=${body.project}` : ""}`, {
604
+ method: "POST",
605
+ headers: { "content-type": "application/json" },
606
+ body: JSON.stringify({ body: current + text }),
607
+ });
608
+ if (!postRes.ok) throw new Error(`memory_set failed: ${postRes.status}`);
609
+ return { ok: true, appended_chars: text.length };
610
+ },
611
+
612
+ memory_list: async (body) => {
613
+ const { default: fs } = await import("node:fs");
614
+ const { default: path } = await import("node:path");
615
+ // Find the project
616
+ const all = projects.list();
617
+ let p = null;
618
+ if (body.project) {
619
+ const ref = String(body.project);
620
+ const found = all.find((x) => String(x.id) === ref || x.path === ref);
621
+ p = found ? projects.get(found.id) : null;
622
+ }
623
+ if (!p) p = projects.get(all.filter((x) => x.id !== 0)[0]?.id) || projects.get(0);
624
+ if (!p) throw new Error("no project registered");
625
+ const agentsDir = path.join(p.path, ".apc", "agents");
626
+ if (!fs.existsSync(agentsDir)) return { agents_with_memory: [] };
627
+ const result = fs.readdirSync(agentsDir).filter((slug) => {
628
+ return fs.existsSync(path.join(agentsDir, slug, "memory.md"));
629
+ }).map((slug) => {
630
+ const memPath = path.join(agentsDir, slug, "memory.md");
631
+ const stat = fs.statSync(memPath);
632
+ return { agent: slug, path: memPath, size: stat.size, mtime: stat.mtime };
633
+ });
634
+ return { project: p.path, agents_with_memory: result };
635
+ },
636
+ };
637
+ }
638
+
639
+ // ---------------------------------------------------------------------------
640
+ // Express router factory
641
+ // ---------------------------------------------------------------------------
642
+
643
+ export function buildRegistryRouter(express, ctx) {
644
+ const { projects, registries } = ctx;
645
+ const router = express.Router();
646
+ const inlineHandlers = makeInlineHandlers({ projects, registries });
647
+
648
+ // GET /tools — lightweight list
649
+ router.get("/", (_req, res) => {
650
+ res.json(listTools());
651
+ });
652
+
653
+ // GET /tools/:name — full schema
654
+ router.get("/:name", (req, res) => {
655
+ const tool = getTool(req.params.name);
656
+ if (!tool) return res.status(404).json({ error: `tool "${req.params.name}" not found` });
657
+ res.json(tool);
658
+ });
659
+
660
+ // POST /tools/:name/call — execute tool
661
+ router.post("/:name/call", async (req, res) => {
662
+ const { name } = req.params;
663
+ const toolDef = TOOL_MAP.get(name);
664
+ if (!toolDef) return res.status(404).json({ error: `tool "${name}" not found` });
665
+
666
+ const body = req.body || {};
667
+
668
+ // If there's an inline handler, use it
669
+ if (inlineHandlers[name]) {
670
+ try {
671
+ const result = await inlineHandlers[name](body);
672
+ return res.json({ tool: name, result });
673
+ } catch (e) {
674
+ return res.status(500).json({ error: e.message });
675
+ }
676
+ }
677
+
678
+ // Otherwise proxy to the HTTP endpoint
679
+ if (!toolDef.endpoint) {
680
+ return res.status(501).json({ error: `tool "${name}" has no endpoint and no inline handler` });
681
+ }
682
+
683
+ try {
684
+ const { default: fetch } = await import("node-fetch");
685
+ const port = process.env.APX_PORT || 7430;
686
+ const base = `http://localhost:${port}`;
687
+
688
+ let urlPath = toolDef.endpoint.path;
689
+ // Replace :pid / :slug / :name params from body if present
690
+ urlPath = urlPath
691
+ .replace(":pid", body.project || "0")
692
+ .replace(":slug", body.agent || body.slug || "")
693
+ .replace(":sid", body.session_id || "")
694
+ .replace(":id", body.session_id || "")
695
+ .replace(":name", body.name || "");
696
+
697
+ const method = toolDef.endpoint.method || "GET";
698
+ let fetchUrl = `${base}${urlPath}`;
699
+
700
+ let fetchOpts = { method, headers: { "content-type": "application/json" } };
701
+
702
+ if (method === "GET") {
703
+ // Append body fields as query params
704
+ const qs = new URLSearchParams();
705
+ for (const [k, v] of Object.entries(body)) {
706
+ if (v !== undefined && v !== null) qs.set(k, String(v));
707
+ }
708
+ const qstr = qs.toString();
709
+ if (qstr) fetchUrl += `?${qstr}`;
710
+ } else {
711
+ fetchOpts.body = JSON.stringify(body);
712
+ }
713
+
714
+ const r = await fetch(fetchUrl, fetchOpts);
715
+ const text = await r.text();
716
+ let data;
717
+ try { data = JSON.parse(text); } catch { data = { raw: text }; }
718
+
719
+ if (!r.ok) return res.status(r.status).json({ error: data?.error || text });
720
+ res.json({ tool: name, result: data });
721
+ } catch (e) {
722
+ res.status(500).json({ error: e.message });
723
+ }
724
+ });
725
+
726
+ return router;
727
+ }
728
+
729
+ export { listTools, getTool, TOOL_DEFINITIONS };