godmode 0.0.1 → 0.0.2
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-AJERMPEA.js} +11 -9
- package/dist/chunk-2J2VDCQ4.js +503 -0
- package/dist/{chunk-EHS56XXY.js → chunk-EXWYJU2I.js} +1 -1
- package/dist/{chunk-I4WS4MMI.js → chunk-GJCJO7YT.js} +3 -3
- package/dist/index.js +1424 -275
- package/dist/{prompt-DCFFOQ7K.js → prompt-SWOWWAOH.js} +1 -1
- package/dist/{mcp-server.js → server-4YVOAJJG.js} +3 -3
- package/package.json +27 -9
- package/dist/chunk-FBMQ3AC4.js +0 -339
- package/dist/mcp-NZIEMQOM.js +0 -58
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# godmode
|
|
2
|
+
|
|
3
|
+
any API as CLI. any API as MCP. zero code generation.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g godmode
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
godmode ext install stripe
|
|
15
|
+
godmode stripe api customers cus_123
|
|
16
|
+
godmode stripe mcp # serve as MCP server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run `godmode --help` or `godmode <extension> --help` at any nesting level for usage.
|
|
20
|
+
|
|
21
|
+
## Claude Code
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"stripe": {
|
|
27
|
+
"command": "godmode",
|
|
28
|
+
"args": ["stripe", "mcp"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
See [documentation](https://docs.godmode.so) for more.
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
addApi
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-2J2VDCQ4.js";
|
|
5
|
+
import "./chunk-EXWYJU2I.js";
|
|
6
6
|
|
|
7
7
|
// src/commands/add.ts
|
|
8
8
|
async function runAdd(args) {
|
|
9
9
|
if (!args[0] || args[0] === "--help" || args[0] === "-h") {
|
|
10
|
-
console.log(`Add an
|
|
10
|
+
console.log(`Add an extension from a folder, built-in name, or manifest.
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
|
-
godmode add <name> Built-in
|
|
14
|
-
godmode add <folder> Folder containing manifest.yaml
|
|
13
|
+
godmode extension add <name> Built-in extension (e.g. stripe, github)
|
|
14
|
+
godmode extension add <folder> Folder containing manifest.yaml
|
|
15
|
+
godmode extension add <manifest> Direct path to manifest.yaml/json
|
|
15
16
|
|
|
16
17
|
Manifest format (manifest.yaml):
|
|
17
|
-
slug: stripe CLI name
|
|
18
|
+
slug: stripe CLI name
|
|
18
19
|
name: Stripe Display name
|
|
19
20
|
description: Payments API Short description
|
|
20
21
|
type: api api | graphql | mcp
|
|
@@ -32,9 +33,10 @@ Types:
|
|
|
32
33
|
mcp MCP endpoint (requires url)
|
|
33
34
|
|
|
34
35
|
Examples:
|
|
35
|
-
$ godmode add stripe
|
|
36
|
-
$ godmode add ./my-
|
|
37
|
-
$ godmode add
|
|
36
|
+
$ godmode extension add stripe
|
|
37
|
+
$ godmode extension add ./my-extension
|
|
38
|
+
$ godmode extension add ./my-extension/manifest.yaml
|
|
39
|
+
$ godmode extension add openai`);
|
|
38
40
|
process.exit(args[0] ? 0 : 1);
|
|
39
41
|
}
|
|
40
42
|
await addApi(args[0]);
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseMcp
|
|
4
|
+
} from "./chunk-EXWYJU2I.js";
|
|
5
|
+
|
|
6
|
+
// ../../interfaces/graphql/src/index.ts
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { isAbsolute, resolve } from "path";
|
|
9
|
+
var INTROSPECTION_QUERY = `{
|
|
10
|
+
__schema {
|
|
11
|
+
queryType { name }
|
|
12
|
+
mutationType { name }
|
|
13
|
+
subscriptionType { name }
|
|
14
|
+
types {
|
|
15
|
+
name kind description
|
|
16
|
+
fields { name description args { name type { name kind ofType { name kind } } } }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}`;
|
|
20
|
+
async function introspect(url, headers) {
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
24
|
+
body: JSON.stringify({ query: INTROSPECTION_QUERY })
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) throw new Error(`Introspection failed: ${res.status} ${res.statusText}`);
|
|
27
|
+
const json = await res.json();
|
|
28
|
+
return json.data?.__schema?.types?.filter((t) => !t.name.startsWith("__")) || [];
|
|
29
|
+
}
|
|
30
|
+
function parseSdl(text) {
|
|
31
|
+
const types = [];
|
|
32
|
+
const typeRegex = /type\s+(\w+)\s*\{([^}]*)}/g;
|
|
33
|
+
let match;
|
|
34
|
+
while ((match = typeRegex.exec(text)) !== null) {
|
|
35
|
+
const [, typeName, body] = match;
|
|
36
|
+
const fields = [];
|
|
37
|
+
const fieldRegex = /(\w+)(?:\(([^)]*)\))?\s*:\s*\S+/g;
|
|
38
|
+
let fm;
|
|
39
|
+
while ((fm = fieldRegex.exec(body)) !== null) {
|
|
40
|
+
const args = [];
|
|
41
|
+
if (fm[2]) {
|
|
42
|
+
const argRegex = /(\w+)\s*:\s*(\w+)/g;
|
|
43
|
+
let am;
|
|
44
|
+
while ((am = argRegex.exec(fm[2])) !== null) {
|
|
45
|
+
args.push({ name: am[1], type: { name: am[2], kind: "SCALAR" } });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
fields.push({ name: fm[1], args: args.length ? args : void 0 });
|
|
49
|
+
}
|
|
50
|
+
types.push({ name: typeName, kind: "OBJECT", fields });
|
|
51
|
+
}
|
|
52
|
+
return types;
|
|
53
|
+
}
|
|
54
|
+
async function parseGraphQL(name, config) {
|
|
55
|
+
let types;
|
|
56
|
+
if (config.spec) {
|
|
57
|
+
const isRemote = /^https?:\/\//.test(config.spec);
|
|
58
|
+
if (isRemote) {
|
|
59
|
+
process.stderr.write(`Fetching ${config.spec}...
|
|
60
|
+
`);
|
|
61
|
+
const res = await fetch(config.spec);
|
|
62
|
+
if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status}`);
|
|
63
|
+
types = parseSdl(await res.text());
|
|
64
|
+
} else {
|
|
65
|
+
const specPath = isAbsolute(config.spec) ? config.spec : resolve(process.cwd(), config.spec);
|
|
66
|
+
process.stderr.write(`Loading ${specPath}...
|
|
67
|
+
`);
|
|
68
|
+
types = parseSdl(await readFile(specPath, "utf-8"));
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
if (!config.url) throw new Error('GraphQL config needs either "spec" or "url"');
|
|
72
|
+
process.stderr.write(`Introspecting ${config.url}...
|
|
73
|
+
`);
|
|
74
|
+
const headers = { ...config.headers };
|
|
75
|
+
const token = config.auth?.env ? process.env[config.auth.env] : void 0;
|
|
76
|
+
if (token) {
|
|
77
|
+
const authType = config.auth?.type || "bearer";
|
|
78
|
+
if (authType === "bearer") headers["Authorization"] = `Bearer ${token}`;
|
|
79
|
+
else if (authType === "api-key") headers[config.auth?.header || "X-API-Key"] = token;
|
|
80
|
+
}
|
|
81
|
+
types = await introspect(config.url, headers);
|
|
82
|
+
}
|
|
83
|
+
const routes = [];
|
|
84
|
+
for (const type of types) {
|
|
85
|
+
if (!type.fields) continue;
|
|
86
|
+
const isQuery = type.name === "Query" || type.name === "RootQuery";
|
|
87
|
+
const isMutation = type.name === "Mutation" || type.name === "RootMutation";
|
|
88
|
+
if (!isQuery && !isMutation) continue;
|
|
89
|
+
const method = isMutation ? "post" : "get";
|
|
90
|
+
for (const field of type.fields) {
|
|
91
|
+
routes.push({
|
|
92
|
+
path: field.name,
|
|
93
|
+
method,
|
|
94
|
+
summary: field.description || "",
|
|
95
|
+
version: "",
|
|
96
|
+
segments: [{ value: field.name, isParam: false }]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
101
|
+
process.stderr.write(`Parsed ${routes.length} fields (${types.length} types)
|
|
102
|
+
`);
|
|
103
|
+
return { name, description: config.description || "", specVersion: "", config, versions: [], resourceDescriptions: {}, routes };
|
|
104
|
+
}
|
|
105
|
+
function validateGraphQLFlags(method, query, body, apiName) {
|
|
106
|
+
if (["put", "patch", "delete", "head"].includes(method)) {
|
|
107
|
+
return `GraphQL only supports POST. ${method.toUpperCase()} is not valid.`;
|
|
108
|
+
}
|
|
109
|
+
if (method === "post") {
|
|
110
|
+
process.stderr.write(`Note: POST is implicit for GraphQL, flag not needed.
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(query).length || Object.keys(body).length) {
|
|
114
|
+
return `GraphQL uses document syntax, not key=value params.
|
|
115
|
+
godmode ${apiName} '{ field { subfield } }'`;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/config.ts
|
|
121
|
+
import { resolve as resolve2, basename, dirname, extname } from "path";
|
|
122
|
+
import { homedir } from "os";
|
|
123
|
+
import { mkdir, readFile as readFile2, writeFile, readdir, unlink, access, rename } from "fs/promises";
|
|
124
|
+
import { execSync } from "child_process";
|
|
125
|
+
import { parse as parseYaml2 } from "yaml";
|
|
126
|
+
|
|
127
|
+
// ../../interfaces/api/src/index.ts
|
|
128
|
+
import { parse as parseYaml } from "yaml";
|
|
129
|
+
async function parseOpenApi(name, config) {
|
|
130
|
+
process.stderr.write(`Fetching ${config.spec}...
|
|
131
|
+
`);
|
|
132
|
+
const res = await fetch(config.spec);
|
|
133
|
+
if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
|
|
134
|
+
const text = await res.text();
|
|
135
|
+
const isJson = text.trimStart().startsWith("{");
|
|
136
|
+
const spec = isJson ? JSON.parse(text) : parseYaml(text);
|
|
137
|
+
if (!spec.paths) throw new Error("No paths found - is this a valid OpenAPI document?");
|
|
138
|
+
if (!config.url) {
|
|
139
|
+
if (spec.servers?.[0]?.url) {
|
|
140
|
+
config.url = spec.servers[0].url;
|
|
141
|
+
} else if (spec.host) {
|
|
142
|
+
const scheme = spec.schemes?.[0] || "https";
|
|
143
|
+
config.url = `${scheme}://${spec.host}${spec.basePath || ""}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const versions = resolveVersions(config, Object.keys(spec.paths));
|
|
147
|
+
const httpMethods = ["get", "post", "put", "patch", "delete", "head"];
|
|
148
|
+
const routes = [];
|
|
149
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
150
|
+
for (const method of httpMethods) {
|
|
151
|
+
const op = pathItem[method];
|
|
152
|
+
if (!op) continue;
|
|
153
|
+
const ver = versions.find((v) => path.startsWith(v.prefix));
|
|
154
|
+
const version = ver?.name || "";
|
|
155
|
+
const stripped = ver ? path.slice(ver.prefix.length).replace(/^\//, "") : path.slice(1);
|
|
156
|
+
const rawSegments = stripped.split("/").filter(Boolean);
|
|
157
|
+
const segments = rawSegments.map((s) => {
|
|
158
|
+
const isParam = s.startsWith("{") && s.endsWith("}");
|
|
159
|
+
return { value: isParam ? s.slice(1, -1) : s, isParam };
|
|
160
|
+
});
|
|
161
|
+
const tag = op.tags?.[0] || "";
|
|
162
|
+
routes.push({ path, method, summary: op.summary || "", version, tag, segments });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
166
|
+
const resourceDescriptions = {};
|
|
167
|
+
if (spec.tags) {
|
|
168
|
+
const stripHtml = (s) => s.replace(/<[^>]+>/g, "").trim();
|
|
169
|
+
for (const t of spec.tags) {
|
|
170
|
+
if (t.name && t.description) {
|
|
171
|
+
const first = stripHtml(t.description).split(/\.\s/)[0];
|
|
172
|
+
resourceDescriptions[t.name] = first.endsWith(".") ? first : first + ".";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const description = config.description || spec.info?.description || "";
|
|
177
|
+
const specVersion = spec.info?.version || "";
|
|
178
|
+
const versionNames = versions.map((v) => v.name).join(", ");
|
|
179
|
+
process.stderr.write(`Parsed ${routes.length} routes (versions: ${versionNames || "none"})
|
|
180
|
+
`);
|
|
181
|
+
return { name, description, specVersion, config, versions, resourceDescriptions, routes };
|
|
182
|
+
}
|
|
183
|
+
function resolveVersions(config, paths) {
|
|
184
|
+
if (config.versions?.length) {
|
|
185
|
+
return config.versions.map((v) => ({
|
|
186
|
+
prefix: v.prefix.endsWith("/") ? v.prefix.slice(0, -1) : v.prefix,
|
|
187
|
+
name: v.name || v.prefix.replace(/^\//, "")
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
if (config.prefix) {
|
|
191
|
+
const prefix = config.prefix.endsWith("/") ? config.prefix.slice(0, -1) : config.prefix;
|
|
192
|
+
return [{ prefix, name: prefix.replace(/^\//, "") }];
|
|
193
|
+
}
|
|
194
|
+
const versionPattern = /^(\/(?:api\/)?v\d+)/;
|
|
195
|
+
const detected = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const p of paths) {
|
|
197
|
+
const match = p.match(versionPattern);
|
|
198
|
+
if (match) detected.set(match[1], (detected.get(match[1]) || 0) + 1);
|
|
199
|
+
}
|
|
200
|
+
if (detected.size) {
|
|
201
|
+
return [...detected.entries()].sort((a, b) => b[1] - a[1]).map(([prefix]) => ({ prefix, name: prefix.replace(/^\//, "") }));
|
|
202
|
+
}
|
|
203
|
+
return [{ prefix: "", name: "" }];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/spec.ts
|
|
207
|
+
function projectManifest(multi, iface) {
|
|
208
|
+
const data = multi.interfaces[iface];
|
|
209
|
+
if (!data) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`extension '${multi.slug}' does not declare an '${iface}' interface (declared: ${Object.keys(multi.interfaces).join(", ")})`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const config = {
|
|
215
|
+
slug: multi.slug,
|
|
216
|
+
name: multi.name,
|
|
217
|
+
description: multi.description,
|
|
218
|
+
type: iface,
|
|
219
|
+
auth: multi.auth,
|
|
220
|
+
headers: multi.headers,
|
|
221
|
+
..."spec" in data ? { spec: data.spec } : {},
|
|
222
|
+
..."url" in data && data.url ? { url: data.url } : {},
|
|
223
|
+
..."prefix" in data && data.prefix ? { prefix: data.prefix } : {},
|
|
224
|
+
versions: data.versions,
|
|
225
|
+
...iface === "mcp" && data._mcpTools ? { _mcpTools: data._mcpTools } : {}
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
name: multi.name,
|
|
229
|
+
description: multi.description,
|
|
230
|
+
specVersion: data.specVersion,
|
|
231
|
+
config,
|
|
232
|
+
versions: data.versions,
|
|
233
|
+
resourceDescriptions: data.resourceDescriptions,
|
|
234
|
+
routes: data.routes
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
var parsers = {
|
|
238
|
+
api: parseOpenApi,
|
|
239
|
+
graphql: parseGraphQL,
|
|
240
|
+
mcp: parseMcp
|
|
241
|
+
};
|
|
242
|
+
async function compileInterface(iface, name, source) {
|
|
243
|
+
const parser = parsers[iface];
|
|
244
|
+
if (!parser) throw new Error(`Unknown interface '${iface}'`);
|
|
245
|
+
const ifaceSource = source.interfaces[iface];
|
|
246
|
+
if (!ifaceSource) throw new Error(`Interface '${iface}' not declared on '${name}'`);
|
|
247
|
+
const legacyConfig = {
|
|
248
|
+
slug: source.slug || name,
|
|
249
|
+
name: source.name,
|
|
250
|
+
description: source.description,
|
|
251
|
+
type: iface,
|
|
252
|
+
auth: source.auth,
|
|
253
|
+
headers: source.headers,
|
|
254
|
+
..."spec" in ifaceSource && ifaceSource.spec ? { spec: ifaceSource.spec } : {},
|
|
255
|
+
..."url" in ifaceSource && ifaceSource.url ? { url: ifaceSource.url } : {},
|
|
256
|
+
..."prefix" in ifaceSource && ifaceSource.prefix ? { prefix: ifaceSource.prefix } : {},
|
|
257
|
+
versions: ifaceSource.versions
|
|
258
|
+
};
|
|
259
|
+
const flat = await parser(name, legacyConfig);
|
|
260
|
+
const base = {
|
|
261
|
+
type: iface,
|
|
262
|
+
specVersion: flat.specVersion,
|
|
263
|
+
versions: flat.versions,
|
|
264
|
+
resourceDescriptions: flat.resourceDescriptions,
|
|
265
|
+
routes: flat.routes
|
|
266
|
+
};
|
|
267
|
+
if (iface === "api") {
|
|
268
|
+
const s2 = ifaceSource;
|
|
269
|
+
return {
|
|
270
|
+
...base,
|
|
271
|
+
type: "api",
|
|
272
|
+
spec: s2.spec,
|
|
273
|
+
url: flat.config.url,
|
|
274
|
+
prefix: s2.prefix
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (iface === "graphql") {
|
|
278
|
+
const s2 = ifaceSource;
|
|
279
|
+
return {
|
|
280
|
+
...base,
|
|
281
|
+
type: "graphql",
|
|
282
|
+
spec: s2.spec,
|
|
283
|
+
url: flat.config.url
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const s = ifaceSource;
|
|
287
|
+
return {
|
|
288
|
+
...base,
|
|
289
|
+
type: "mcp",
|
|
290
|
+
url: s.url,
|
|
291
|
+
_mcpTools: flat.config._mcpTools
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/config.ts
|
|
296
|
+
var GODMODE_HOME = process.platform === "linux" && process.env.XDG_CONFIG_HOME ? resolve2(process.env.XDG_CONFIG_HOME, "godmode") : resolve2(homedir(), ".godmode");
|
|
297
|
+
var GODMODE_EXTENSIONS_DIR = process.env.GODMODE_EXTENSIONS_DIR ? resolve2(process.env.GODMODE_EXTENSIONS_DIR) : resolve2(GODMODE_HOME, "extensions");
|
|
298
|
+
var LEGACY_APIS_DIR = resolve2(GODMODE_HOME, "apis");
|
|
299
|
+
var INTERFACE_KEYS = ["api", "graphql", "mcp"];
|
|
300
|
+
async function ensureDirs() {
|
|
301
|
+
if (await exists(LEGACY_APIS_DIR) && !await exists(GODMODE_EXTENSIONS_DIR)) {
|
|
302
|
+
await mkdir(dirname(GODMODE_EXTENSIONS_DIR), { recursive: true });
|
|
303
|
+
await rename(LEGACY_APIS_DIR, GODMODE_EXTENSIONS_DIR);
|
|
304
|
+
process.stderr.write(`Migrated ${LEGACY_APIS_DIR} -> ${GODMODE_EXTENSIONS_DIR}
|
|
305
|
+
`);
|
|
306
|
+
}
|
|
307
|
+
await mkdir(GODMODE_EXTENSIONS_DIR, { recursive: true });
|
|
308
|
+
}
|
|
309
|
+
async function exists(path) {
|
|
310
|
+
try {
|
|
311
|
+
await access(path);
|
|
312
|
+
return true;
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function validateSource(raw, origin) {
|
|
318
|
+
if (!raw || typeof raw !== "object") {
|
|
319
|
+
throw new Error(`${origin}: not a valid object`);
|
|
320
|
+
}
|
|
321
|
+
const m = raw;
|
|
322
|
+
if (!m.name || typeof m.name !== "string") {
|
|
323
|
+
throw new Error(`${origin}: missing or invalid 'name'`);
|
|
324
|
+
}
|
|
325
|
+
if (!m.interfaces || typeof m.interfaces !== "object" || Object.keys(m.interfaces).length === 0) {
|
|
326
|
+
throw new Error(`${origin}: 'interfaces' must be an object with at least one key`);
|
|
327
|
+
}
|
|
328
|
+
for (const key of Object.keys(m.interfaces)) {
|
|
329
|
+
if (!INTERFACE_KEYS.includes(key)) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`${origin}: unknown interface '${key}' (valid: ${INTERFACE_KEYS.join(", ")})`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return m;
|
|
336
|
+
}
|
|
337
|
+
async function loadSourceFromDir(dir) {
|
|
338
|
+
for (const ext of [".yaml", ".yml", ".json"]) {
|
|
339
|
+
const filePath = resolve2(dir, `manifest${ext}`);
|
|
340
|
+
if (await exists(filePath)) {
|
|
341
|
+
const text = await readFile2(filePath, "utf-8");
|
|
342
|
+
const raw = ext === ".json" ? JSON.parse(text) : parseYaml2(text);
|
|
343
|
+
return validateSource(raw, filePath);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
async function loadSourceFromFile(filePath) {
|
|
349
|
+
if (!await exists(filePath)) return null;
|
|
350
|
+
const ext = extname(filePath);
|
|
351
|
+
if (![".yaml", ".yml", ".json"].includes(ext)) return null;
|
|
352
|
+
const text = await readFile2(filePath, "utf-8");
|
|
353
|
+
const raw = ext === ".json" ? JSON.parse(text) : parseYaml2(text);
|
|
354
|
+
return { source: validateSource(raw, filePath), dir: dirname(filePath) };
|
|
355
|
+
}
|
|
356
|
+
async function resolveSource(input) {
|
|
357
|
+
const asPath = resolve2(process.cwd(), input);
|
|
358
|
+
const fromFile = await loadSourceFromFile(asPath);
|
|
359
|
+
if (fromFile) {
|
|
360
|
+
const name = fromFile.source.slug || fromFile.source.name || basename(fromFile.dir);
|
|
361
|
+
return { name, source: fromFile.source, dir: fromFile.dir };
|
|
362
|
+
}
|
|
363
|
+
const fromDir = await loadSourceFromDir(asPath);
|
|
364
|
+
if (fromDir) {
|
|
365
|
+
const name = fromDir.slug || fromDir.name || basename(asPath);
|
|
366
|
+
return { name, source: fromDir, dir: asPath };
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const pkgDir = resolve2(import.meta.dirname, "..", "..", "..", "extensions", input);
|
|
370
|
+
const fromPkg = await loadSourceFromDir(pkgDir);
|
|
371
|
+
if (fromPkg) {
|
|
372
|
+
const name = fromPkg.slug || fromPkg.name || input;
|
|
373
|
+
return { name, source: fromPkg, dir: pkgDir };
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
throw new Error(
|
|
378
|
+
`No manifest found: ${input} (expected manifest file, folder containing manifest.yaml, or @godmode-cli/${input})`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
async function addApi(input) {
|
|
382
|
+
await ensureDirs();
|
|
383
|
+
const resolved = await resolveSource(input).catch(() => null);
|
|
384
|
+
if (resolved) {
|
|
385
|
+
const { name: name2, source } = resolved;
|
|
386
|
+
const ifaceKeys = Object.keys(source.interfaces).filter(
|
|
387
|
+
(k) => INTERFACE_KEYS.includes(k)
|
|
388
|
+
);
|
|
389
|
+
if (ifaceKeys.length > 0) {
|
|
390
|
+
const multi = {
|
|
391
|
+
name: source.name,
|
|
392
|
+
slug: source.slug || name2,
|
|
393
|
+
description: source.description || "",
|
|
394
|
+
auth: source.auth,
|
|
395
|
+
headers: source.headers,
|
|
396
|
+
interfaces: {}
|
|
397
|
+
};
|
|
398
|
+
for (const iface of ifaceKeys) {
|
|
399
|
+
const data = await compileInterface(iface, name2, source);
|
|
400
|
+
multi.interfaces[iface] = data;
|
|
401
|
+
}
|
|
402
|
+
await writeFile(resolve2(GODMODE_EXTENSIONS_DIR, `${name2}.json`), JSON.stringify(multi, null, 2));
|
|
403
|
+
const ifaceSummary = ifaceKeys.map((k) => {
|
|
404
|
+
const d = multi.interfaces[k];
|
|
405
|
+
return d ? `${k}=${d.routes.length}` : k;
|
|
406
|
+
}).join(", ");
|
|
407
|
+
process.stderr.write(`Registered "${name2}" - interfaces: ${ifaceSummary}
|
|
408
|
+
`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const name = resolved?.name || input;
|
|
413
|
+
const installTarget = resolved?.dir && await exists(resolve2(resolved.dir, "package.json")) ? resolved.dir : input.startsWith("@") ? input : `@godmode-cli/${input}`;
|
|
414
|
+
process.stderr.write(`Installing ${name}...
|
|
415
|
+
`);
|
|
416
|
+
try {
|
|
417
|
+
execSync(`npm install ${installTarget} --prefix ${GODMODE_HOME}`, { stdio: "pipe" });
|
|
418
|
+
} catch (e) {
|
|
419
|
+
const err = e;
|
|
420
|
+
throw new Error(`Failed to install ${name}: ${err.stderr?.toString().trim() || err.message}`);
|
|
421
|
+
}
|
|
422
|
+
const pkgName = `@godmode-cli/${name}`;
|
|
423
|
+
const mcpConfigPath = resolve2(GODMODE_HOME, "node_modules", pkgName, ".mcp.json");
|
|
424
|
+
if (await exists(mcpConfigPath)) {
|
|
425
|
+
process.stderr.write(`Installed "${name}" (MCP server extension)
|
|
426
|
+
`);
|
|
427
|
+
} else {
|
|
428
|
+
process.stderr.write(`Installed "${name}"
|
|
429
|
+
`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function updateApi(name) {
|
|
433
|
+
await addApi(name);
|
|
434
|
+
}
|
|
435
|
+
async function removeApi(name) {
|
|
436
|
+
try {
|
|
437
|
+
await unlink(resolve2(GODMODE_EXTENSIONS_DIR, `${name}.json`));
|
|
438
|
+
process.stderr.write(`Removed "${name}"
|
|
439
|
+
`);
|
|
440
|
+
} catch {
|
|
441
|
+
process.stderr.write(`Extension "${name}" not found
|
|
442
|
+
`);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function listApis() {
|
|
447
|
+
await ensureDirs();
|
|
448
|
+
const files = await readdir(GODMODE_EXTENSIONS_DIR);
|
|
449
|
+
const apis = files.filter((f) => f.endsWith(".json"));
|
|
450
|
+
if (!apis.length) {
|
|
451
|
+
console.log("No extensions registered. Create a manifest.yaml and run: godmode extension add <name>");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
for (const file of apis) {
|
|
455
|
+
const m = JSON.parse(await readFile2(resolve2(GODMODE_EXTENSIONS_DIR, file), "utf-8"));
|
|
456
|
+
const ifaces = Object.keys(m.interfaces).join(", ");
|
|
457
|
+
const desc = m.description ? ` ${m.description}` : "";
|
|
458
|
+
const routeTotal = Object.values(m.interfaces).reduce((n, d) => n + (d?.routes.length ?? 0), 0);
|
|
459
|
+
console.log(` ${m.slug} [${ifaces}] ${routeTotal} routes${desc}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function loadMultiManifest(name) {
|
|
463
|
+
await ensureDirs();
|
|
464
|
+
try {
|
|
465
|
+
return JSON.parse(await readFile2(resolve2(GODMODE_EXTENSIONS_DIR, `${name}.json`), "utf-8"));
|
|
466
|
+
} catch {
|
|
467
|
+
process.stderr.write(`Extension "${name}" not found. Create ${name}.yaml and run: godmode extension add ${name}
|
|
468
|
+
`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function loadManifest(name, iface) {
|
|
473
|
+
const multi = await loadMultiManifest(name);
|
|
474
|
+
const declared = Object.keys(multi.interfaces);
|
|
475
|
+
if (declared.length === 0) {
|
|
476
|
+
process.stderr.write(`Extension "${name}" has no interfaces declared
|
|
477
|
+
`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
const target = iface ?? declared[0];
|
|
481
|
+
if (!multi.interfaces[target]) {
|
|
482
|
+
process.stderr.write(
|
|
483
|
+
`Extension "${name}" does not declare a '${target}' interface (declared: ${declared.join(", ")}).
|
|
484
|
+
`
|
|
485
|
+
);
|
|
486
|
+
process.stderr.write(`Try: godmode ${declared[0]} ${name} --help
|
|
487
|
+
`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
return projectManifest(multi, target);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export {
|
|
494
|
+
validateGraphQLFlags,
|
|
495
|
+
GODMODE_HOME,
|
|
496
|
+
GODMODE_EXTENSIONS_DIR,
|
|
497
|
+
addApi,
|
|
498
|
+
updateApi,
|
|
499
|
+
removeApi,
|
|
500
|
+
listApis,
|
|
501
|
+
loadMultiManifest,
|
|
502
|
+
loadManifest
|
|
503
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/request.ts
|
|
3
|
+
// ../../interfaces/api/src/request.ts
|
|
4
4
|
async function executeToString(manifest, match, options) {
|
|
5
5
|
const baseUrl = manifest.config.url;
|
|
6
6
|
if (!baseUrl) {
|
|
@@ -16,9 +16,9 @@ async function executeToString(manifest, match, options) {
|
|
|
16
16
|
}
|
|
17
17
|
const headers = { ...manifest.config.headers, ...options.headers };
|
|
18
18
|
const authConfig = manifest.config.auth;
|
|
19
|
-
const token =
|
|
19
|
+
const token = authConfig?.env ? process.env[authConfig.env] : void 0;
|
|
20
20
|
if (authConfig?.env && !token && !options.dryRun) {
|
|
21
|
-
throw new Error(`Missing ${authConfig.env}. Set it in .env or
|
|
21
|
+
throw new Error(`Missing ${authConfig.env}. Set it in .env or export it in your shell.`);
|
|
22
22
|
}
|
|
23
23
|
if (token) {
|
|
24
24
|
const authType = authConfig?.type || "bearer";
|