godmode 0.0.1 → 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,18 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- execute
4
- } from "./chunk-I4WS4MMI.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";
5
21
  import {
6
- listApis,
22
+ warnSettingsErrors
23
+ } from "./chunk-SMQBMG24.js";
24
+ import {
25
+ BUILTINS,
26
+ GODMODE_HOME,
27
+ findInstalledManifestSync,
7
28
  loadManifest,
8
- removeApi,
9
- updateApi,
29
+ loadMultiManifest,
10
30
  validateGraphQLFlags
11
- } from "./chunk-FBMQ3AC4.js";
31
+ } from "./chunk-KHFZI7IA.js";
32
+ import {
33
+ execute
34
+ } from "./chunk-AWGLXW3B.js";
12
35
  import {
13
36
  executeMcpTool,
14
37
  validateMcpFlags
15
- } from "./chunk-EHS56XXY.js";
38
+ } from "./chunk-3AQ2CZKC.js";
39
+ import "./chunk-YDXTIN53.js";
16
40
 
17
41
  // src/env.ts
18
42
  import { readFileSync } from "fs";
@@ -35,100 +59,24 @@ function loadEnv() {
35
59
  }
36
60
  }
37
61
 
38
- // src/match.ts
39
- function matchRoute(manifest, userSegments, method) {
40
- if (userSegments.length > 0) {
41
- const ver = manifest.versions.find((v) => v.name === userSegments[0]);
42
- if (ver) {
43
- const match = findMatch(manifest.routes, userSegments.slice(1), method, ver.name);
44
- if (match) return match;
45
- }
46
- }
47
- return findMatch(manifest.routes, userSegments, method);
48
- }
49
- function findMatch(routes, userSegments, method, version) {
50
- let bestMatch = null;
51
- let bestScore = -1;
52
- for (const route of routes) {
53
- if (route.method !== method) continue;
54
- if (route.segments.length !== userSegments.length) continue;
55
- if (version !== void 0 && route.version !== version) continue;
56
- let score = 0;
57
- const params = {};
58
- let matches = true;
59
- for (let i = 0; i < route.segments.length; i++) {
60
- const seg = route.segments[i];
61
- const userSeg = userSegments[i];
62
- if (seg.isParam) {
63
- params[seg.value] = userSeg;
64
- } else if (seg.value === userSeg) {
65
- score += 1;
66
- } else {
67
- matches = false;
68
- break;
69
- }
70
- }
71
- if (matches && (score > bestScore || score === bestScore && isLaterVersion(route, bestMatch?.route))) {
72
- bestScore = score;
73
- bestMatch = { route, params };
74
- }
75
- }
76
- return bestMatch;
77
- }
78
- function isLaterVersion(a, b) {
79
- if (!b) return true;
80
- return a.version > b.version;
81
- }
82
- function suggestRoutes(manifest, userSegments) {
83
- if (!userSegments.length) return [];
84
- return manifest.routes.filter((route) => {
85
- if (route.segments.length < userSegments.length) return false;
86
- const firstSeg = route.segments[0];
87
- return !firstSeg.isParam && firstSeg.value === userSegments[0];
88
- });
89
- }
90
-
91
62
  // src/args.ts
63
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
92
64
  function parseArgs(args) {
93
65
  const segments = [];
94
66
  const headers = {};
95
67
  const query = {};
96
68
  const body = {};
97
69
  let method = "get";
98
- let token;
70
+ let explicitMethod = false;
99
71
  let filter;
100
72
  let methodFilter;
101
73
  let all = false;
102
- let verbose = false;
103
- let dryRun = false;
74
+ const debug = process.env.GODMODE_DEBUG === "1";
75
+ const dryRun = process.env.GODMODE_DRY_RUN === "1";
104
76
  let help = false;
105
77
  for (let i = 0; i < args.length; i++) {
106
78
  const arg = args[i];
107
79
  switch (arg) {
108
- // HTTP methods
109
- case "-g":
110
- case "--get":
111
- method = "get";
112
- break;
113
- case "-po":
114
- case "--post":
115
- method = "post";
116
- break;
117
- case "-pu":
118
- case "--put":
119
- method = "put";
120
- break;
121
- case "-pa":
122
- case "--patch":
123
- method = "patch";
124
- break;
125
- case "-d":
126
- case "--delete":
127
- method = "delete";
128
- break;
129
- case "--head":
130
- method = "head";
131
- break;
132
80
  // Options
133
81
  case "-H":
134
82
  case "--header": {
@@ -137,25 +85,18 @@ function parseArgs(args) {
137
85
  if (idx > 0) headers[val.slice(0, idx).trim()] = val.slice(idx + 1).trim();
138
86
  break;
139
87
  }
140
- case "--token":
141
- token = args[++i];
142
- break;
88
+ case "-F":
143
89
  case "--filter":
144
90
  filter = args[++i];
145
91
  break;
92
+ case "-X":
146
93
  case "--method":
147
94
  methodFilter = args[++i];
148
95
  break;
96
+ case "-A":
149
97
  case "--all":
150
98
  all = true;
151
99
  break;
152
- case "-v":
153
- case "--verbose":
154
- verbose = true;
155
- break;
156
- case "--dry-run":
157
- dryRun = true;
158
- break;
159
100
  case "-h":
160
101
  case "--help":
161
102
  help = true;
@@ -166,6 +107,11 @@ function parseArgs(args) {
166
107
  `);
167
108
  process.exit(1);
168
109
  }
110
+ if (!segments.length && !explicitMethod && HTTP_METHODS.has(arg.toUpperCase())) {
111
+ method = arg.toLowerCase();
112
+ explicitMethod = true;
113
+ break;
114
+ }
169
115
  const eqeq = arg.indexOf("==");
170
116
  const eq = arg.indexOf("=");
171
117
  if (eqeq > 0) {
@@ -177,8 +123,7 @@ function parseArgs(args) {
177
123
  }
178
124
  }
179
125
  }
180
- if (Object.keys(body).length && method === "get") method = "post";
181
- return { segments, method, headers, query, body, token, filter, methodFilter, all, verbose, dryRun, help };
126
+ return { segments, method, explicitMethod, headers, query, body, filter, methodFilter, all, debug, dryRun, help };
182
127
  }
183
128
  async function readStdin() {
184
129
  if (process.stdin.isTTY) return void 0;
@@ -188,423 +133,349 @@ async function readStdin() {
188
133
  return text || void 0;
189
134
  }
190
135
 
191
- // src/help.ts
192
- import fuzzysort from "fuzzysort";
193
- function buildTrie(routes) {
194
- const root = { name: "", isParam: false, methods: [], children: [] };
195
- for (const route of routes) {
196
- let node = root;
197
- for (const seg of route.segments) {
198
- let child = node.children.find((c) => c.name === seg.value && c.isParam === seg.isParam);
199
- if (!child) {
200
- child = { name: seg.value, isParam: seg.isParam, methods: [], children: [] };
201
- node.children.push(child);
202
- }
203
- node = child;
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.
144
+
145
+ Usage:
146
+ godmode mcp <name> [options]
147
+
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);
155
+ }
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];
161
+ }
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;
204
177
  }
205
- node.methods.push({ method: route.method, summary: route.summary });
178
+ } catch {
206
179
  }
207
- return root;
180
+ const manifest = await rt.loadManifest(name);
181
+ const { serveMcp } = await import("./server-L3L4TKK3.js");
182
+ await serveMcp(manifest, { filter, method, checkPermission: rt.checkPermission });
208
183
  }
209
- function navigateTrie(root, segments) {
210
- let node = root;
211
- const fullPath = [];
212
- const params = [];
213
- for (const seg of segments) {
214
- const staticChild = node.children.find((c) => !c.isParam && c.name === seg);
215
- if (staticChild) {
216
- fullPath.push(seg);
217
- node = staticChild;
218
- continue;
219
- }
220
- const paramChild = node.children.find((c) => c.isParam);
221
- if (paramChild) {
222
- const nested = paramChild.children.find((c) => !c.isParam && c.name === seg);
223
- if (nested) {
224
- fullPath.push(`<${paramChild.name}>`, seg);
225
- params.push({ name: paramChild.name, provided: false });
226
- node = nested;
227
- continue;
228
- }
229
- fullPath.push(seg);
230
- params.push({ name: paramChild.name, provided: true, value: seg });
231
- node = paramChild;
232
- continue;
233
- }
184
+
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() {
234
199
  return null;
235
200
  }
236
- return { node, fullPath, params };
237
- }
238
- function getChildren(node) {
239
- const statics = node.children.filter((c) => !c.isParam);
240
- const params = node.children.filter((c) => c.isParam);
241
- const result = [...statics];
242
- for (const p of params) result.push(...p.children);
243
- result.sort((a, b) => a.name.localeCompare(b.name));
244
- return result;
245
- }
246
- var ACTION_ORDER = ["list", "create", "get", "update", "delete"];
247
- var METHOD_LABEL = {
248
- get: "\x1B[1;38;5;34mGET\x1B[0m",
249
- post: "\x1B[1;38;5;25mPOST\x1B[0m",
250
- put: "\x1B[1;38;5;172mPUT\x1B[0m",
251
- patch: "\x1B[1;38;5;30mPATCH\x1B[0m",
252
- delete: "\x1B[1;38;5;160mDELETE\x1B[0m"
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);
205
+ }
253
206
  };
254
- var DIM = "\x1B[2m";
255
- var ITALIC = "\x1B[3m";
256
- var RESET = "\x1B[0m";
257
- function methodToAction(method, isResource) {
258
- switch (method) {
259
- case "get":
260
- return isResource ? "get" : "list";
261
- case "post":
262
- return isResource ? "update" : "create";
263
- case "put":
264
- case "patch":
265
- return "update";
266
- case "delete":
267
- return "delete";
268
- default:
269
- return method;
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();
270
214
  }
271
- }
272
- function getNodeActions(node) {
273
- const actions = /* @__PURE__ */ new Set();
274
- const paramChild = node.children.find((c) => c.isParam);
275
- const isLeaf = !paramChild;
276
- for (const ep of node.methods) actions.add(methodToAction(ep.method, isLeaf));
277
- if (paramChild) {
278
- for (const ep of paramChild.methods) actions.add(methodToAction(ep.method, true));
279
- }
280
- return [...actions].sort((a, b) => ACTION_ORDER.indexOf(a) - ACTION_ORDER.indexOf(b));
281
- }
282
- function getParamName(node) {
283
- return node.children.find((c) => c.isParam)?.name || null;
284
- }
285
- function methodLabels(node) {
286
- const methods = /* @__PURE__ */ new Set();
287
- for (const ep of node.methods) methods.add(ep.method);
288
- const paramChild = node.children.find((c) => c.isParam);
289
- if (paramChild) for (const ep of paramChild.methods) methods.add(ep.method);
290
- const order = ["get", "post", "put", "patch", "delete"];
291
- return order.filter((m) => methods.has(m)).map((m) => METHOD_LABEL[m]).join(" ");
292
- }
293
- function showHelp() {
294
- console.log(`\u0262\u1D0F\u1D05\u1D0D\u1D0F\u1D05\u1D07
295
- \x1B[2mbetter than mcp\x1B[0m
296
-
297
- Usage:
298
- godmode <api> <resource> [id] [flags]
299
- godmode <api> /path [flags]
300
-
301
- Setup:
302
- create Create own custom API entrypoint
303
- add <name|file> Add API as CLI command from <name>.yaml config
304
- update <name> Re-fetch OpenAPI spec and rebuild routes
305
- remove <name> Unregister an API
306
- list Show all registered APIs
307
- mcp <name> Serve registered API as MCP server (stdio)
308
- ... Run \x1B[2mgodmode add --help\x1B[0m for config format
309
-
310
- Navigation:
311
- <api> --help Show resources, auth, and usage
312
- <api> <resource> --help Show operations and sub-resources
313
-
314
- Methods:
315
- -g, --get GET (default)
316
- -po, --post POST
317
- -pu, --put PUT
318
- -pa, --patch PATCH
319
- -d, --delete DELETE
320
-
321
- Data (httpie-style):
322
- key=value Body field (JSON, implies POST)
323
- key==value Query param (URL)
324
-
325
- Options:
326
- -H <key:value> Add header
327
- --token <tok> Auth token (overrides config)
328
- --dry-run Preview request without sending
329
- -v, --verbose Show full request/response
330
-
331
- Use "godmode <api> --help" for API-specific usage.`);
332
- }
333
- function showApiHelp(manifest, apiName, path, filter, methodFilter, all) {
334
- const root = buildTrie(manifest.routes);
335
- const nav = navigateTrie(root, path);
336
- if (!nav) {
337
- console.log("No matching resource.");
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);
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
+ });
338
246
  return;
339
247
  }
340
- const { node, fullPath } = nav;
341
- const actions = getNodeActions(node);
342
- const param = getParamName(node);
343
- const children = getChildren(node).filter((c) => !c.isParam);
344
- const childNames = [...new Set(children.map((c) => c.name))];
345
- const resourceName = fullPath.join(" ");
346
- if (!path.length) {
347
- const name = manifest.config.name || apiName;
348
- const ver = manifest.specVersion ? ` \x1B[2mv${manifest.specVersion}\x1B[0m` : "";
349
- const desc = manifest.description ? `
350
- \x1B[2m${manifest.description}\x1B[0m` : "";
351
- console.log(`${name}${ver}${desc}
248
+ const match = matchRoute(manifest, parsed.segments, parsed.method);
249
+ if (!match) {
250
+ reportNoMatch(ctx);
251
+ process.exit(EXIT_CODES.notFound);
252
+ }
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}
352
260
  `);
353
- console.log(`Usage:
354
- godmode ${apiName} <resource> [id] [flags]
261
+ process.stderr.write(`Suggested allow rule:
262
+ ${suggestedAllowRule({ extension: extensionName, resource: resourceFromSegments(match.route.segments), method: parsed.method })}
355
263
  `);
356
- const auth = manifest.config.auth;
357
- if (auth?.env) {
358
- const ok = !!process.env[auth.env];
359
- const status = ok ? "\x1B[32m(token provided)\x1B[0m" : "\x1B[31m(missing token)\x1B[0m";
360
- console.log(`${auth.env} ${status}
264
+ process.exit(EXIT_CODES.permissionDenied);
265
+ }
266
+ await execute(manifest, match, {
267
+ headers: parsed.headers,
268
+ query,
269
+ body,
270
+ debug: parsed.debug,
271
+ dryRun: parsed.dryRun
272
+ });
273
+ }
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(" ")}
361
281
  `);
362
- }
363
- } else {
364
- const paramHint = getParamName(node);
365
- const idRef = paramHint ? ` [${paramHint}]` : "";
366
- console.log(`Usage:
367
- godmode ${apiName} ${resourceName}${idRef} [flags]
282
+ process.stderr.write(`Method goes first: try 'godmode ${extensionName} ${iface} ${trailingVerb} ${rest.join(" ")}'
283
+ `);
284
+ return;
285
+ }
286
+ process.stderr.write(`No ${parsed.method.toUpperCase()} route matching: ${parsed.segments.join(" ")}
287
+ `);
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(" ")}
368
292
  `);
369
- const auth = manifest.config.auth;
370
- if (auth?.env) {
371
- const ok = !!process.env[auth.env];
372
- const status = ok ? "\x1B[32m(token provided)\x1B[0m" : "\x1B[31m(missing token)\x1B[0m";
373
- console.log(`${auth.env} ${status}`);
374
- }
375
- for (const p of nav.params) {
376
- const status = p.provided ? `\x1B[32m(${p.value})\x1B[0m` : "\x1B[31m(missing parameter)\x1B[0m";
377
- console.log(`<${p.name}> ${status}`);
378
293
  }
379
- console.log("");
380
- const mcpTools = manifest.config._mcpTools;
381
- const mcpTool = mcpTools?.find((t) => t.name === path[path.length - 1]);
382
- if (mcpTool?.inputSchema?.properties) {
383
- const props = mcpTool.inputSchema.properties;
384
- const required = new Set(mcpTool.inputSchema.required || []);
385
- const toolName = mcpTool.name;
386
- if (mcpTool.description) {
387
- const first = mcpTool.description.split("\n")[0];
388
- console.log(`${ITALIC}${first}${RESET}
294
+ }
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}
389
304
  `);
390
- }
391
- console.log("Parameters:");
392
- for (const [name, schema] of Object.entries(props)) {
393
- const req = required.has(name) ? `\x1B[1;38;5;160m[REQUIRED]${RESET} ` : "";
394
- const desc = schema.description ? `${DIM}${ITALIC}${schema.description}${RESET}` : "";
395
- console.log(` ${name.padEnd(28)}${req}${desc}`);
396
- }
397
- console.log("");
398
- console.log(`Example:
399
- godmode ${apiName} ${toolName} ${[...required].map((r) => `${r}=...`).join(" ")}`);
400
- console.log("");
401
- } else {
402
- const order = ["get", "post", "put", "patch", "delete"];
403
- const pc = node.children.find((c) => c.isParam);
404
- const rows = [];
405
- for (const ep of node.methods) {
406
- rows.push({ method: ep.method, summary: ep.summary, needsId: false });
407
- }
408
- if (pc) {
409
- for (const ep of pc.methods) {
410
- rows.push({ method: ep.method, summary: ep.summary, needsId: true });
411
- }
412
- }
413
- if (rows.length) {
414
- const lastSeg = path[path.length - 1] || resourceName;
415
- const base = lastSeg;
416
- const lines = [];
417
- for (const m of order) {
418
- for (const r of rows.filter((r2) => r2.method === m)) {
419
- const id = r.needsId ? ` <${pc.name}>` : "";
420
- let suffix = "";
421
- if (m === "post" || m === "put" || m === "patch") suffix = " key=val";
422
- if (m === "delete") suffix = " -d";
423
- const needsFlag = !r.needsId && m !== "get" && m !== "post" || r.needsId && m === "post";
424
- const flag = needsFlag ? ` -${m === "post" ? "po" : m === "put" ? "pu" : m === "patch" ? "pa" : ""}` : "";
425
- const cmd = `${base}${id}${suffix}${flag}`.trim();
426
- lines.push({ cmd, label: METHOD_LABEL[m] || m, desc: r.summary || "" });
427
- }
428
- }
429
- const maxCmd = Math.max(...lines.map((l) => l.cmd.length));
430
- for (const l of lines) {
431
- const desc = l.desc ? ` ${ITALIC}${l.desc}${RESET}` : "";
432
- console.log(` ${l.cmd.padEnd(maxCmd + 2)}${l.label}${desc}`);
433
- }
434
- console.log("");
435
- }
436
305
  }
437
306
  }
438
- if (childNames.length) {
439
- const ALL_METHODS = ["get", "post", "put", "patch", "delete"];
440
- const mf = methodFilter ? fuzzysort.go(methodFilter, ALL_METHODS)[0]?.target : void 0;
441
- if (methodFilter && !mf) {
442
- console.log(`
443
- No method matching "${methodFilter}".`);
444
- return;
307
+ }
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.`;
445
313
  }
446
- let candidates = mf ? childNames.filter((n) => {
447
- const child = children.find((c) => c.name === n);
448
- if (!child) return false;
449
- const methods = /* @__PURE__ */ new Set();
450
- for (const ep of child.methods) methods.add(ep.method);
451
- const pc = child.children.find((c) => c.isParam);
452
- if (pc) for (const ep of pc.methods) methods.add(ep.method);
453
- return methods.has(mf);
454
- }) : childNames;
455
- const filtered = filter ? fuzzysort.go(filter, candidates).map((r) => r.target) : candidates;
456
- const prefix = `godmode ${apiName}${resourceName ? " " + resourceName : ""}`;
457
- const label = filter || mf ? `Resources${filter ? ` matching "${filter}"` : ""}${mf ? ` with ${mf.toUpperCase()}${methodFilter !== mf ? ` (matched "${methodFilter}")` : ""}` : ""}:` : "Resources:";
458
- console.log(`
459
- ${label}`);
460
- if (!filtered.length) {
461
- console.log(" No matches.");
462
- } else {
463
- const limit = filter || mf || all ? filtered.length : 5;
464
- const shown = filtered.slice(0, limit);
465
- const more = filtered.length - shown.length;
466
- for (const n of shown) {
467
- const child = children.find((c) => c.name === n);
468
- const labels = child ? methodLabels(child) : "";
469
- const subCount = child ? getChildren(child).filter((c) => !c.isParam).length : 0;
470
- const sub = subCount ? ` ${DIM}(${subCount} sub)${RESET}` : "";
471
- const rawDesc = manifest.resourceDescriptions[n] || "";
472
- const truncated = rawDesc.length > 60 ? rawDesc.slice(0, 57) + "..." : rawDesc;
473
- const descStr = truncated ? ` ${ITALIC}${truncated}${RESET}` : "";
474
- console.log(` ${n.padEnd(28)}${labels}${sub}${descStr}`);
475
- }
476
- if (more) {
477
- console.log(` \x1B[2m...${more} more \u2192 use --all, --filter or --method\x1B[0m`);
478
- }
314
+ return null;
315
+ }
316
+ execute() {
317
+ return executeRoute(this.ctx);
318
+ }
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);
324
+ }
325
+ execute() {
326
+ return executeRoute(this.ctx);
327
+ }
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
+ );
340
+ }
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}
355
+ `);
356
+ process.stderr.write(`Suggested allow rule:
357
+ ${suggestedAllowRule({ extension: extensionName, resource: resourceFromTool(tool), method: "mcp" })}
358
+ `);
359
+ process.exit(EXIT_CODES.permissionDenied);
479
360
  }
480
- console.log(`
481
- Use "${prefix} <resource> --help" for more.`);
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}`);
482
378
  }
483
379
  }
484
380
 
485
381
  // src/index.ts
486
382
  loadEnv();
383
+ var VALID_INTERFACES = /* @__PURE__ */ new Set(["api", "graphql", "mcp", "skill"]);
487
384
  async function main() {
488
385
  const args = process.argv.slice(2);
489
386
  if (!args.length || args.length === 1 && (args[0] === "-h" || args[0] === "--help")) {
387
+ warnSettingsErrors();
490
388
  showHelp();
491
389
  return;
492
390
  }
493
- const cmd = args[0];
494
- if (cmd === "create") {
495
- const { configWizard } = await import("./prompt-DCFFOQ7K.js");
496
- await configWizard();
391
+ if (args.length === 1 && (args[0] === "--version" || args[0] === "-v")) {
392
+ await showVersion();
497
393
  return;
498
394
  }
499
- if (cmd === "add") {
500
- const { runAdd } = await import("./add-ZRNIX6DM.js");
501
- await runAdd(args.slice(1));
395
+ const extensionSlug = args[0];
396
+ const rest = args.slice(1);
397
+ const builtin = BUILTINS.get(extensionSlug);
398
+ if (builtin) {
399
+ await builtin.run(rest);
502
400
  return;
503
401
  }
504
- if (cmd === "update") {
505
- if (!args[1]) {
506
- console.error("Usage: godmode update <name>");
507
- process.exit(1);
508
- }
509
- await updateApi(args[1]);
510
- return;
402
+ const multi = findInstalledManifestSync(extensionSlug);
403
+ if (!multi) {
404
+ process.stderr.write(`'${extensionSlug}' is not an installed extension.
405
+ `);
406
+ process.stderr.write(`Try 'godmode ext list' to see installed extensions.
407
+ `);
408
+ process.stderr.write(`Try 'godmode --help' for more information.
409
+ `);
410
+ process.exit(EXIT_CODES.notFound);
511
411
  }
512
- if (cmd === "remove") {
513
- if (!args[1]) {
514
- console.error("Usage: godmode remove <name>");
515
- process.exit(1);
516
- }
517
- await removeApi(args[1]);
412
+ const declared = Object.keys(multi.interfaces);
413
+ const first = rest[0];
414
+ if (!first || first === "--help" || first === "-h") {
415
+ showExtensionOverview(multi);
518
416
  return;
519
417
  }
520
- if (cmd === "list") {
521
- await listApis();
418
+ if (first === "--version" || first === "-v") {
419
+ showExtensionVersion(multi);
522
420
  return;
523
421
  }
524
- if (cmd === "mcp") {
525
- const { runMcp } = await import("./mcp-NZIEMQOM.js");
526
- await runMcp(args.slice(1));
527
- return;
422
+ if (!VALID_INTERFACES.has(first)) {
423
+ process.stderr.write(`Missing interface.
424
+ `);
425
+ process.stderr.write(`'${extensionSlug}' declares: ${declared.join(", ")}.
426
+ `);
427
+ process.stderr.write(`Try 'godmode ${extensionSlug} ${declared[0]} ${first}' or 'godmode ${extensionSlug} --help'.
428
+ `);
429
+ process.exit(EXIT_CODES.usage);
528
430
  }
529
- const apiName = cmd;
530
- const parsed = parseArgs(args.slice(1));
531
- const manifest = await loadManifest(apiName);
532
- if (parsed.help || !parsed.segments.length) {
533
- showApiHelp(manifest, apiName, parsed.segments, parsed.filter, parsed.methodFilter, parsed.all);
534
- return;
431
+ if (!declared.includes(first)) {
432
+ process.stderr.write(`'${extensionSlug}' does not declare a '${first}' interface.
433
+ `);
434
+ process.stderr.write(`Declared: ${declared.join(", ")}.
435
+ `);
436
+ process.stderr.write(`Try 'godmode ${extensionSlug} --help' for more information.
437
+ `);
438
+ process.exit(EXIT_CODES.usage);
535
439
  }
536
- if (manifest.config.type === "graphql") {
537
- const err = validateGraphQLFlags(parsed.method, parsed.query, parsed.body, apiName);
538
- if (err) {
539
- process.stderr.write(err + "\n");
540
- process.exit(1);
541
- }
440
+ await runInterface(first, extensionSlug, rest.slice(1));
441
+ }
442
+ async function runInterface(iface, extensionName, rest) {
443
+ if (iface === "skill") {
444
+ process.stderr.write(`Skill interface not yet implemented.
445
+ `);
446
+ process.exit(EXIT_CODES.usage);
542
447
  }
543
- if (manifest.config.type === "mcp") {
544
- const err = validateMcpFlags(parsed.method, parsed.query);
545
- if (err) {
546
- process.stderr.write(err + "\n");
547
- process.exit(1);
548
- }
549
- const result = await executeMcpTool(manifest.config, parsed.segments[0], parsed.body, {
550
- verbose: parsed.verbose,
551
- dryRun: parsed.dryRun
552
- });
553
- if (result) process.stdout.write(result + "\n");
448
+ if (rest.includes("--version") || rest.includes("-v")) {
449
+ const multi2 = await loadMultiManifest(extensionName);
450
+ showExtensionVersion(multi2);
554
451
  return;
555
452
  }
556
- const query = parsed.query;
557
- const hasBody = Object.keys(parsed.body).length > 0;
558
- let body = hasBody ? JSON.stringify(parsed.body) : void 0;
559
- if (!body && ["post", "put", "patch"].includes(parsed.method)) body = await readStdin();
560
- if (parsed.segments[0]?.startsWith("/")) {
561
- const rawPath = parsed.segments[0];
562
- const syntheticRoute = { path: rawPath, method: parsed.method, summary: "", version: "", segments: [] };
563
- await execute(manifest, { route: syntheticRoute, params: {} }, {
564
- headers: parsed.headers,
565
- query,
566
- body,
567
- token: parsed.token,
568
- verbose: parsed.verbose,
569
- dryRun: parsed.dryRun
570
- });
453
+ const ifaceKey = iface;
454
+ const parsed = parseArgs(rest);
455
+ const multi = await loadMultiManifest(extensionName);
456
+ const manifest = await loadManifest(extensionName, ifaceKey);
457
+ const handler = getInterface({
458
+ iface: ifaceKey,
459
+ extensionName,
460
+ manifest,
461
+ multi,
462
+ parsed,
463
+ rawRest: rest
464
+ });
465
+ if (parsed.help) {
466
+ handler.showHelp();
571
467
  return;
572
468
  }
573
- const match = matchRoute(manifest, parsed.segments, parsed.method);
574
- if (!match) {
575
- process.stderr.write(`No ${parsed.method.toUpperCase()} route matching: ${parsed.segments.join(" ")}
576
- `);
577
- for (const m of ["get", "post", "put", "patch", "delete"]) {
578
- if (m === parsed.method) continue;
579
- const alt = matchRoute(manifest, parsed.segments, m);
580
- if (alt) {
581
- const flag = m === "delete" ? "-d" : `--${m}`;
582
- process.stderr.write(` try: godmode ${apiName} ${parsed.segments.join(" ")} ${flag}
583
- `);
584
- }
585
- }
586
- const similar = suggestRoutes(manifest, parsed.segments).slice(0, 5);
587
- if (similar.length) {
588
- process.stderr.write("\nSimilar:\n");
589
- const seen = /* @__PURE__ */ new Set();
590
- for (const r of similar) {
591
- const p = r.segments.map((s) => s.isParam ? `{${s.value}}` : s.value).join(" ");
592
- if (seen.has(p)) continue;
593
- seen.add(p);
594
- process.stderr.write(` ${p}
595
- `);
596
- }
597
- }
598
- process.exit(1);
469
+ if (!parsed.segments.length) {
470
+ await handler.handleEmpty();
471
+ return;
599
472
  }
600
- await execute(manifest, match, {
601
- headers: parsed.headers,
602
- query,
603
- body,
604
- token: parsed.token,
605
- verbose: parsed.verbose,
606
- dryRun: parsed.dryRun
607
- });
473
+ const err = handler.validate();
474
+ if (err) {
475
+ process.stderr.write(err + "\n");
476
+ process.exit(EXIT_CODES.usage);
477
+ }
478
+ await handler.execute();
608
479
  }
609
480
  main().catch((err) => {
610
481
  process.stderr.write(`${err.message || err}