godmode 0.0.2 → 0.0.3

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/dist/index.js CHANGED
@@ -1,25 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- execute
4
- } from "./chunk-GJCJO7YT.js";
3
+ showApiHelp,
4
+ showExtensionOverview,
5
+ showExtensionVersion,
6
+ showHelp,
7
+ showVersion
8
+ } from "./chunk-ZTPILA7B.js";
9
+ import {
10
+ checkPermission,
11
+ matchRoute,
12
+ resourceFromRawPath,
13
+ resourceFromSegments,
14
+ resourceFromTool,
15
+ suggestRoutes,
16
+ suggestedAllowRule
17
+ } from "./chunk-4EU5JX6C.js";
18
+ import {
19
+ EXIT_CODES
20
+ } from "./chunk-XFFBFBN6.js";
21
+ import {
22
+ warnSettingsErrors
23
+ } from "./chunk-SMQBMG24.js";
5
24
  import {
6
- GODMODE_EXTENSIONS_DIR,
25
+ BUILTINS,
7
26
  GODMODE_HOME,
8
- listApis,
27
+ findInstalledManifestSync,
9
28
  loadManifest,
10
29
  loadMultiManifest,
11
- removeApi,
12
- updateApi,
13
30
  validateGraphQLFlags
14
- } from "./chunk-2J2VDCQ4.js";
31
+ } from "./chunk-KHFZI7IA.js";
32
+ import {
33
+ execute
34
+ } from "./chunk-AWGLXW3B.js";
15
35
  import {
16
36
  executeMcpTool,
17
37
  validateMcpFlags
18
- } from "./chunk-EXWYJU2I.js";
19
-
20
- // src/index.ts
21
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
22
- import { resolve as resolve5 } from "path";
38
+ } from "./chunk-3AQ2CZKC.js";
39
+ import "./chunk-YDXTIN53.js";
23
40
 
24
41
  // src/env.ts
25
42
  import { readFileSync } from "fs";
@@ -42,108 +59,6 @@ function loadEnv() {
42
59
  }
43
60
  }
44
61
 
45
- // ../../interfaces/api/src/match.ts
46
- function matchRoute(manifest, userSegments, method) {
47
- if (userSegments.length > 0) {
48
- const ver = manifest.versions.find((v) => v.name === userSegments[0]);
49
- if (ver) {
50
- const match = findMatch(manifest.routes, userSegments.slice(1), method, ver.name);
51
- if (match) return match;
52
- }
53
- }
54
- return findMatch(manifest.routes, userSegments, method);
55
- }
56
- function findMatch(routes, userSegments, method, version) {
57
- let bestMatch = null;
58
- let bestScore = -1;
59
- for (const route of routes) {
60
- if (route.method !== method) continue;
61
- if (route.segments.length !== userSegments.length) continue;
62
- if (version !== void 0 && route.version !== version) continue;
63
- let score = 0;
64
- const params = {};
65
- let matches = true;
66
- for (let i = 0; i < route.segments.length; i++) {
67
- const seg = route.segments[i];
68
- const userSeg = userSegments[i];
69
- if (seg.isParam) {
70
- params[seg.value] = userSeg;
71
- } else if (seg.value === userSeg) {
72
- score += 1;
73
- } else {
74
- matches = false;
75
- break;
76
- }
77
- }
78
- if (matches && (score > bestScore || score === bestScore && isLaterVersion(route, bestMatch?.route))) {
79
- bestScore = score;
80
- bestMatch = { route, params };
81
- }
82
- }
83
- return bestMatch;
84
- }
85
- function isLaterVersion(a, b) {
86
- if (!b) return true;
87
- return a.version > b.version;
88
- }
89
- function suggestRoutes(manifest, userSegments) {
90
- if (!userSegments.length) return [];
91
- return manifest.routes.filter((route) => {
92
- if (route.segments.length < userSegments.length) return false;
93
- const firstSeg = route.segments[0];
94
- return !firstSeg.isParam && firstSeg.value === userSegments[0];
95
- });
96
- }
97
-
98
- // ../../interfaces/mcp/src/command.ts
99
- import { resolve as resolve2 } from "path";
100
- import { readFile } from "fs/promises";
101
- import { spawn } from "child_process";
102
- async function runMcp(rt, args) {
103
- const name = args[0];
104
- if (!name || name === "--help" || name === "-h") {
105
- console.log(`Serve a registered API as an MCP server over stdio.
106
-
107
- Usage:
108
- godmode mcp <name> [options]
109
-
110
- Options:
111
- --filter <text> Fuzzy-filter routes by resource name
112
- --method <method> Filter by HTTP method (get, post, etc.)
113
-
114
- For package-based extensions, spawns the extension's MCP server.
115
- For spec-based extensions, serves routes as MCP tools.`);
116
- process.exit(name ? 0 : 1);
117
- }
118
- let filter;
119
- let method;
120
- for (let i = 1; i < args.length; i++) {
121
- if (args[i] === "--filter" && args[i + 1]) filter = args[++i];
122
- else if (args[i] === "--method" && args[i + 1]) method = args[++i];
123
- }
124
- const pkgName = name.startsWith("@") ? name : `@godmode-cli/${name}`;
125
- const pkgDir = resolve2(rt.godmodeHome, "node_modules", pkgName);
126
- const mcpConfigPath = resolve2(pkgDir, ".mcp.json");
127
- try {
128
- const mcpConfig = JSON.parse(await readFile(mcpConfigPath, "utf-8"));
129
- const serverKey = Object.keys(mcpConfig.mcpServers)[0];
130
- const server = mcpConfig.mcpServers[serverKey];
131
- if (server.type === "stdio" && server.command) {
132
- const child = spawn(server.command, server.args || [], {
133
- stdio: "inherit",
134
- cwd: pkgDir,
135
- env: { ...process.env, ...server.env }
136
- });
137
- child.on("exit", (code) => process.exit(code ?? 0));
138
- return;
139
- }
140
- } catch {
141
- }
142
- const manifest = await rt.loadManifest(name);
143
- const { serveMcp } = await import("./server-4YVOAJJG.js");
144
- await serveMcp(manifest, { filter, method });
145
- }
146
-
147
62
  // src/args.ts
148
63
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
149
64
  function parseArgs(args) {
@@ -156,7 +71,7 @@ function parseArgs(args) {
156
71
  let filter;
157
72
  let methodFilter;
158
73
  let all = false;
159
- const verbose = process.env.GODMODE_VERBOSE === "1";
74
+ const debug = process.env.GODMODE_DEBUG === "1";
160
75
  const dryRun = process.env.GODMODE_DRY_RUN === "1";
161
76
  let help = false;
162
77
  for (let i = 0; i < args.length; i++) {
@@ -208,7 +123,7 @@ function parseArgs(args) {
208
123
  }
209
124
  }
210
125
  }
211
- return { segments, method, explicitMethod, headers, query, body, filter, methodFilter, all, verbose, dryRun, help };
126
+ return { segments, method, explicitMethod, headers, query, body, filter, methodFilter, all, debug, dryRun, help };
212
127
  }
213
128
  async function readStdin() {
214
129
  if (process.stdin.isTTY) return void 0;
@@ -218,1266 +133,258 @@ async function readStdin() {
218
133
  return text || void 0;
219
134
  }
220
135
 
221
- // src/help.ts
222
- import fuzzysort from "fuzzysort";
136
+ // ../../interfaces/mcp/src/command.ts
137
+ import { resolve as resolve2 } from "path";
138
+ import { readFile } from "fs/promises";
139
+ import { spawn } from "child_process";
140
+ async function runMcp(rt, args) {
141
+ const name = args[0];
142
+ if (!name || name === "--help" || name === "-h") {
143
+ console.log(`Serve a registered API as an MCP server over stdio.
223
144
 
224
- // ../cli-tools/src/index.ts
225
- var USE_COLOR = process.stdout.isTTY;
226
- var DIM = USE_COLOR ? "\x1B[2m" : "";
227
- var ITALIC = USE_COLOR ? "\x1B[3m" : "";
228
- var RESET = USE_COLOR ? "\x1B[0m" : "";
229
- var RED = USE_COLOR ? "\x1B[31m" : "";
230
- function visibleLength(s) {
231
- return s.replace(/\x1b\[[0-9;]*m/g, "").length;
232
- }
233
- function wrapText(text, width) {
234
- if (text.length <= width) return [text];
235
- const out = [];
236
- let line = "";
237
- for (const word of text.split(/\s+/)) {
238
- if (!line) {
239
- line = word;
240
- continue;
241
- }
242
- if (line.length + 1 + word.length <= width) {
243
- line += " " + word;
244
- } else {
245
- out.push(line);
246
- line = word;
247
- }
248
- }
249
- if (line) out.push(line);
250
- return out;
251
- }
252
- function renderSections(sections, maxLineWidth = 80) {
253
- const INDENT = " ";
254
- const GAP = " ";
255
- const flattened = sections.map((s) => {
256
- if (!s.rows.length) return { title: s.title, rows: [] };
257
- const cols = Math.max(...s.rows.map((r) => r.length));
258
- const widths = Array(cols).fill(0);
259
- for (const row of s.rows) {
260
- for (let i = 0; i < cols; i++) {
261
- widths[i] = Math.max(widths[i], visibleLength(row[i] ?? ""));
262
- }
263
- }
264
- const rows = s.rows.map((row) => {
265
- const left = row.slice(0, cols - 1).map((cell, i) => (cell ?? "") + " ".repeat(widths[i] - visibleLength(cell ?? ""))).join(GAP).replace(/\s+$/, "");
266
- const desc = row[cols - 1] ?? "";
267
- return [left, desc];
268
- });
269
- return { title: s.title, rows };
270
- });
271
- const allRows = flattened.flatMap((s) => s.rows);
272
- if (!allRows.length) return;
273
- const rowsWithDesc = allRows.filter((r) => r[1].length > 0);
274
- const widthCandidates = (rowsWithDesc.length ? rowsWithDesc : allRows).map((r) => visibleLength(r[0]));
275
- const globalLeft = Math.max(...widthCandidates);
276
- const descBudget = Math.max(20, maxLineWidth - INDENT.length - globalLeft - GAP.length);
277
- for (const s of flattened) {
278
- if (!s.rows.length) continue;
279
- if (s.title) {
280
- console.log("");
281
- console.log(s.title);
282
- }
283
- for (const [left, desc] of s.rows) {
284
- const pad = " ".repeat(Math.max(0, globalLeft - visibleLength(left)));
285
- const plain = desc.replace(/\x1b\[[0-9;]*m/g, "");
286
- if (!plain) {
287
- console.log(`${INDENT}${left}${pad}`.trimEnd());
288
- continue;
289
- }
290
- if (plain.length <= descBudget) {
291
- console.log(`${INDENT}${left}${pad}${GAP}${desc}`);
292
- continue;
293
- }
294
- const italic = /^\x1b\[3m/.test(desc);
295
- const wrap = (s2) => italic ? `${ITALIC}${s2}${RESET}` : s2;
296
- const lines = wrapText(plain, Math.max(10, descBudget));
297
- console.log(`${INDENT}${left}${pad}${GAP}${wrap(lines[0])}`);
298
- for (let i = 1; i < lines.length; i++) {
299
- console.log(`${INDENT}${" ".repeat(Math.max(0, globalLeft))}${GAP}${wrap(lines[i])}`);
300
- }
301
- }
302
- }
303
- }
304
- function authMissingLabel(type) {
305
- switch (type) {
306
- case "api-key":
307
- return "missing api-key";
308
- case "basic":
309
- return "missing basic auth credentials";
310
- case "bearer":
311
- default:
312
- return "missing bearer token";
313
- }
314
- }
315
- var HelpPage = class {
316
- title() {
317
- return null;
318
- }
319
- tagline() {
320
- return null;
321
- }
322
- authNote() {
323
- return null;
324
- }
325
- usage() {
326
- return [];
327
- }
328
- sections() {
329
- return [];
330
- }
331
- footer() {
332
- return null;
333
- }
334
- toJSON() {
335
- return {
336
- title: this.title(),
337
- tagline: this.tagline(),
338
- authNote: this.authNote(),
339
- usage: this.usage(),
340
- sections: this.sections(),
341
- footer: this.footer()
342
- };
343
- }
344
- render() {
345
- const d = this.toJSON();
346
- let wrote = false;
347
- if (d.title) {
348
- console.log(d.title);
349
- wrote = true;
350
- }
351
- if (d.tagline) {
352
- if (wrote) console.log("");
353
- console.log(d.tagline);
354
- wrote = true;
355
- }
356
- if (d.authNote && !d.authNote.present) {
357
- const arrow = USE_COLOR ? `${RED}-->${RESET}` : "-->";
358
- const label = authMissingLabel(d.authNote.authType);
359
- const body = USE_COLOR ? `${RED}${label}${RESET}` : label;
360
- if (wrote) console.log("");
361
- console.log(`${arrow} ${d.authNote.env}: ${body}`);
362
- wrote = true;
363
- }
364
- if (d.usage.length) {
365
- if (wrote) console.log("");
366
- for (let i = 0; i < d.usage.length; i++) {
367
- const prefix = i === 0 ? "Usage:" : " or:";
368
- console.log(`${prefix} ${d.usage[i]}`);
369
- }
370
- wrote = true;
371
- }
372
- renderSections(d.sections);
373
- if (d.footer) {
374
- if (d.footer.extras?.length) {
375
- console.log("");
376
- for (const line of d.footer.extras) console.log(line);
377
- }
378
- if (d.footer.reportBugs || d.footer.homepage) {
379
- console.log("");
380
- if (d.footer.reportBugs) console.log(`Report bugs to <${d.footer.reportBugs}>.`);
381
- if (d.footer.homepage) console.log(`Godmode home page: <${d.footer.homepage}>.`);
382
- }
383
- }
384
- }
385
- };
145
+ Usage:
146
+ godmode mcp <name> [options]
386
147
 
387
- // src/help.ts
388
- function buildTrie(routes) {
389
- const root = { name: "", isParam: false, methods: [], children: [] };
390
- for (const route of routes) {
391
- let node = root;
392
- for (const seg of route.segments) {
393
- let child = node.children.find((c) => c.name === seg.value && c.isParam === seg.isParam);
394
- if (!child) {
395
- child = { name: seg.value, isParam: seg.isParam, methods: [], children: [] };
396
- node.children.push(child);
397
- }
398
- node = child;
399
- }
400
- node.methods.push({ method: route.method, summary: route.summary });
401
- }
402
- return root;
403
- }
404
- function navigateTrie(root, segments) {
405
- let node = root;
406
- const fullPath = [];
407
- const params = [];
408
- for (const seg of segments) {
409
- const staticChild = node.children.find((c) => !c.isParam && c.name === seg);
410
- if (staticChild) {
411
- fullPath.push(seg);
412
- node = staticChild;
413
- continue;
414
- }
415
- const paramChild = node.children.find((c) => c.isParam);
416
- if (paramChild) {
417
- const nested = paramChild.children.find((c) => !c.isParam && c.name === seg);
418
- if (nested) {
419
- fullPath.push(`<${paramChild.name}>`, seg);
420
- params.push({ name: paramChild.name, provided: false });
421
- node = nested;
422
- continue;
423
- }
424
- fullPath.push(seg);
425
- params.push({ name: paramChild.name, provided: true, value: seg });
426
- node = paramChild;
427
- continue;
428
- }
429
- return null;
430
- }
431
- return { node, fullPath, params };
432
- }
433
- function getChildren(node) {
434
- const statics = node.children.filter((c) => !c.isParam);
435
- const params = node.children.filter((c) => c.isParam);
436
- const result = [...statics];
437
- for (const p of params) result.push(...p.children);
438
- result.sort((a, b) => a.name.localeCompare(b.name));
439
- return result;
440
- }
441
- var METHOD_LABEL = USE_COLOR ? {
442
- get: "\x1B[1;38;5;34mGET\x1B[0m",
443
- post: "\x1B[1;38;5;25mPOST\x1B[0m",
444
- put: "\x1B[1;38;5;172mPUT\x1B[0m",
445
- patch: "\x1B[1;38;5;30mPATCH\x1B[0m",
446
- delete: "\x1B[1;38;5;160mDELETE\x1B[0m"
447
- } : { get: "GET", post: "POST", put: "PUT", patch: "PATCH", delete: "DELETE" };
448
- var INTERFACE_LABEL = {
449
- api: "API",
450
- graphql: "GraphQL",
451
- mcp: "MCP",
452
- skill: "Skill"
453
- };
454
- var titleCase = (s) => s.charAt(0).toUpperCase() + s.slice(1);
455
- var ROOT_OPTION_ROWS = [
456
- ["-v, --version", "output version information and exit"]
457
- ];
458
- var INTERFACE_OPTION_ROWS = [
459
- ["-H, --header <key:value>", "add a request header"],
460
- ["-A, --all", "list all resources"],
461
- ["-F, --filter <text>", "fuzzy-filter resources by name"],
462
- ["-X, --method <verb>", "filter resources by HTTP method"],
463
- ["-h, --help", "show help for this subcommand"],
464
- ["-v, --version", "show extension spec versions"]
465
- ];
466
- function getParamName(node) {
467
- return node.children.find((c) => c.isParam)?.name || null;
468
- }
469
- function methodLabels(node) {
470
- const methods = /* @__PURE__ */ new Set();
471
- for (const ep of node.methods) methods.add(ep.method);
472
- const paramChild = node.children.find((c) => c.isParam);
473
- if (paramChild) for (const ep of paramChild.methods) methods.add(ep.method);
474
- const order = ["get", "post", "put", "patch", "delete"];
475
- return order.filter((m) => methods.has(m)).map((m) => METHOD_LABEL[m]).join(" ");
476
- }
477
- var RootHelp = class extends HelpPage {
478
- tagline() {
479
- return "A control surface of agentic I/O.";
480
- }
481
- usage() {
482
- return [
483
- "godmode <extension> [args]...",
484
- "godmode <extension> <interface> [args]..."
485
- ];
486
- }
487
- sections() {
488
- return [
489
- { title: "Built-in extensions:", rows: [
490
- ["ext", "Install and manage godmode extensions"],
491
- ["agent", "Coding-agent workflows"]
492
- ] },
493
- { title: "Options:", rows: ROOT_OPTION_ROWS }
494
- ];
495
- }
496
- footer() {
497
- return {
498
- extras: [
499
- 'Run "godmode ext list" to see installed extensions.',
500
- 'Run "godmode <extension> --help" for extension-specific usage.'
501
- ],
502
- reportBugs: "https://github.com/tomsiwik/godmode/issues",
503
- homepage: "https://godmode.so"
504
- };
505
- }
506
- };
507
- var ExtensionOverview = class extends HelpPage {
508
- constructor(multi) {
509
- super();
510
- this.multi = multi;
511
- }
512
- title() {
513
- return titleCase(this.multi.name || this.multi.slug);
514
- }
515
- usage() {
516
- const declared = Object.keys(this.multi.interfaces);
517
- const args = (iface) => iface === "mcp" ? " <tool> [args]" : iface === "graphql" ? " <query> [flags]" : " <method> <resource> [id] [flags]";
518
- return declared.map((iface) => `godmode ${this.multi.slug} ${iface}${args(iface)}`);
519
- }
520
- sections() {
521
- const declared = Object.keys(this.multi.interfaces);
522
- return [
523
- { title: "Interfaces:", rows: declared.map((iface) => {
524
- const d = this.multi.interfaces[iface];
525
- const url = d && "url" in d && d.url ? d.url : "(local)";
526
- return [iface, url];
527
- }) },
528
- { title: "Options:", rows: [["-v, --version", "show extension spec versions"]] }
529
- ];
530
- }
531
- };
532
- var ExtensionVersionPage = class extends HelpPage {
533
- constructor(multi) {
534
- super();
535
- this.multi = multi;
536
- }
537
- title() {
538
- return titleCase(this.multi.name || this.multi.slug);
539
- }
540
- sections() {
541
- const declared = Object.keys(this.multi.interfaces);
542
- return [
543
- { title: "", rows: declared.map((iface) => {
544
- const d = this.multi.interfaces[iface];
545
- return [iface, d?.specVersion || "(unversioned)"];
546
- }) }
547
- ];
548
- }
549
- };
550
- var InterfaceHelp = class extends HelpPage {
551
- constructor(manifest, apiName, rawPath = [], opts = {}) {
552
- super();
553
- this.manifest = manifest;
554
- this.apiName = apiName;
555
- this.opts = opts;
556
- const HTTP_VERBS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
557
- const cleaned = [...rawPath];
558
- while (cleaned.length && HTTP_VERBS.has(cleaned[cleaned.length - 1].toUpperCase())) {
559
- cleaned.pop();
560
- }
561
- this.path = cleaned;
562
- }
563
- path;
564
- get ifaceType() {
565
- return this.manifest.config.type;
566
- }
567
- getNav() {
568
- const root = buildTrie(this.manifest.routes);
569
- return navigateTrie(root, this.path);
570
- }
571
- title() {
572
- if (this.path.length) return null;
573
- return `${titleCase(this.manifest.config.name || this.apiName)} ${INTERFACE_LABEL[this.ifaceType] || this.ifaceType}`;
574
- }
575
- authNote() {
576
- const auth = this.manifest.config.auth;
577
- if (!auth?.env) return null;
578
- return {
579
- env: auth.env,
580
- authType: auth.type || "bearer",
581
- present: !!process.env[auth.env]
582
- };
148
+ Options:
149
+ --filter <text> Fuzzy-filter routes by resource name
150
+ --method <method> Filter by HTTP method (get, post, etc.)
151
+
152
+ For package-based extensions, spawns the extension's MCP server.
153
+ For spec-based extensions, serves routes as MCP tools.`);
154
+ process.exit(name ? 0 : 1);
583
155
  }
584
- usage() {
585
- if (!this.path.length) {
586
- const multi = this.opts.multi;
587
- const declared = multi ? Object.keys(multi.interfaces) : [this.ifaceType];
588
- const ordered = [this.ifaceType, ...declared.filter((k) => k !== this.ifaceType)];
589
- const args = (iface) => iface === "mcp" ? " <tool> [args]" : iface === "graphql" ? " <query> [flags]" : " <method> <resource> [id] [flags]";
590
- return ordered.map((iface) => `godmode ${this.apiName} ${iface}${args(iface)}`);
591
- }
592
- const nav = this.getNav();
593
- if (!nav) return [];
594
- const paramHint = getParamName(nav.node);
595
- const idRef = paramHint ? ` [${paramHint}]` : "";
596
- const methodSlot = this.ifaceType === "api" ? "<method> " : "";
597
- return [`godmode ${this.apiName} ${this.ifaceType} ${methodSlot}${nav.fullPath.join(" ")}${idRef} [flags]`];
156
+ let filter;
157
+ let method;
158
+ for (let i = 1; i < args.length; i++) {
159
+ if (args[i] === "--filter" && args[i + 1]) filter = args[++i];
160
+ else if (args[i] === "--method" && args[i + 1]) method = args[++i];
598
161
  }
599
- sections() {
600
- const nav = this.getNav();
601
- if (!nav) return [];
602
- const sections = [];
603
- if (!this.path.length) {
604
- if (this.ifaceType === "api") {
605
- const methodsPresent = new Set(this.manifest.routes.map((r) => r.method.toLowerCase()));
606
- const METHOD_DESC = {
607
- get: "retrieve a resource",
608
- post: "create a resource or send a command",
609
- put: "replace a resource",
610
- patch: "modify a resource",
611
- delete: "remove a resource",
612
- head: "retrieve headers only"
613
- };
614
- const order = ["get", "post", "put", "patch", "delete", "head"];
615
- const methodsHere = order.filter((m) => methodsPresent.has(m));
616
- if (methodsHere.length) {
617
- sections.push({
618
- title: "Methods:",
619
- rows: methodsHere.map((m) => [METHOD_LABEL[m] || m.toUpperCase(), METHOD_DESC[m]])
620
- });
621
- }
622
- }
623
- } else {
624
- const mcpTools = this.manifest.config._mcpTools;
625
- const mcpTool = mcpTools?.find((t) => t.name === this.path[this.path.length - 1]);
626
- if (mcpTool?.inputSchema?.properties) {
627
- const props = mcpTool.inputSchema.properties;
628
- const required = new Set(mcpTool.inputSchema.required || []);
629
- const rows = [];
630
- for (const [name, schema] of Object.entries(props)) {
631
- const req = required.has(name) ? `${RED}[REQUIRED]${RESET}` : "";
632
- const desc = schema.description ? `${ITALIC}${schema.description}${RESET}` : "";
633
- rows.push([name, req, desc]);
634
- }
635
- sections.push({ title: "Parameters:", rows });
636
- } else {
637
- const order = ["get", "post", "put", "patch", "delete"];
638
- const pc = nav.node.children.find((c) => c.isParam);
639
- const items = [];
640
- for (const ep of nav.node.methods) items.push({ method: ep.method, summary: ep.summary, needsId: false });
641
- if (pc) for (const ep of pc.methods) items.push({ method: ep.method, summary: ep.summary, needsId: true });
642
- if (items.length) {
643
- const rows = [];
644
- for (const m of order) {
645
- for (const r of items.filter((r2) => r2.method === m)) {
646
- const label = METHOD_LABEL[m] || m.toUpperCase();
647
- const idArg = r.needsId ? `<${pc.name}>` : "";
648
- const desc = r.summary ? `${ITALIC}${r.summary}${RESET}` : "";
649
- rows.push([label, idArg, desc]);
650
- }
651
- }
652
- sections.push({ title: "Methods:", rows });
653
- }
654
- }
162
+ const pkgName = name.startsWith("@") ? name : `@godmode-cli/${name}`;
163
+ const pkgDir = resolve2(rt.godmodeHome, "node_modules", pkgName);
164
+ const mcpConfigPath = resolve2(pkgDir, ".mcp.json");
165
+ try {
166
+ const mcpConfig = JSON.parse(await readFile(mcpConfigPath, "utf-8"));
167
+ const serverKey = Object.keys(mcpConfig.mcpServers)[0];
168
+ const server = mcpConfig.mcpServers[serverKey];
169
+ if (server.type === "stdio" && server.command) {
170
+ const child = spawn(server.command, server.args || [], {
171
+ stdio: "inherit",
172
+ cwd: pkgDir,
173
+ env: { ...process.env, ...server.env }
174
+ });
175
+ child.on("exit", (code) => process.exit(code ?? 0));
176
+ return;
655
177
  }
656
- const children = getChildren(nav.node).filter((c) => !c.isParam);
657
- const childNames = [...new Set(children.map((c) => c.name))];
658
- const resourceSection = buildResourceSection(
659
- children,
660
- childNames,
661
- this.manifest,
662
- this.opts.filter,
663
- this.opts.methodFilter,
664
- this.opts.all
665
- );
666
- if (resourceSection) sections.push(resourceSection.section);
667
- sections.push({ title: "Options:", rows: INTERFACE_OPTION_ROWS });
668
- return sections;
669
- }
670
- /** Drilled-in view also shows param status lines (ANSI-only). */
671
- render() {
672
- super.render();
178
+ } catch {
673
179
  }
674
- };
675
- function showHelp() {
676
- new RootHelp().render();
677
- }
678
- function showExtensionOverview(multi) {
679
- new ExtensionOverview(multi).render();
680
- }
681
- function showExtensionVersion(multi) {
682
- new ExtensionVersionPage(multi).render();
683
- }
684
- function showApiHelp(manifest, apiName, path, filter, methodFilter, all, multi) {
685
- new InterfaceHelp(manifest, apiName, path, { multi, filter, methodFilter, all }).render();
180
+ const manifest = await rt.loadManifest(name);
181
+ const { serveMcp } = await import("./server-L3L4TKK3.js");
182
+ await serveMcp(manifest, { filter, method, checkPermission: rt.checkPermission });
686
183
  }
687
- async function showVersion() {
688
- const { readFile: readFile2 } = await import("fs/promises");
689
- const { resolve: resolve6, dirname } = await import("path");
690
- const { fileURLToPath } = await import("url");
691
- const root = resolve6(dirname(fileURLToPath(import.meta.url)), "..");
692
- const pkg = JSON.parse(await readFile2(resolve6(root, "package.json"), "utf-8"));
693
- const license = await readFile2(resolve6(root, "LICENSE"), "utf-8");
694
- const copyright = license.match(/^Copyright.*$/m)?.[0] ?? "";
695
- console.log(`godmode ${pkg.version}
696
184
 
697
- ${pkg.license} License
698
- ${copyright}`);
699
- }
700
- function buildResourceSection(children, childNames, manifest, filter, methodFilter, all) {
701
- if (!childNames.length) return null;
702
- const ALL_METHODS = ["get", "post", "put", "patch", "delete"];
703
- const mf = methodFilter ? fuzzysort.go(methodFilter, ALL_METHODS)[0]?.target : void 0;
704
- if (methodFilter && !mf) return null;
705
- const candidates = mf ? childNames.filter((n) => {
706
- const child = children.find((c) => c.name === n);
707
- if (!child) return false;
708
- const methods = /* @__PURE__ */ new Set();
709
- for (const ep of child.methods) methods.add(ep.method);
710
- const pc = child.children.find((c) => c.isParam);
711
- if (pc) for (const ep of pc.methods) methods.add(ep.method);
712
- return methods.has(mf);
713
- }) : childNames;
714
- const filtered = filter ? fuzzysort.go(filter, candidates).map((r) => r.target) : candidates;
715
- const title = filter || mf ? `Resources${filter ? ` matching "${filter}"` : ""}${mf ? ` with ${mf.toUpperCase()}` : ""}:` : "Resources:";
716
- if (!filtered.length) return { section: { title, rows: [[" No matches.", ""]] } };
717
- const limit = filter || mf || all ? filtered.length : 5;
718
- const shown = filtered.slice(0, limit);
719
- const more = filtered.length - shown.length;
720
- const rows = [];
721
- for (const n of shown) {
722
- const child = children.find((c) => c.name === n);
723
- const labels = child ? methodLabels(child) : "";
724
- const subCount = child ? getChildren(child).filter((c) => !c.isParam).length : 0;
725
- const sub = subCount ? `(${subCount} sub)` : "";
726
- const rawDesc = manifest.resourceDescriptions[n] || "";
727
- const desc = rawDesc ? `${ITALIC}${rawDesc}${RESET}` : "";
728
- const parts = [labels, sub, desc].filter(Boolean);
729
- rows.push([n, parts.join(" ")]);
730
- }
731
- if (more) {
732
- const verb = mf ? ` ${mf.toUpperCase()}` : "";
733
- const hintText = `${more} more${verb} resources. Use options to display more`;
734
- const dots = USE_COLOR ? `${DIM}...${RESET}` : "...";
735
- const body = USE_COLOR ? `${DIM}${hintText}${RESET}` : hintText;
736
- rows.push([dots, body]);
185
+ // src/interfaces.ts
186
+ var Interface = class {
187
+ constructor(ctx) {
188
+ this.ctx = ctx;
189
+ }
190
+ ctx;
191
+ /** `godmode <ext> <iface>` with no resource by default shows help.
192
+ * MCP overrides to serve as an MCP server over stdio. */
193
+ async handleEmpty() {
194
+ this.showHelp();
195
+ }
196
+ /** Interface-specific arg validation. Return an error string to bail, or
197
+ * null to proceed. Called after segments have been provided. */
198
+ validate() {
199
+ return null;
737
200
  }
738
- return { section: { title, rows } };
739
- }
740
-
741
- // ../../commands/agent/dist/index.js
742
- import { spawnSync } from "child_process";
743
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
744
- import { resolve as resolve3 } from "path";
745
- import { closeSync, existsSync as existsSync2, openSync, readFileSync as readFileSync2, readSync, statSync, writeFileSync as writeFileSync2 } from "fs";
746
- import { resolve as resolve22 } from "path";
747
- import { randomUUID } from "crypto";
748
- import { mkdirSync, readFileSync as readFileSync4, writeFileSync, existsSync, readdirSync } from "fs";
749
- import { homedir } from "os";
750
- import { resolve as resolve4 } from "path";
751
- var DEFAULT_HARNESS_ORDER = ["claude", "codex", "gemini", "pi"];
752
- var HARNESSES = {
753
- claude: {
754
- id: "claude",
755
- displayName: "Claude Code",
756
- command: "claude",
757
- modelFlags: ["--model"],
758
- effortFlags: ["--effort"],
759
- helpArgs: ["--help"],
760
- promptHints: ["[prompt]", "your prompt"],
761
- buildTurnArgs: ({ prompt, model, effort, passthroughArgs, resumeToken }) => [
762
- "-p",
763
- "--verbose",
764
- "--output-format",
765
- "stream-json",
766
- ...resumeToken ? ["--resume", resumeToken] : [],
767
- ...model ? ["--model", model] : [],
768
- ...effort ? ["--effort", effort] : [],
769
- prompt,
770
- ...passthroughArgs
771
- ]
772
- },
773
- codex: {
774
- id: "codex",
775
- displayName: "Codex CLI",
776
- command: "codex",
777
- modelFlags: ["-m", "--model"],
778
- effortFlags: [],
779
- helpArgs: ["--help"],
780
- promptHints: ["[prompt]", "optional user prompt"],
781
- buildTurnArgs: ({ prompt, model, passthroughArgs }) => [
782
- "exec",
783
- "--json",
784
- ...model ? ["-m", model] : [],
785
- prompt,
786
- ...passthroughArgs
787
- ]
788
- },
789
- gemini: {
790
- id: "gemini",
791
- displayName: "Gemini CLI",
792
- command: "gemini",
793
- modelFlags: ["-m", "--model"],
794
- effortFlags: [],
795
- helpArgs: ["--help"],
796
- promptHints: ["query", "initial prompt"],
797
- buildTurnArgs: ({ prompt, model, passthroughArgs, resumeToken }) => [
798
- ...resumeToken ? ["--resume", resumeToken] : [],
799
- ...model ? ["-m", model] : [],
800
- "-p",
801
- prompt,
802
- "-o",
803
- "stream-json",
804
- ...passthroughArgs
805
- ]
806
- },
807
- pi: {
808
- id: "pi",
809
- displayName: "Pi",
810
- command: "pi",
811
- modelFlags: ["--model"],
812
- effortFlags: ["--thinking"],
813
- helpArgs: ["--help"],
814
- promptHints: ["[messages...]", "initial prompt"],
815
- buildTurnArgs: ({ prompt, model, effort, passthroughArgs, resumeToken, sessionDir }) => [
816
- "--print",
817
- "--mode",
818
- "json",
819
- ...sessionDir ? ["--session-dir", sessionDir] : [],
820
- ...resumeToken ? ["--continue"] : [],
821
- ...model ? ["--model", model] : [],
822
- ...effort ? ["--thinking", effort] : [],
823
- prompt,
824
- ...passthroughArgs
825
- ]
201
+ showHelp() {
202
+ const { manifest, extensionName, parsed, multi } = this.ctx;
203
+ const implicitMethodFilter = parsed.methodFilter || (parsed.explicitMethod ? parsed.method : void 0);
204
+ showApiHelp(manifest, extensionName, parsed.segments, parsed.filter, implicitMethodFilter, parsed.all, multi);
826
205
  }
827
206
  };
828
- function detectHarness(executor, requested) {
829
- const ids = requested ? [requested] : DEFAULT_HARNESS_ORDER;
830
- for (const id of ids) {
831
- const harness = HARNESSES[id];
832
- const result = executor(harness.command, harness.helpArgs);
833
- if (!result.error && result.status === 0) return harness;
834
- }
835
- throw new Error(requested ? `Harness "${requested}" not found.` : "No supported coding harness found.");
836
- }
837
- function event(ts, id, turn, partial) {
838
- return { ts, id, turn, ...partial };
839
- }
840
- function normalizeTurn(harness, id, turn, stdout, stderr, ts) {
841
- const lines = stdout.split(/\r?\n/);
842
- if (harness === "claude") return parseClaude(lines, id, turn, stderr, ts);
843
- if (harness === "gemini") return parseGemini(lines, id, turn, stderr, ts);
844
- if (harness === "pi") return parsePi(lines, id, turn, stderr, ts);
845
- return {
846
- sessionId: null,
847
- assistantText: stdout.trim(),
848
- events: [
849
- event(ts, id, turn, { type: "turn.started" }),
850
- event(ts, id, turn, { type: "assistant.completed", text: stdout.trim() }),
851
- event(ts, id, turn, { type: "turn.completed", status: "completed" })
852
- ]
853
- };
854
- }
855
- function parseClaude(lines, id, turn, stderr, ts) {
856
- const events = [];
857
- let sessionId = null;
858
- let assistantText = "";
859
- let usage;
860
- let timing;
861
- for (const line of lines) {
862
- if (!line.trim()) continue;
863
- let parsed;
864
- try {
865
- parsed = JSON.parse(line);
866
- } catch {
867
- continue;
868
- }
869
- if (parsed.type === "system" && parsed.subtype === "init") {
870
- sessionId = parsed.session_id ?? sessionId;
871
- events.push(event(ts, id, turn, { type: "turn.started" }));
872
- events.push(event(ts, id, turn, { type: "state.changed", state: "starting" }));
873
- }
874
- if (parsed.type === "assistant") {
875
- const text = (parsed.message?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("");
876
- if (text) assistantText = text;
877
- if (text) events.push(event(ts, id, turn, { type: "assistant.completed", text }));
878
- }
879
- if (parsed.type === "result") {
880
- sessionId = parsed.session_id ?? sessionId;
881
- usage = {
882
- inputTokens: parsed.usage?.input_tokens,
883
- outputTokens: parsed.usage?.output_tokens,
884
- cachedTokens: parsed.usage?.cache_read_input_tokens
885
- };
886
- timing = { durationMs: parsed.duration_ms };
887
- events.push(event(ts, id, turn, { type: "turn.completed", status: parsed.is_error ? "failed" : "completed" }));
888
- }
207
+ async function executeRoute(ctx) {
208
+ const { manifest, extensionName, iface, parsed } = ctx;
209
+ const query = parsed.query;
210
+ const hasBody = Object.keys(parsed.body).length > 0;
211
+ let body = hasBody ? JSON.stringify(parsed.body) : void 0;
212
+ if (!body && ["post", "put", "patch"].includes(parsed.method)) {
213
+ body = await readStdin();
889
214
  }
890
- for (const line of stderr.split(/\r?\n/).filter(Boolean)) events.push(event(ts, id, turn, { type: "warning", message: line }));
891
- return { sessionId, assistantText, events, usage, timing };
892
- }
893
- function parseGemini(lines, id, turn, stderr, ts) {
894
- const events = [];
895
- let sessionId = null;
896
- let assistantText = "";
897
- let usage;
898
- let timing;
899
- for (const line of lines) {
900
- if (!line.trim()) continue;
901
- let parsed;
902
- try {
903
- parsed = JSON.parse(line);
904
- } catch {
905
- continue;
906
- }
907
- if (parsed.type === "init") {
908
- sessionId = parsed.session_id ?? sessionId;
909
- events.push(event(ts, id, turn, { type: "turn.started" }));
910
- events.push(event(ts, id, turn, { type: "state.changed", state: "starting" }));
911
- }
912
- if (parsed.type === "message" && parsed.role === "assistant" && parsed.delta === true && typeof parsed.content === "string") {
913
- assistantText += parsed.content;
914
- events.push(event(ts, id, turn, { type: "assistant.delta", text: parsed.content }));
915
- }
916
- if (parsed.type === "result") {
917
- usage = {
918
- inputTokens: parsed.stats?.input_tokens,
919
- outputTokens: parsed.stats?.output_tokens,
920
- cachedTokens: parsed.stats?.cached,
921
- totalTokens: parsed.stats?.total_tokens
922
- };
923
- timing = { durationMs: parsed.stats?.duration_ms };
924
- events.push(event(ts, id, turn, { type: "assistant.completed", text: assistantText }));
925
- events.push(event(ts, id, turn, { type: "turn.completed", status: parsed.status === "success" ? "completed" : "failed" }));
215
+ if (parsed.segments[0]?.startsWith("/")) {
216
+ const rawPath = parsed.segments[0];
217
+ const route = manifest.routes.find((r) => r.method === parsed.method && r.path === rawPath);
218
+ const resource = route ? resourceFromSegments(route.segments) : resourceFromRawPath(rawPath);
219
+ const check2 = checkPermission({
220
+ extension: extensionName,
221
+ resource,
222
+ method: parsed.method
223
+ });
224
+ if (!check2.allowed) {
225
+ process.stderr.write(`Blocked: ${check2.reason}
226
+ `);
227
+ process.stderr.write(`Suggested allow rule:
228
+ ${suggestedAllowRule({ extension: extensionName, resource, method: parsed.method })}
229
+ `);
230
+ process.exit(EXIT_CODES.permissionDenied);
926
231
  }
232
+ const syntheticRoute = {
233
+ path: rawPath,
234
+ method: parsed.method,
235
+ summary: "",
236
+ version: "",
237
+ segments: []
238
+ };
239
+ await execute(manifest, { route: syntheticRoute, params: {} }, {
240
+ headers: parsed.headers,
241
+ query,
242
+ body,
243
+ debug: parsed.debug,
244
+ dryRun: parsed.dryRun
245
+ });
246
+ return;
927
247
  }
928
- for (const line of stderr.split(/\r?\n/).filter(Boolean)) events.push(event(ts, id, turn, { type: "warning", message: line }));
929
- return { sessionId, assistantText, events, usage, timing };
930
- }
931
- function parsePi(lines, id, turn, stderr, ts) {
932
- const events = [];
933
- let sessionId = null;
934
- let assistantText = "";
935
- for (const line of lines) {
936
- if (!line.trim()) continue;
937
- let parsed;
938
- try {
939
- parsed = JSON.parse(line);
940
- } catch {
941
- continue;
942
- }
943
- if (parsed.type === "session") {
944
- sessionId = parsed.id ?? sessionId;
945
- events.push(event(ts, id, turn, { type: "turn.started" }));
946
- events.push(event(ts, id, turn, { type: "state.changed", state: "starting" }));
947
- }
948
- if (parsed.type === "turn_start") events.push(event(ts, id, turn, { type: "state.changed", state: "running" }));
949
- if (parsed.type === "message_update" && parsed.assistantMessageEvent?.type === "thinking_start") {
950
- events.push(event(ts, id, turn, { type: "state.changed", state: "thinking" }));
951
- }
952
- if (parsed.type === "message_update" && parsed.assistantMessageEvent?.type === "text_delta") {
953
- const delta = parsed.assistantMessageEvent.delta ?? "";
954
- assistantText += delta;
955
- if (delta) events.push(event(ts, id, turn, { type: "assistant.delta", text: delta }));
956
- }
957
- if (parsed.type === "turn_end") {
958
- const text = (parsed.message?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("");
959
- if (text) assistantText = text;
960
- events.push(event(ts, id, turn, { type: "assistant.completed", text: assistantText }));
961
- events.push(event(ts, id, turn, { type: "turn.completed", status: "completed" }));
962
- }
248
+ const match = matchRoute(manifest, parsed.segments, parsed.method);
249
+ if (!match) {
250
+ reportNoMatch(ctx);
251
+ process.exit(EXIT_CODES.notFound);
963
252
  }
964
- for (const line of stderr.split(/\r?\n/).filter(Boolean)) events.push(event(ts, id, turn, { type: "warning", message: line }));
965
- return { sessionId, assistantText, events };
966
- }
967
- var GODMODE_HOME2 = process.platform === "linux" && process.env.XDG_CONFIG_HOME ? resolve4(process.env.XDG_CONFIG_HOME, "godmode") : resolve4(homedir(), ".godmode");
968
- var CODING_AGENTS_HOME = resolve4(GODMODE_HOME2, "coding-agents");
969
- var RUNS_DIR = resolve4(CODING_AGENTS_HOME, "runs");
970
- var ACTIVE_RUNS_PATH = resolve4(CODING_AGENTS_HOME, "active-runs.json");
971
- function timestamp() {
972
- return (/* @__PURE__ */ new Date()).toISOString();
973
- }
974
- function ensureDirs() {
975
- mkdirSync(RUNS_DIR, { recursive: true });
976
- }
977
- function shellEscape(value) {
978
- return `'${value.replace(/'/g, `"'"'`)}'`;
979
- }
980
- function writeJson(path, value) {
981
- mkdirSync(resolve4(path, ".."), { recursive: true });
982
- writeFileSync(path, `${JSON.stringify(value, null, 2)}
253
+ const check = checkPermission({
254
+ extension: extensionName,
255
+ resource: resourceFromSegments(match.route.segments),
256
+ method: parsed.method
257
+ });
258
+ if (!check.allowed) {
259
+ process.stderr.write(`Blocked: ${check.reason}
983
260
  `);
984
- }
985
- function readJson(path) {
986
- try {
987
- return JSON.parse(readFileSync4(path, "utf-8"));
988
- } catch {
989
- return null;
261
+ process.stderr.write(`Suggested allow rule:
262
+ ${suggestedAllowRule({ extension: extensionName, resource: resourceFromSegments(match.route.segments), method: parsed.method })}
263
+ `);
264
+ process.exit(EXIT_CODES.permissionDenied);
990
265
  }
991
- }
992
- function readSettingsFile(path) {
993
- return readJson(path) ?? {};
994
- }
995
- function loadSettings(cwd = process.cwd()) {
996
- const globalSettings = readSettingsFile(resolve4(GODMODE_HOME2, "settings.json"));
997
- const projectSettings = readSettingsFile(resolve4(cwd, ".godmode", "settings.json"));
998
- return {
999
- ...globalSettings,
1000
- ...projectSettings,
1001
- plugins: {
1002
- ...globalSettings.plugins ?? {},
1003
- ...projectSettings.plugins ?? {},
1004
- "coding-agents": {
1005
- ...globalSettings.plugins?.["coding-agents"] ?? {},
1006
- ...projectSettings.plugins?.["coding-agents"] ?? {}
1007
- }
1008
- }
1009
- };
1010
- }
1011
- function loadActiveRuns() {
1012
- return readJson(ACTIVE_RUNS_PATH) ?? {};
1013
- }
1014
- function saveActiveRun(cwd, id) {
1015
- const current = loadActiveRuns();
1016
- current[cwd] = id;
1017
- writeJson(ACTIVE_RUNS_PATH, current);
1018
- }
1019
- function activeRunIdForCwd(cwd) {
1020
- return loadActiveRuns()[cwd] ?? null;
1021
- }
1022
- function runDir(id) {
1023
- return resolve4(RUNS_DIR, id);
1024
- }
1025
- function runPath(id) {
1026
- return resolve4(runDir(id), "run.json");
1027
- }
1028
- function turnDir(id, turn) {
1029
- return resolve4(runDir(id), "turns", String(turn).padStart(4, "0"));
1030
- }
1031
- function loadRun(id) {
1032
- const run = readJson(runPath(id));
1033
- if (!run) throw new Error(`Run not found: ${id}`);
1034
- return run;
1035
- }
1036
- function saveRun(run) {
1037
- run.updatedAt = timestamp();
1038
- writeJson(runPath(run.id), run);
1039
- }
1040
- function listRuns() {
1041
- ensureDirs();
1042
- return readdirSync(RUNS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readJson(runPath(entry.name))).filter((value) => !!value).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1043
- }
1044
- function createRun(cwd, harness, model, effort) {
1045
- ensureDirs();
1046
- const id = `run-${Date.now()}-${randomUUID().slice(0, 8)}`;
1047
- const run = {
1048
- id,
1049
- cwd,
1050
- harness: harness.id,
1051
- harnessName: harness.displayName,
1052
- sessions: {
1053
- zmx: `godmode-agent-${harness.id}-${id.replace(/^run-/, "")}`
1054
- },
1055
- model: model ?? null,
1056
- effort: effort ?? null,
1057
- status: "idle",
1058
- createdAt: timestamp(),
1059
- updatedAt: timestamp(),
1060
- lastTurn: 0
1061
- };
1062
- mkdirSync(runDir(id), { recursive: true });
1063
- mkdirSync(resolve4(runDir(id), "turns"), { recursive: true });
1064
- saveRun(run);
1065
- saveActiveRun(cwd, id);
1066
- return run;
1067
- }
1068
- function buildShellCommand(harness, run, turn, prompt, passthroughArgs) {
1069
- const dir = turnDir(run.id, turn);
1070
- mkdirSync(dir, { recursive: true });
1071
- const stdoutPath = resolve4(dir, "stdout.log");
1072
- const stderrPath = resolve4(dir, "stderr.log");
1073
- const exitCodePath = resolve4(dir, "exit-code.txt");
1074
- const sessionDir = resolve4(runDir(run.id), "pi-session");
1075
- mkdirSync(sessionDir, { recursive: true });
1076
- const args = harness.buildTurnArgs({
1077
- prompt,
1078
- model: run.model ?? void 0,
1079
- effort: run.effort ?? void 0,
1080
- passthroughArgs,
1081
- resumeToken: run.sessions[run.harness] ?? void 0,
1082
- sessionDir
266
+ await execute(manifest, match, {
267
+ headers: parsed.headers,
268
+ query,
269
+ body,
270
+ debug: parsed.debug,
271
+ dryRun: parsed.dryRun
1083
272
  });
1084
- const command = `${[harness.command, ...args].map(shellEscape).join(" ")} > ${shellEscape(stdoutPath)} 2> ${shellEscape(stderrPath)}`;
1085
- const shell = `${command}; code=$?; printf '%s' "$code" > ${shellEscape(exitCodePath)}; exit "$code"`;
1086
- return { shell, stdoutPath, stderrPath, exitCodePath };
1087
- }
1088
- function waitForFile(path, timeoutMs = 3e5) {
1089
- const started = Date.now();
1090
- while (!existsSync(path)) {
1091
- if (Date.now() - started > timeoutMs) throw new Error(`Timed out waiting for ${path}`);
1092
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
1093
- }
1094
- }
1095
- function latestTurn(run) {
1096
- if (run.lastTurn < 1) throw new Error(`Run ${run.id} has no turns yet.`);
1097
- const turn = readJson(resolve4(turnDir(run.id, run.lastTurn), "normalized.json"));
1098
- if (!turn) throw new Error(`Turn ${run.lastTurn} not found for ${run.id}`);
1099
- return turn;
1100
273
  }
1101
- function resolveRunId(cwd, explicit) {
1102
- if (explicit) return explicit;
1103
- const active = activeRunIdForCwd(cwd);
1104
- if (!active) throw new Error(`No active coding-agent run for ${cwd}`);
1105
- return active;
1106
- }
1107
- function renderTurn(turn, outputMode) {
1108
- if (outputMode === "assistant-text") return turn.assistant.text;
1109
- if (outputMode === "raw") {
1110
- const stdout = existsSync2(turn.paths.stdout) ? readFileSync2(turn.paths.stdout, "utf-8") : "";
1111
- const stderr = existsSync2(turn.paths.stderr) ? readFileSync2(turn.paths.stderr, "utf-8") : "";
1112
- return JSON.stringify({ stdout, stderr }, null, 2);
1113
- }
1114
- if (outputMode === "events") {
1115
- return existsSync2(turn.paths.events) ? readFileSync2(turn.paths.events, "utf-8").trimEnd() : "";
1116
- }
1117
- return JSON.stringify(turn, null, 2);
1118
- }
1119
- function renderStatus(run) {
1120
- return JSON.stringify(run, null, 2);
1121
- }
1122
- function writeEvents(path, events) {
1123
- writeFileSyncSafe(path, `${events.map((event2) => JSON.stringify(event2)).join("\n")}
274
+ function reportNoMatch(ctx) {
275
+ const { manifest, extensionName, iface, parsed } = ctx;
276
+ const HTTP_VERBS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
277
+ const trailingVerb = parsed.segments.map((s) => s.toUpperCase()).find((s) => HTTP_VERBS.has(s));
278
+ if (trailingVerb) {
279
+ const rest = parsed.segments.filter((s) => s.toUpperCase() !== trailingVerb);
280
+ process.stderr.write(`No route matching: ${parsed.segments.join(" ")}
1124
281
  `);
1125
- }
1126
- function writeFileSyncSafe(path, content) {
1127
- writeFileSync2(path, content);
1128
- }
1129
- function attachHelp() {
1130
- return [
1131
- "Usage: godmode agent attach run <id>",
1132
- " or: godmode agent attach session <zmx-session-id>"
1133
- ].join("\n");
1134
- }
1135
- var AgentHelp = class extends HelpPage {
1136
- usage() {
1137
- return [
1138
- "godmode agent start [--harness <name>] [--model <id>] <prompt>",
1139
- "godmode agent send [--harness <name>] [--model <id>] <prompt>",
1140
- "godmode agent attach run <id>",
1141
- "godmode agent attach session <session-id>",
1142
- "godmode agent output [id] [--json|--assistant-text|--events|--raw]",
1143
- "godmode agent status [id]",
1144
- "godmode agent list"
1145
- ];
1146
- }
1147
- sections() {
1148
- return [
1149
- { title: "Options:", rows: [
1150
- [" --harness <name>", "coding agent harness (claude, codex, gemini, etc.)"],
1151
- [" --model <id>", "model identifier passed to the harness"],
1152
- [" --effort <level>", "effort level passed to the harness"],
1153
- [" --json", "output machine-readable events"],
1154
- [" --follow", "stream output until the run completes"]
1155
- ] },
1156
- { title: "Shortcuts:", rows: [
1157
- ["godmode agent <prompt>", "send prompt to active run, or start a new run"]
1158
- ] }
1159
- ];
1160
- }
1161
- };
1162
- function agentHelp() {
1163
- const lines = [];
1164
- const origLog = console.log;
1165
- console.log = (...args) => {
1166
- lines.push(args.join(" "));
1167
- };
1168
- try {
1169
- new AgentHelp().render();
1170
- } finally {
1171
- console.log = origLog;
282
+ process.stderr.write(`Method goes first: try 'godmode ${extensionName} ${iface} ${trailingVerb} ${rest.join(" ")}'
283
+ `);
284
+ return;
1172
285
  }
1173
- return lines.join("\n");
1174
- }
1175
- function followOutput(run, turn, outputMode, writer) {
1176
- const dir = turnDir(run.id, turn);
1177
- const stdoutPath = resolve22(dir, "stdout.log");
1178
- const stderrPath = resolve22(dir, "stderr.log");
1179
- const statePath = runPath(run.id);
1180
- let assistant = "";
1181
- const emit = (event2) => {
1182
- if (outputMode === "events") writer.write(`${JSON.stringify(event2)}
286
+ process.stderr.write(`No ${parsed.method.toUpperCase()} route matching: ${parsed.segments.join(" ")}
1183
287
  `);
1184
- if (outputMode === "assistant-text") {
1185
- if (event2.type === "assistant.delta" && event2.text) writer.write(event2.text);
1186
- if (event2.type === "assistant.completed" && !assistant && event2.text) writer.write(event2.text);
1187
- }
1188
- };
1189
- streamFile(stdoutPath, (line) => {
1190
- const normalized = normalizeTurn(run.harness, run.id, turn, `${line}
1191
- `, "", timestamp());
1192
- for (const event2 of normalized.events) {
1193
- if (event2.type === "assistant.delta" && event2.text) assistant += event2.text;
1194
- emit(event2);
1195
- }
1196
- }, () => (readJson2(statePath)?.status ?? "completed") === "running");
1197
- if (outputMode === "assistant-text") writer.write("\n");
1198
- if (outputMode === "events" && existsSync2(stderrPath)) {
1199
- for (const line of readFileSync2(stderrPath, "utf-8").split(/\r?\n/).filter(Boolean)) {
1200
- writer.write(`${JSON.stringify({ ts: timestamp(), id: run.id, turn, type: "warning", message: line })}
288
+ for (const m of ["get", "post", "put", "patch", "delete"]) {
289
+ if (m === parsed.method) continue;
290
+ if (matchRoute(manifest, parsed.segments, m)) {
291
+ process.stderr.write(` try: godmode ${extensionName} api ${m.toUpperCase()} ${parsed.segments.join(" ")}
1201
292
  `);
1202
293
  }
1203
294
  }
1204
- }
1205
- function streamFile(path, onLine, until) {
1206
- let offset = 0;
1207
- let pending = "";
1208
- while (true) {
1209
- if (existsSync2(path)) {
1210
- const size = statSync(path).size;
1211
- if (size > offset) {
1212
- const fd = openSync(path, "r");
1213
- const buffer = Buffer.alloc(size - offset);
1214
- try {
1215
- const bytes = buffer.length ? readSync(fd, buffer, 0, buffer.length, offset) : 0;
1216
- offset += bytes;
1217
- pending += buffer.toString("utf-8", 0, bytes);
1218
- const lines = pending.split(/\r?\n/);
1219
- pending = lines.pop() ?? "";
1220
- for (const line of lines) if (line) onLine(line);
1221
- } finally {
1222
- closeSync(fd);
1223
- }
1224
- }
1225
- }
1226
- if (!until()) {
1227
- if (pending) onLine(pending);
1228
- return;
1229
- }
1230
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
1231
- }
1232
- }
1233
- function readJson2(path) {
1234
- try {
1235
- return JSON.parse(readFileSync2(path, "utf-8"));
1236
- } catch {
1237
- return null;
1238
- }
1239
- }
1240
- function defaultExecutor(command, args, stdio = "pipe") {
1241
- const spawnStdio = stdio === "inherit" ? "inherit" : "pipe";
1242
- const result = spawnSync(command, args, { encoding: "utf-8", stdio: spawnStdio });
1243
- return {
1244
- status: result.status,
1245
- stdout: stdio === "pipe" && typeof result.stdout === "string" ? result.stdout : "",
1246
- stderr: stdio === "pipe" && typeof result.stderr === "string" ? result.stderr : "",
1247
- error: result.error ?? void 0
1248
- };
1249
- }
1250
- function parseOutputMode(args) {
1251
- let outputMode = "json";
1252
- let follow = false;
1253
- const remaining = [];
1254
- for (const arg of args) {
1255
- if (arg === "--json") outputMode = "json";
1256
- else if (arg === "--assistant-text") outputMode = "assistant-text";
1257
- else if (arg === "--events") outputMode = "events";
1258
- else if (arg === "--raw") outputMode = "raw";
1259
- else if (arg === "--follow") follow = true;
1260
- else remaining.push(arg);
1261
- }
1262
- return { outputMode, follow, remaining };
1263
- }
1264
- function parseStartSendArgs(action, argv) {
1265
- const { outputMode, remaining } = parseOutputMode(argv);
1266
- let harness;
1267
- let model;
1268
- let effort;
1269
- let passthroughArgs = [];
1270
- const promptParts = [];
1271
- for (let i = 0; i < remaining.length; i++) {
1272
- const arg = remaining[i];
1273
- if (arg === "--") {
1274
- passthroughArgs = remaining.slice(i + 1);
1275
- break;
1276
- }
1277
- if (arg === "--harness") {
1278
- const value = remaining[++i];
1279
- if (!value || !(value in HARNESSES)) throw new Error("Missing or invalid value for --harness");
1280
- harness = value;
1281
- continue;
1282
- }
1283
- if (arg === "--model") {
1284
- model = remaining[++i];
1285
- if (!model) throw new Error("Missing value for --model");
1286
- continue;
1287
- }
1288
- if (arg === "--effort") {
1289
- effort = remaining[++i];
1290
- if (!effort) throw new Error("Missing value for --effort");
1291
- continue;
295
+ const similar = suggestRoutes(manifest, parsed.segments).slice(0, 5);
296
+ if (similar.length) {
297
+ process.stderr.write("\nSimilar:\n");
298
+ const seen = /* @__PURE__ */ new Set();
299
+ for (const r of similar) {
300
+ const p = r.segments.map((s) => s.isParam ? `{${s.value}}` : s.value).join(" ");
301
+ if (seen.has(p)) continue;
302
+ seen.add(p);
303
+ process.stderr.write(` ${p}
304
+ `);
1292
305
  }
1293
- if (arg === "--help" || arg === "-h") throw new Error(agentHelp());
1294
- if (arg.startsWith("-")) throw new Error(`Unknown flag: ${arg}. Use -- to pass flags through to the native harness.`);
1295
- promptParts.push(arg);
1296
306
  }
1297
- return { action, prompt: promptParts.join(" ").trim(), harness, model, effort, passthroughArgs, outputMode };
1298
- }
1299
- function parseOutputArgs(argv) {
1300
- const { outputMode, follow, remaining } = parseOutputMode(argv);
1301
- return { outputMode, follow, id: remaining.find((arg) => !arg.startsWith("-")) };
1302
307
  }
1303
- function classifyStatus(exitCode, stdout, stderr) {
1304
- const text = `${stdout}
1305
- ${stderr}`.toLowerCase();
1306
- if (exitCode === 0) return "completed";
1307
- if (text.includes("auth") || text.includes("api key") || text.includes("permission") || text.includes("may not exist")) return "blocked";
1308
- return "failed";
1309
- }
1310
- function resolveRunAndHarness(parsed, executor, cwd, options) {
1311
- const settings = options.settings ?? loadSettings(cwd);
1312
- const plugin = settings.plugins?.["coding-agents"];
1313
- const requestedHarnessId = parsed.harness ?? plugin?.harness;
1314
- let model = parsed.model ?? plugin?.model;
1315
- let effort = parsed.effort ?? plugin?.effort;
1316
- let run;
1317
- let harness;
1318
- if (parsed.action === "start") {
1319
- harness = detectHarness(executor, requestedHarnessId);
1320
- if (effort && harness.effortFlags.length === 0) throw new Error(`Harness "${harness.id}" does not support --effort.`);
1321
- run = createRun(cwd, harness, model, effort);
1322
- } else {
1323
- const active = activeRunIdForCwd(cwd);
1324
- if (active) {
1325
- run = loadRun(active);
1326
- if (parsed.harness && parsed.harness !== run.harness) throw new Error(`Active run uses harness ${run.harness}; start a new run for ${parsed.harness}.`);
1327
- harness = detectHarness(executor, run.harness);
1328
- model = parsed.model ?? run.model ?? plugin?.model ?? void 0;
1329
- effort = parsed.effort ?? run.effort ?? plugin?.effort ?? void 0;
1330
- if (effort && harness.effortFlags.length === 0) throw new Error(`Harness "${harness.id}" does not support --effort.`);
1331
- run.model = model ?? run.model;
1332
- run.effort = effort ?? run.effort;
1333
- } else {
1334
- harness = detectHarness(executor, requestedHarnessId);
1335
- if (effort && harness.effortFlags.length === 0) throw new Error(`Harness "${harness.id}" does not support --effort.`);
1336
- run = createRun(cwd, harness, model, effort);
308
+ var ApiInterface = class extends Interface {
309
+ validate() {
310
+ if (!this.ctx.parsed.explicitMethod) {
311
+ return `Missing HTTP method. Try: godmode ${this.ctx.extensionName} api GET ${this.ctx.parsed.segments.join(" ")}
312
+ Valid methods: GET, POST, PUT, PATCH, DELETE, HEAD.`;
1337
313
  }
314
+ return null;
1338
315
  }
1339
- run.status = "running";
1340
- run.model = model ?? run.model;
1341
- run.effort = effort ?? run.effort;
1342
- run.lastTurn += 1;
1343
- saveRun(run);
1344
- saveActiveRun(cwd, run.id);
1345
- return { run, harness };
1346
- }
1347
- function executeTurn(parsed, options) {
1348
- const executor = options.executor ?? defaultExecutor;
1349
- const writer = options.writer ?? process.stdout;
1350
- const errorWriter = options.errorWriter ?? process.stderr;
1351
- const cwd = options.cwd ?? process.cwd();
1352
- const zmxCheck = executor("zmx", ["--help"]);
1353
- if (zmxCheck.error || zmxCheck.status !== 0) {
1354
- errorWriter.write("zmx not found. Install zmx to use coding agents.\n");
1355
- return 2;
1356
- }
1357
- const { run, harness } = resolveRunAndHarness(parsed, executor, cwd, options);
1358
- const { shell, stdoutPath, stderrPath, exitCodePath } = buildShellCommand(harness, run, run.lastTurn, parsed.prompt, parsed.passthroughArgs);
1359
- const launch = executor("zmx", ["run", run.sessions.zmx, "sh", "-lc", shell], "ignore");
1360
- if (launch.error || launch.status !== 0) {
1361
- run.status = "failed";
1362
- saveRun(run);
1363
- errorWriter.write(`Failed to queue turn in zmx session ${run.sessions.zmx}.
1364
- `);
1365
- return 3;
316
+ execute() {
317
+ return executeRoute(this.ctx);
1366
318
  }
1367
- waitForFile(exitCodePath);
1368
- const exitCode = Number(readFileSync3(exitCodePath, "utf-8").trim() || "1");
1369
- const stdout = existsSync3(stdoutPath) ? readFileSync3(stdoutPath, "utf-8") : "";
1370
- const stderr = existsSync3(stderrPath) ? readFileSync3(stderrPath, "utf-8") : "";
1371
- const normalized = normalizeTurn(run.harness, run.id, run.lastTurn, stdout, stderr, (/* @__PURE__ */ new Date()).toISOString());
1372
- const turnStatus = classifyStatus(exitCode, stdout, stderr);
1373
- const turn = {
1374
- id: run.id,
1375
- turn: run.lastTurn,
1376
- status: turnStatus,
1377
- prompt: parsed.prompt,
1378
- harness: run.harness,
1379
- sessions: {
1380
- zmx: run.sessions.zmx,
1381
- ...normalized.sessionId || run.sessions[run.harness] ? { [run.harness]: normalized.sessionId ?? run.sessions[run.harness] } : {}
1382
- },
1383
- assistant: { text: normalized.assistantText },
1384
- usage: normalized.usage,
1385
- timing: normalized.timing,
1386
- paths: {
1387
- stdout: stdoutPath,
1388
- stderr: stderrPath,
1389
- events: resolve3(turnDir(run.id, run.lastTurn), "events.jsonl")
1390
- },
1391
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
1392
- };
1393
- writeEvents(turn.paths.events, normalized.events);
1394
- writeJson(resolve3(turnDir(run.id, run.lastTurn), "normalized.json"), turn);
1395
- if (turn.sessions[run.harness]) run.sessions[run.harness] = turn.sessions[run.harness];
1396
- run.status = turnStatus;
1397
- saveRun(run);
1398
- writer.write(`${renderTurn(turn, parsed.outputMode)}
1399
- `);
1400
- return turnStatus === "completed" ? 0 : 1;
1401
- }
1402
- function attachToTarget(argv, options) {
1403
- const executor = options.executor ?? defaultExecutor;
1404
- const errorWriter = options.errorWriter ?? process.stderr;
1405
- if (argv.length < 2) {
1406
- errorWriter.write(`${attachHelp()}
1407
- `);
1408
- return 1;
319
+ };
320
+ var GraphqlInterface = class extends Interface {
321
+ validate() {
322
+ const { parsed, extensionName } = this.ctx;
323
+ return validateGraphQLFlags(parsed.method, parsed.query, parsed.body, extensionName);
1409
324
  }
1410
- const session = argv[0] === "run" ? loadRun(argv[1]).sessions.zmx : argv[0] === "session" ? argv[1] : null;
1411
- if (!session) {
1412
- errorWriter.write(`${attachHelp()}
1413
- `);
1414
- return 1;
325
+ execute() {
326
+ return executeRoute(this.ctx);
1415
327
  }
1416
- return executor("zmx", ["attach", session], "inherit").status ?? 1;
1417
- }
1418
- function outputCommand(argv, options) {
1419
- const writer = options.writer ?? process.stdout;
1420
- const cwd = options.cwd ?? process.cwd();
1421
- const parsed = parseOutputArgs(argv);
1422
- const run = loadRun(resolveRunId(cwd, parsed.id));
1423
- if (parsed.follow && run.status === "running") {
1424
- followOutput(run, run.lastTurn, parsed.outputMode, writer);
1425
- return 0;
328
+ };
329
+ var McpInterface = class extends Interface {
330
+ async handleEmpty() {
331
+ const { extensionName, rawRest } = this.ctx;
332
+ await runMcp(
333
+ {
334
+ godmodeHome: GODMODE_HOME,
335
+ loadManifest: (n) => loadManifest(n, "mcp"),
336
+ checkPermission
337
+ },
338
+ [extensionName, ...rawRest]
339
+ );
1426
340
  }
1427
- writer.write(`${renderTurn(latestTurn(run), parsed.outputMode)}
1428
- `);
1429
- return 0;
1430
- }
1431
- function statusCommand(argv, options) {
1432
- const writer = options.writer ?? process.stdout;
1433
- const cwd = options.cwd ?? process.cwd();
1434
- writer.write(`${renderStatus(loadRun(resolveRunId(cwd, argv.find((arg) => !arg.startsWith("-")))))}
1435
- `);
1436
- return 0;
1437
- }
1438
- function listCommand(options) {
1439
- const writer = options.writer ?? process.stdout;
1440
- writer.write(`${JSON.stringify(listRuns(), null, 2)}
1441
- `);
1442
- return 0;
1443
- }
1444
- async function runAgentCommand(argv, options = {}) {
1445
- ensureDirs();
1446
- const writer = options.writer ?? process.stdout;
1447
- const errorWriter = options.errorWriter ?? process.stderr;
1448
- if (!argv.length) {
1449
- writer.write(`${agentHelp()}
341
+ validate() {
342
+ const { parsed } = this.ctx;
343
+ return validateMcpFlags(parsed.method, parsed.query);
344
+ }
345
+ async execute() {
346
+ const { manifest, parsed, extensionName } = this.ctx;
347
+ const tool = parsed.segments[0];
348
+ const check = checkPermission({
349
+ extension: extensionName,
350
+ resource: resourceFromTool(tool),
351
+ method: "mcp"
352
+ });
353
+ if (!check.allowed) {
354
+ process.stderr.write(`Blocked: ${check.reason}
1450
355
  `);
1451
- return 0;
1452
- }
1453
- try {
1454
- const [cmd, ...rest] = argv;
1455
- if (cmd === "--help" || cmd === "-h" || cmd === "help") {
1456
- writer.write(`${agentHelp()}
356
+ process.stderr.write(`Suggested allow rule:
357
+ ${suggestedAllowRule({ extension: extensionName, resource: resourceFromTool(tool), method: "mcp" })}
1457
358
  `);
1458
- return 0;
359
+ process.exit(EXIT_CODES.permissionDenied);
1459
360
  }
1460
- if (cmd === "start") return executeTurn(parseStartSendArgs("start", rest), options);
1461
- if (cmd === "send") return executeTurn(parseStartSendArgs("send", rest), options);
1462
- if (cmd === "attach") return attachToTarget(rest, options);
1463
- if (cmd === "output") return outputCommand(rest, options);
1464
- if (cmd === "status") return statusCommand(rest, options);
1465
- if (cmd === "list") return listCommand(options);
1466
- return executeTurn(parseStartSendArgs("send", argv), options);
1467
- } catch (error) {
1468
- errorWriter.write(`${error.message || error}
1469
- `);
1470
- return 1;
361
+ const result = await executeMcpTool(manifest.config, tool, parsed.body, {
362
+ debug: parsed.debug,
363
+ dryRun: parsed.dryRun
364
+ });
365
+ if (result) process.stdout.write(result + "\n");
366
+ }
367
+ };
368
+ function getInterface(ctx) {
369
+ switch (ctx.iface) {
370
+ case "api":
371
+ return new ApiInterface(ctx);
372
+ case "graphql":
373
+ return new GraphqlInterface(ctx);
374
+ case "mcp":
375
+ return new McpInterface(ctx);
376
+ default:
377
+ throw new Error(`Unknown interface: ${ctx.iface}`);
1471
378
  }
1472
379
  }
1473
380
 
1474
381
  // src/index.ts
1475
382
  loadEnv();
1476
383
  var VALID_INTERFACES = /* @__PURE__ */ new Set(["api", "graphql", "mcp", "skill"]);
1477
- var RESERVED_SLUGS = /* @__PURE__ */ new Set(["ext", "agent"]);
1478
384
  async function main() {
1479
385
  const args = process.argv.slice(2);
1480
386
  if (!args.length || args.length === 1 && (args[0] === "-h" || args[0] === "--help")) {
387
+ warnSettingsErrors();
1481
388
  showHelp();
1482
389
  return;
1483
390
  }
@@ -1487,15 +394,12 @@ async function main() {
1487
394
  }
1488
395
  const extensionSlug = args[0];
1489
396
  const rest = args.slice(1);
1490
- if (extensionSlug === "ext") {
1491
- await runExt(rest);
397
+ const builtin = BUILTINS.get(extensionSlug);
398
+ if (builtin) {
399
+ await builtin.run(rest);
1492
400
  return;
1493
401
  }
1494
- if (extensionSlug === "agent") {
1495
- await runAgent(rest);
1496
- return;
1497
- }
1498
- const multi = installedExtension(extensionSlug);
402
+ const multi = findInstalledManifestSync(extensionSlug);
1499
403
  if (!multi) {
1500
404
  process.stderr.write(`'${extensionSlug}' is not an installed extension.
1501
405
  `);
@@ -1503,9 +407,9 @@ async function main() {
1503
407
  `);
1504
408
  process.stderr.write(`Try 'godmode --help' for more information.
1505
409
  `);
1506
- process.exit(1);
410
+ process.exit(EXIT_CODES.notFound);
1507
411
  }
1508
- const declared = declaredInterfaces(multi);
412
+ const declared = Object.keys(multi.interfaces);
1509
413
  const first = rest[0];
1510
414
  if (!first || first === "--help" || first === "-h") {
1511
415
  showExtensionOverview(multi);
@@ -1522,7 +426,7 @@ async function main() {
1522
426
  `);
1523
427
  process.stderr.write(`Try 'godmode ${extensionSlug} ${declared[0]} ${first}' or 'godmode ${extensionSlug} --help'.
1524
428
  `);
1525
- process.exit(1);
429
+ process.exit(EXIT_CODES.usage);
1526
430
  }
1527
431
  if (!declared.includes(first)) {
1528
432
  process.stderr.write(`'${extensionSlug}' does not declare a '${first}' interface.
@@ -1531,229 +435,47 @@ async function main() {
1531
435
  `);
1532
436
  process.stderr.write(`Try 'godmode ${extensionSlug} --help' for more information.
1533
437
  `);
1534
- process.exit(1);
438
+ process.exit(EXIT_CODES.usage);
1535
439
  }
1536
440
  await runInterface(first, extensionSlug, rest.slice(1));
1537
441
  }
1538
- function installedExtension(name) {
1539
- const extPath = resolve5(GODMODE_EXTENSIONS_DIR, `${name}.json`);
1540
- if (!existsSync4(extPath)) return null;
1541
- try {
1542
- return JSON.parse(readFileSync5(extPath, "utf-8"));
1543
- } catch {
1544
- return null;
1545
- }
1546
- }
1547
- function declaredInterfaces(m) {
1548
- return Object.keys(m.interfaces);
1549
- }
1550
- function showExtHelp() {
1551
- console.log(`Install, inspect, and manage godmode extensions.
1552
-
1553
- Usage: godmode ext <command> [args]
1554
-
1555
- Commands:
1556
- install <name|folder> Install an extension
1557
- uninstall <name> Uninstall an extension
1558
- update <name> Re-fetch spec, rebuild routes
1559
- list Show installed extensions
1560
- create Interactive manifest wizard`);
1561
- }
1562
- async function runExt(rest) {
1563
- const cmd = rest[0];
1564
- if (!cmd || cmd === "--help" || cmd === "-h") {
1565
- showExtHelp();
1566
- return;
1567
- }
1568
- if (cmd === "install") {
1569
- const target = rest[1];
1570
- if (!target) {
1571
- console.error("Usage: godmode ext install <name|folder>");
1572
- process.exit(1);
1573
- }
1574
- if (RESERVED_SLUGS.has(target)) {
1575
- process.stderr.write(`'${target}' is a reserved extension slug and cannot be installed.
1576
- `);
1577
- process.stderr.write(`Reserved: ${[...RESERVED_SLUGS].join(", ")}.
1578
- `);
1579
- process.exit(1);
1580
- }
1581
- const { runAdd } = await import("./add-AJERMPEA.js");
1582
- await runAdd(rest.slice(1));
1583
- return;
1584
- }
1585
- if (cmd === "uninstall") {
1586
- if (!rest[1]) {
1587
- console.error("Usage: godmode ext uninstall <name>");
1588
- process.exit(1);
1589
- }
1590
- await removeApi(rest[1]);
1591
- return;
1592
- }
1593
- if (cmd === "update") {
1594
- if (!rest[1]) {
1595
- console.error("Usage: godmode ext update <name>");
1596
- process.exit(1);
1597
- }
1598
- await updateApi(rest[1]);
1599
- return;
1600
- }
1601
- if (cmd === "list") {
1602
- await listApis();
1603
- return;
1604
- }
1605
- if (cmd === "create") {
1606
- const { configWizard } = await import("./prompt-SWOWWAOH.js");
1607
- await configWizard();
1608
- return;
1609
- }
1610
- if (installedExtension(cmd)) {
1611
- const multi = installedExtension(cmd);
1612
- const ifaces = declaredInterfaces(multi);
1613
- process.stderr.write(`'${cmd}' is an installed extension, not an ext command.
1614
-
1615
- `);
1616
- process.stderr.write(`Did you mean:
1617
- `);
1618
- process.stderr.write(` godmode ${cmd} --help
1619
- `);
1620
- for (const i of ifaces) {
1621
- process.stderr.write(` godmode ${cmd} ${i} --help
1622
- `);
1623
- }
1624
- process.stderr.write(`
1625
- Try 'godmode ext --help' for more information.
1626
- `);
1627
- process.exit(1);
1628
- }
1629
- process.stderr.write(`Unknown ext command '${cmd}'.
1630
- `);
1631
- process.stderr.write(`Try 'godmode ext --help' for more information.
1632
- `);
1633
- process.exit(1);
1634
- }
1635
- async function runAgent(rest) {
1636
- const code = await runAgentCommand(rest);
1637
- if (code !== 0) process.exit(code);
1638
- }
1639
442
  async function runInterface(iface, extensionName, rest) {
1640
443
  if (iface === "skill") {
1641
444
  process.stderr.write(`Skill interface not yet implemented.
1642
445
  `);
1643
- process.exit(1);
446
+ process.exit(EXIT_CODES.usage);
1644
447
  }
1645
448
  if (rest.includes("--version") || rest.includes("-v")) {
1646
449
  const multi2 = await loadMultiManifest(extensionName);
1647
450
  showExtensionVersion(multi2);
1648
451
  return;
1649
452
  }
1650
- const parsed = parseArgs(rest);
1651
453
  const ifaceKey = iface;
454
+ const parsed = parseArgs(rest);
1652
455
  const multi = await loadMultiManifest(extensionName);
1653
456
  const manifest = await loadManifest(extensionName, ifaceKey);
1654
- const implicitMethodFilter = parsed.methodFilter || (parsed.explicitMethod ? parsed.method : void 0);
457
+ const handler = getInterface({
458
+ iface: ifaceKey,
459
+ extensionName,
460
+ manifest,
461
+ multi,
462
+ parsed,
463
+ rawRest: rest
464
+ });
1655
465
  if (parsed.help) {
1656
- showApiHelp(manifest, extensionName, parsed.segments, parsed.filter, implicitMethodFilter, parsed.all, multi);
1657
- return;
1658
- }
1659
- if (iface === "mcp" && !parsed.segments.length) {
1660
- await runMcp(
1661
- { godmodeHome: GODMODE_HOME, loadManifest: (n) => loadManifest(n, "mcp") },
1662
- [extensionName, ...rest]
1663
- );
466
+ handler.showHelp();
1664
467
  return;
1665
468
  }
1666
469
  if (!parsed.segments.length) {
1667
- showApiHelp(manifest, extensionName, parsed.segments, parsed.filter, implicitMethodFilter, parsed.all, multi);
470
+ await handler.handleEmpty();
1668
471
  return;
1669
472
  }
1670
- if (iface === "api" && !parsed.explicitMethod) {
1671
- process.stderr.write(`Missing HTTP method. Try: godmode ${extensionName} api GET ${parsed.segments.join(" ")}
1672
- `);
1673
- process.stderr.write(`Valid methods: GET, POST, PUT, PATCH, DELETE, HEAD.
1674
- `);
1675
- process.exit(1);
473
+ const err = handler.validate();
474
+ if (err) {
475
+ process.stderr.write(err + "\n");
476
+ process.exit(EXIT_CODES.usage);
1676
477
  }
1677
- if (iface === "graphql") {
1678
- const err = validateGraphQLFlags(parsed.method, parsed.query, parsed.body, extensionName);
1679
- if (err) {
1680
- process.stderr.write(err + "\n");
1681
- process.exit(1);
1682
- }
1683
- }
1684
- if (manifest.config.type === "mcp") {
1685
- const err = validateMcpFlags(parsed.method, parsed.query);
1686
- if (err) {
1687
- process.stderr.write(err + "\n");
1688
- process.exit(1);
1689
- }
1690
- const result = await executeMcpTool(manifest.config, parsed.segments[0], parsed.body, {
1691
- verbose: parsed.verbose,
1692
- dryRun: parsed.dryRun
1693
- });
1694
- if (result) process.stdout.write(result + "\n");
1695
- return;
1696
- }
1697
- const query = parsed.query;
1698
- const hasBody = Object.keys(parsed.body).length > 0;
1699
- let body = hasBody ? JSON.stringify(parsed.body) : void 0;
1700
- if (!body && ["post", "put", "patch"].includes(parsed.method)) body = await readStdin();
1701
- if (parsed.segments[0]?.startsWith("/")) {
1702
- const rawPath = parsed.segments[0];
1703
- const syntheticRoute = { path: rawPath, method: parsed.method, summary: "", version: "", segments: [] };
1704
- await execute(manifest, { route: syntheticRoute, params: {} }, {
1705
- headers: parsed.headers,
1706
- query,
1707
- body,
1708
- verbose: parsed.verbose,
1709
- dryRun: parsed.dryRun
1710
- });
1711
- return;
1712
- }
1713
- const match = matchRoute(manifest, parsed.segments, parsed.method);
1714
- if (!match) {
1715
- const HTTP_VERBS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
1716
- const trailingVerb = parsed.segments.map((s) => s.toUpperCase()).find((s) => HTTP_VERBS.has(s));
1717
- if (trailingVerb) {
1718
- const rest2 = parsed.segments.filter((s) => s.toUpperCase() !== trailingVerb);
1719
- process.stderr.write(`No route matching: ${parsed.segments.join(" ")}
1720
- `);
1721
- process.stderr.write(`Method goes first: try 'godmode ${extensionName} ${iface} ${trailingVerb} ${rest2.join(" ")}'
1722
- `);
1723
- process.exit(1);
1724
- }
1725
- process.stderr.write(`No ${parsed.method.toUpperCase()} route matching: ${parsed.segments.join(" ")}
1726
- `);
1727
- for (const m of ["get", "post", "put", "patch", "delete"]) {
1728
- if (m === parsed.method) continue;
1729
- const alt = matchRoute(manifest, parsed.segments, m);
1730
- if (alt) {
1731
- process.stderr.write(` try: godmode ${extensionName} api ${m.toUpperCase()} ${parsed.segments.join(" ")}
1732
- `);
1733
- }
1734
- }
1735
- const similar = suggestRoutes(manifest, parsed.segments).slice(0, 5);
1736
- if (similar.length) {
1737
- process.stderr.write("\nSimilar:\n");
1738
- const seen = /* @__PURE__ */ new Set();
1739
- for (const r of similar) {
1740
- const p = r.segments.map((s) => s.isParam ? `{${s.value}}` : s.value).join(" ");
1741
- if (seen.has(p)) continue;
1742
- seen.add(p);
1743
- process.stderr.write(` ${p}
1744
- `);
1745
- }
1746
- }
1747
- process.exit(1);
1748
- }
1749
- await execute(manifest, match, {
1750
- headers: parsed.headers,
1751
- query,
1752
- body,
1753
- token: parsed.token,
1754
- verbose: parsed.verbose,
1755
- dryRun: parsed.dryRun
1756
- });
478
+ await handler.execute();
1757
479
  }
1758
480
  main().catch((err) => {
1759
481
  process.stderr.write(`${err.message || err}