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/README.md +34 -0
- package/dist/{add-ZRNIX6DM.js → add-D74TIN3J.js} +14 -11
- package/dist/{chunk-EHS56XXY.js → chunk-3AQ2CZKC.js} +6 -5
- package/dist/chunk-4EU5JX6C.js +160 -0
- package/dist/{chunk-I4WS4MMI.js → chunk-AWGLXW3B.js} +27 -14
- package/dist/chunk-KHFZI7IA.js +745 -0
- package/dist/chunk-SMQBMG24.js +102 -0
- package/dist/chunk-XFFBFBN6.js +16 -0
- package/dist/chunk-YDXTIN53.js +212 -0
- package/dist/chunk-ZTPILA7B.js +403 -0
- package/dist/dist-I5CEQ4AC.js +779 -0
- package/dist/ext-K4XEAZGS.js +102 -0
- package/dist/index.js +340 -469
- package/dist/permissions-E4G66OUZ.js +125 -0
- package/dist/{prompt-DCFFOQ7K.js → prompt-YPAZ5433.js} +12 -6
- package/dist/{mcp-server.js → server-L3L4TKK3.js} +20 -3
- package/dist/settings-3FF5WWEY.js +17 -0
- package/package.json +34 -15
- package/dist/chunk-FBMQ3AC4.js +0 -339
- package/dist/mcp-NZIEMQOM.js +0 -58
package/dist/index.js
CHANGED
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
22
|
+
warnSettingsErrors
|
|
23
|
+
} from "./chunk-SMQBMG24.js";
|
|
24
|
+
import {
|
|
25
|
+
BUILTINS,
|
|
26
|
+
GODMODE_HOME,
|
|
27
|
+
findInstalledManifestSync,
|
|
7
28
|
loadManifest,
|
|
8
|
-
|
|
9
|
-
updateApi,
|
|
29
|
+
loadMultiManifest,
|
|
10
30
|
validateGraphQLFlags
|
|
11
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
70
|
+
let explicitMethod = false;
|
|
99
71
|
let filter;
|
|
100
72
|
let methodFilter;
|
|
101
73
|
let all = false;
|
|
102
|
-
|
|
103
|
-
|
|
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 "
|
|
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
|
-
|
|
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/
|
|
192
|
-
import
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
178
|
+
} catch {
|
|
206
179
|
}
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
261
|
+
process.stderr.write(`Suggested allow rule:
|
|
262
|
+
${suggestedAllowRule({ extension: extensionName, resource: resourceFromSegments(match.route.segments), method: parsed.method })}
|
|
355
263
|
`);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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 (
|
|
521
|
-
|
|
418
|
+
if (first === "--version" || first === "-v") {
|
|
419
|
+
showExtensionVersion(multi);
|
|
522
420
|
return;
|
|
523
421
|
}
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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 (
|
|
544
|
-
const
|
|
545
|
-
|
|
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
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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}
|