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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
explainPermission,
|
|
4
|
+
matchRoute,
|
|
5
|
+
resourceFromRawPath,
|
|
6
|
+
resourceFromSegments,
|
|
7
|
+
suggestedAllowRule
|
|
8
|
+
} from "./chunk-4EU5JX6C.js";
|
|
9
|
+
import {
|
|
10
|
+
EXIT_CODES
|
|
11
|
+
} from "./chunk-XFFBFBN6.js";
|
|
12
|
+
import {
|
|
13
|
+
warnSettingsErrors
|
|
14
|
+
} from "./chunk-SMQBMG24.js";
|
|
15
|
+
import {
|
|
16
|
+
loadManifest
|
|
17
|
+
} from "./chunk-KHFZI7IA.js";
|
|
18
|
+
import "./chunk-3AQ2CZKC.js";
|
|
19
|
+
import "./chunk-YDXTIN53.js";
|
|
20
|
+
|
|
21
|
+
// src/commands/permissions.ts
|
|
22
|
+
function showPermissionsHelp() {
|
|
23
|
+
console.log(`Inspect godmode permission policy.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
godmode permissions list
|
|
27
|
+
godmode permissions explain <extension> <interface> <target> [method]
|
|
28
|
+
|
|
29
|
+
Exit codes:
|
|
30
|
+
0 success / allowed
|
|
31
|
+
2 usage or parse error
|
|
32
|
+
3 extension or route not found
|
|
33
|
+
4 permission denied
|
|
34
|
+
10 upstream HTTP 4xx
|
|
35
|
+
11 upstream HTTP 5xx
|
|
36
|
+
12 upstream failure`);
|
|
37
|
+
}
|
|
38
|
+
async function runPermissions(rest) {
|
|
39
|
+
const cmd = rest[0];
|
|
40
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
41
|
+
warnSettingsErrors();
|
|
42
|
+
showPermissionsHelp();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (cmd === "list") {
|
|
46
|
+
const { loadSettingsDetailed } = await import("./settings-3FF5WWEY.js");
|
|
47
|
+
const loaded = loadSettingsDetailed();
|
|
48
|
+
for (const error of loaded.errors) {
|
|
49
|
+
process.stderr.write(`Warning: cannot parse ${error.path}: ${error.message}
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
const sources = loaded.sources.flatMap((source) => {
|
|
53
|
+
const extensions = source.settings.extensions ?? {};
|
|
54
|
+
return Object.entries(extensions).flatMap(([slug, settings]) => {
|
|
55
|
+
const permissions = settings.permissions;
|
|
56
|
+
if (!permissions?.allow?.length && !permissions?.deny?.length) return [];
|
|
57
|
+
return [{ slug, permissions, origin: `${source.scope}:${source.path}` }];
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
if (!sources.length) {
|
|
61
|
+
console.log("No permissions configured.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const { slug, permissions, origin } of sources) {
|
|
65
|
+
console.log(`${slug}:`);
|
|
66
|
+
for (const effect of ["allow", "deny"]) {
|
|
67
|
+
for (const rule of permissions[effect] ?? []) {
|
|
68
|
+
const resources = rule.resources?.join(", ") || "*";
|
|
69
|
+
const methods = rule.methods?.join(", ") || "*";
|
|
70
|
+
console.log(` ${effect} resources=[${resources}] methods=[${methods}] origin=${origin}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (cmd === "explain") {
|
|
77
|
+
const [extension, iface, target, maybeMethod] = rest.slice(1);
|
|
78
|
+
if (!extension || !iface || !target) {
|
|
79
|
+
process.stderr.write("Usage: godmode permissions explain <extension> <interface> <target> [method]\n");
|
|
80
|
+
process.exit(EXIT_CODES.usage);
|
|
81
|
+
}
|
|
82
|
+
const method = iface === "mcp" ? "mcp" : maybeMethod || "GET";
|
|
83
|
+
const resource = await explainResource(extension, iface, target, method);
|
|
84
|
+
const decision = explainPermission({ extension, resource, method });
|
|
85
|
+
console.log(`${decision.allowed ? "allow" : "deny"} ${extension} ${iface} ${target} ${method.toUpperCase()}`);
|
|
86
|
+
if (decision.reason) console.log(decision.reason);
|
|
87
|
+
if (decision.rule) {
|
|
88
|
+
console.log(`winning rule: resources=[${decision.rule.resources?.join(", ") || "*"}] methods=[${decision.rule.methods?.join(", ") || "*"}] origin=${decision.origin || "settings.yaml"}`);
|
|
89
|
+
}
|
|
90
|
+
if (!decision.allowed) {
|
|
91
|
+
console.log(`suggested allow rule:
|
|
92
|
+
${suggestedAllowRule({ extension, resource, method })}`);
|
|
93
|
+
process.exit(EXIT_CODES.permissionDenied);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
process.stderr.write(`Unknown permissions command '${cmd}'.
|
|
98
|
+
`);
|
|
99
|
+
process.stderr.write(`Try 'godmode permissions --help' for more information.
|
|
100
|
+
`);
|
|
101
|
+
process.exit(EXIT_CODES.usage);
|
|
102
|
+
}
|
|
103
|
+
async function explainResource(extension, iface, target, method) {
|
|
104
|
+
if (iface === "mcp") return target;
|
|
105
|
+
if (iface === "api" || iface === "graphql") {
|
|
106
|
+
try {
|
|
107
|
+
const manifest = await loadManifest(extension, iface);
|
|
108
|
+
const normalizedMethod = method.toLowerCase();
|
|
109
|
+
if (target.startsWith("/")) {
|
|
110
|
+
const exact = manifest.routes.find((route) => route.method === normalizedMethod && route.path === target);
|
|
111
|
+
if (exact) return resourceFromSegments(exact.segments);
|
|
112
|
+
}
|
|
113
|
+
const segments = target.replace(/^\/+/, "").split(/[/.]/).filter(Boolean);
|
|
114
|
+
const match = matchRoute(manifest, segments, normalizedMethod);
|
|
115
|
+
if (match) return resourceFromSegments(match.route.segments);
|
|
116
|
+
return resourceFromRawPath(target);
|
|
117
|
+
} catch {
|
|
118
|
+
return target.replace(/^\/+/, "").replace(/\//g, ".").replace(/^v\d+\./i, "") || "*";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return target.replace(/^\/+/, "").replace(/\//g, ".").replace(/^v\d+\./i, "") || "*";
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
runPermissions
|
|
125
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/prompt.ts
|
|
4
4
|
import prompts from "prompts";
|
|
5
|
-
import { writeFileSync } from "fs";
|
|
5
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
6
6
|
import { resolve } from "path";
|
|
7
7
|
import { stringify } from "yaml";
|
|
8
8
|
async function configWizard() {
|
|
@@ -60,11 +60,15 @@ async function configWizard() {
|
|
|
60
60
|
} });
|
|
61
61
|
const config = {
|
|
62
62
|
name: response.name.charAt(0).toUpperCase() + response.name.slice(1),
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
slug: response.name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
64
|
+
interfaces: {
|
|
65
|
+
api: {
|
|
66
|
+
spec: response.spec
|
|
67
|
+
}
|
|
68
|
+
}
|
|
65
69
|
};
|
|
66
70
|
if (response.description) config.description = response.description;
|
|
67
|
-
if (response.url) config.url = response.url;
|
|
71
|
+
if (response.url) config.interfaces.api.url = response.url;
|
|
68
72
|
if (response.envVar) {
|
|
69
73
|
config.auth = { env: response.envVar };
|
|
70
74
|
if (response.authType && response.authType !== "bearer") {
|
|
@@ -72,7 +76,8 @@ async function configWizard() {
|
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
const yaml = stringify(config);
|
|
75
|
-
const
|
|
79
|
+
const dirname = response.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
80
|
+
const filename = `${dirname}/manifest.yaml`;
|
|
76
81
|
console.log(`
|
|
77
82
|
\x1B[2m${yaml}\x1B[0m`);
|
|
78
83
|
const { write } = await prompts({
|
|
@@ -86,10 +91,11 @@ async function configWizard() {
|
|
|
86
91
|
return;
|
|
87
92
|
}
|
|
88
93
|
const filepath = resolve(process.cwd(), filename);
|
|
94
|
+
mkdirSync(resolve(process.cwd(), dirname), { recursive: true });
|
|
89
95
|
writeFileSync(filepath, yaml);
|
|
90
96
|
console.log(`
|
|
91
97
|
Saved ${filepath}`);
|
|
92
|
-
console.log(`Run \x1B[1mgodmode
|
|
98
|
+
console.log(`Run \x1B[1mgodmode ext install ./${dirname}\x1B[0m to register it.`);
|
|
93
99
|
}
|
|
94
100
|
export {
|
|
95
101
|
configWizard
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
executeToString
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-AWGLXW3B.js";
|
|
5
5
|
import {
|
|
6
6
|
executeMcpTool
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3AQ2CZKC.js";
|
|
8
|
+
import "./chunk-YDXTIN53.js";
|
|
8
9
|
|
|
9
|
-
// src/
|
|
10
|
+
// ../../interfaces/mcp/src/server.ts
|
|
10
11
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
13
|
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -98,6 +99,15 @@ async function serveMcp(manifest, options = {}) {
|
|
|
98
99
|
}
|
|
99
100
|
try {
|
|
100
101
|
let result;
|
|
102
|
+
const extension = manifest.config.slug || manifest.name;
|
|
103
|
+
const resource = type === "mcp" ? resourceFromTool(toolName) : resourceFromSegments(tool.route.segments);
|
|
104
|
+
const method = type === "mcp" ? "mcp" : tool.route.method;
|
|
105
|
+
if (options.checkPermission) {
|
|
106
|
+
const check = options.checkPermission({ extension, resource, method });
|
|
107
|
+
if (!check.allowed) {
|
|
108
|
+
return { content: [{ type: "text", text: `Blocked: ${check.reason}` }], isError: true };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
101
111
|
if (type === "mcp") {
|
|
102
112
|
result = await executeMcpTool(manifest.config, toolName, args, {});
|
|
103
113
|
} else if (type === "graphql") {
|
|
@@ -124,6 +134,13 @@ async function serveMcp(manifest, options = {}) {
|
|
|
124
134
|
});
|
|
125
135
|
await server.connect(new StdioServerTransport());
|
|
126
136
|
}
|
|
137
|
+
function resourceFromSegments(segments) {
|
|
138
|
+
const parts = segments.filter((s) => !s.isParam).map((s) => s.value).filter((v) => !/^v\d+$/i.test(v));
|
|
139
|
+
return parts.join(".") || "*";
|
|
140
|
+
}
|
|
141
|
+
function resourceFromTool(toolName) {
|
|
142
|
+
return toolName;
|
|
143
|
+
}
|
|
127
144
|
export {
|
|
128
145
|
serveMcp
|
|
129
146
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
extensionSettings,
|
|
4
|
+
loadSettings,
|
|
5
|
+
loadSettingsDetailed,
|
|
6
|
+
resetSettingsCache,
|
|
7
|
+
settingsErrorMessage,
|
|
8
|
+
warnSettingsErrors
|
|
9
|
+
} from "./chunk-SMQBMG24.js";
|
|
10
|
+
export {
|
|
11
|
+
extensionSettings,
|
|
12
|
+
loadSettings,
|
|
13
|
+
loadSettingsDetailed,
|
|
14
|
+
resetSettingsCache,
|
|
15
|
+
settingsErrorMessage,
|
|
16
|
+
warnSettingsErrors
|
|
17
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "godmode",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "any API as CLI. any API as MCP. zero code generation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Tomas Sivicki",
|
|
8
|
+
"homepage": "https://godmode.so",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/tomsiwik/godmode",
|
|
12
|
+
"directory": "packages/cli"
|
|
13
|
+
},
|
|
14
|
+
"bugs": "https://github.com/tomsiwik/godmode/issues",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cli",
|
|
17
|
+
"api",
|
|
18
|
+
"mcp",
|
|
19
|
+
"openapi",
|
|
20
|
+
"graphql",
|
|
21
|
+
"claude-code"
|
|
22
|
+
],
|
|
8
23
|
"bin": {
|
|
9
24
|
"godmode": "./dist/index.js"
|
|
10
25
|
},
|
|
11
26
|
"exports": {
|
|
12
|
-
"./
|
|
27
|
+
"./spec": "./src/spec.ts",
|
|
28
|
+
"./config": "./src/config.ts",
|
|
29
|
+
"./permissions": "./src/permissions.ts",
|
|
30
|
+
"./help": "./src/help.ts"
|
|
13
31
|
},
|
|
14
32
|
"files": [
|
|
15
33
|
"dist",
|
|
@@ -17,19 +35,23 @@
|
|
|
17
35
|
"README.md"
|
|
18
36
|
],
|
|
19
37
|
"dependencies": {
|
|
20
|
-
"@hey-api/openapi-ts": "^0.
|
|
21
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
38
|
+
"@hey-api/openapi-ts": "^0.97.0",
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
40
|
"fuzzysort": "^3.1.0",
|
|
23
41
|
"prompts": "^2.4.2",
|
|
24
|
-
"yaml": "^2.8.
|
|
42
|
+
"yaml": "^2.8.4",
|
|
43
|
+
"@godmode-cli/cli": "0.0.1",
|
|
44
|
+
"@godmode-cli/command-agent": "0.0.1",
|
|
45
|
+
"@godmode-cli/interface-api": "0.0.1",
|
|
46
|
+
"@godmode-cli/interface-graphql": "0.0.1",
|
|
47
|
+
"@godmode-cli/interface-mcp": "0.0.1"
|
|
25
48
|
},
|
|
26
49
|
"devDependencies": {
|
|
27
|
-
"@
|
|
28
|
-
"@types/node": "^22.0.0",
|
|
50
|
+
"@types/node": "^25.6.0",
|
|
29
51
|
"@types/prompts": "^2.4.9",
|
|
30
|
-
"tsup": "^8.
|
|
31
|
-
"typescript": "^
|
|
32
|
-
"vitest": "^4.1.
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"typescript": "^6.0.3",
|
|
54
|
+
"vitest": "^4.1.5"
|
|
33
55
|
},
|
|
34
56
|
"engines": {
|
|
35
57
|
"node": ">=20.0.0"
|
|
@@ -38,9 +60,6 @@
|
|
|
38
60
|
"build": "tsup",
|
|
39
61
|
"dev": "tsup --watch",
|
|
40
62
|
"start": "node dist/index.js",
|
|
41
|
-
"test": "vitest run"
|
|
42
|
-
"changeset": "changeset",
|
|
43
|
-
"version": "changeset version",
|
|
44
|
-
"release": "pnpm build && changeset publish"
|
|
63
|
+
"test": "vitest run"
|
|
45
64
|
}
|
|
46
65
|
}
|
package/dist/chunk-FBMQ3AC4.js
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
parseMcp
|
|
4
|
-
} from "./chunk-EHS56XXY.js";
|
|
5
|
-
|
|
6
|
-
// src/protocols/graphql.ts
|
|
7
|
-
var INTROSPECTION_QUERY = `{
|
|
8
|
-
__schema {
|
|
9
|
-
queryType { name }
|
|
10
|
-
mutationType { name }
|
|
11
|
-
subscriptionType { name }
|
|
12
|
-
types {
|
|
13
|
-
name kind description
|
|
14
|
-
fields { name description args { name type { name kind ofType { name kind } } } }
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}`;
|
|
18
|
-
async function introspect(url, headers) {
|
|
19
|
-
const res = await fetch(url, {
|
|
20
|
-
method: "POST",
|
|
21
|
-
headers: { "Content-Type": "application/json", ...headers },
|
|
22
|
-
body: JSON.stringify({ query: INTROSPECTION_QUERY })
|
|
23
|
-
});
|
|
24
|
-
if (!res.ok) throw new Error(`Introspection failed: ${res.status} ${res.statusText}`);
|
|
25
|
-
const json = await res.json();
|
|
26
|
-
return json.data?.__schema?.types?.filter((t) => !t.name.startsWith("__")) || [];
|
|
27
|
-
}
|
|
28
|
-
function parseSdl(text) {
|
|
29
|
-
const types = [];
|
|
30
|
-
const typeRegex = /type\s+(\w+)\s*\{([^}]*)}/g;
|
|
31
|
-
let match;
|
|
32
|
-
while ((match = typeRegex.exec(text)) !== null) {
|
|
33
|
-
const [, typeName, body] = match;
|
|
34
|
-
const fields = [];
|
|
35
|
-
const fieldRegex = /(\w+)(?:\(([^)]*)\))?\s*:\s*\S+/g;
|
|
36
|
-
let fm;
|
|
37
|
-
while ((fm = fieldRegex.exec(body)) !== null) {
|
|
38
|
-
const args = [];
|
|
39
|
-
if (fm[2]) {
|
|
40
|
-
const argRegex = /(\w+)\s*:\s*(\w+)/g;
|
|
41
|
-
let am;
|
|
42
|
-
while ((am = argRegex.exec(fm[2])) !== null) {
|
|
43
|
-
args.push({ name: am[1], type: { name: am[2], kind: "SCALAR" } });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
fields.push({ name: fm[1], args: args.length ? args : void 0 });
|
|
47
|
-
}
|
|
48
|
-
types.push({ name: typeName, kind: "OBJECT", fields });
|
|
49
|
-
}
|
|
50
|
-
return types;
|
|
51
|
-
}
|
|
52
|
-
async function parseGraphQL(name, config) {
|
|
53
|
-
let types;
|
|
54
|
-
if (config.spec) {
|
|
55
|
-
process.stderr.write(`Fetching ${config.spec}...
|
|
56
|
-
`);
|
|
57
|
-
const res = await fetch(config.spec);
|
|
58
|
-
if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status}`);
|
|
59
|
-
types = parseSdl(await res.text());
|
|
60
|
-
} else {
|
|
61
|
-
if (!config.url) throw new Error('GraphQL config needs either "spec" or "url"');
|
|
62
|
-
process.stderr.write(`Introspecting ${config.url}...
|
|
63
|
-
`);
|
|
64
|
-
const headers = { ...config.headers };
|
|
65
|
-
const token = config.auth?.env ? process.env[config.auth.env] : void 0;
|
|
66
|
-
if (token) {
|
|
67
|
-
const authType = config.auth?.type || "bearer";
|
|
68
|
-
if (authType === "bearer") headers["Authorization"] = `Bearer ${token}`;
|
|
69
|
-
else if (authType === "api-key") headers[config.auth?.header || "X-API-Key"] = token;
|
|
70
|
-
}
|
|
71
|
-
types = await introspect(config.url, headers);
|
|
72
|
-
}
|
|
73
|
-
const routes = [];
|
|
74
|
-
for (const type of types) {
|
|
75
|
-
if (!type.fields) continue;
|
|
76
|
-
const isQuery = type.name === "Query" || type.name === "RootQuery";
|
|
77
|
-
const isMutation = type.name === "Mutation" || type.name === "RootMutation";
|
|
78
|
-
if (!isQuery && !isMutation) continue;
|
|
79
|
-
const method = isMutation ? "post" : "get";
|
|
80
|
-
for (const field of type.fields) {
|
|
81
|
-
routes.push({
|
|
82
|
-
path: field.name,
|
|
83
|
-
method,
|
|
84
|
-
summary: field.description || "",
|
|
85
|
-
version: "",
|
|
86
|
-
segments: [{ value: field.name, isParam: false }]
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
91
|
-
process.stderr.write(`Parsed ${routes.length} fields (${types.length} types)
|
|
92
|
-
`);
|
|
93
|
-
return { name, description: config.description || "", specVersion: "", config, versions: [], resourceDescriptions: {}, routes };
|
|
94
|
-
}
|
|
95
|
-
function validateGraphQLFlags(method, query, body, apiName) {
|
|
96
|
-
if (["put", "patch", "delete", "head"].includes(method)) {
|
|
97
|
-
return `GraphQL only supports POST. ${method.toUpperCase()} is not valid.`;
|
|
98
|
-
}
|
|
99
|
-
if (method === "post") {
|
|
100
|
-
process.stderr.write(`Note: POST is implicit for GraphQL, flag not needed.
|
|
101
|
-
`);
|
|
102
|
-
}
|
|
103
|
-
if (Object.keys(query).length || Object.keys(body).length) {
|
|
104
|
-
return `GraphQL uses document syntax, not key=value params.
|
|
105
|
-
godmode ${apiName} '{ field { subfield } }'`;
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// src/config.ts
|
|
111
|
-
import { resolve, basename } from "path";
|
|
112
|
-
import { homedir } from "os";
|
|
113
|
-
import { mkdir, readFile, writeFile, readdir, unlink, access } from "fs/promises";
|
|
114
|
-
import { execSync } from "child_process";
|
|
115
|
-
import { parse as parseYaml2 } from "yaml";
|
|
116
|
-
|
|
117
|
-
// src/protocols/api.ts
|
|
118
|
-
import { parse as parseYaml } from "yaml";
|
|
119
|
-
async function parseOpenApi(name, config) {
|
|
120
|
-
process.stderr.write(`Fetching ${config.spec}...
|
|
121
|
-
`);
|
|
122
|
-
const res = await fetch(config.spec);
|
|
123
|
-
if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
|
|
124
|
-
const text = await res.text();
|
|
125
|
-
const isJson = text.trimStart().startsWith("{");
|
|
126
|
-
const spec = isJson ? JSON.parse(text) : parseYaml(text);
|
|
127
|
-
if (!spec.paths) throw new Error("No paths found - is this a valid OpenAPI document?");
|
|
128
|
-
if (!config.url) {
|
|
129
|
-
if (spec.servers?.[0]?.url) {
|
|
130
|
-
config.url = spec.servers[0].url;
|
|
131
|
-
} else if (spec.host) {
|
|
132
|
-
const scheme = spec.schemes?.[0] || "https";
|
|
133
|
-
config.url = `${scheme}://${spec.host}${spec.basePath || ""}`;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const versions = resolveVersions(config, Object.keys(spec.paths));
|
|
137
|
-
const httpMethods = ["get", "post", "put", "patch", "delete", "head"];
|
|
138
|
-
const routes = [];
|
|
139
|
-
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
140
|
-
for (const method of httpMethods) {
|
|
141
|
-
const op = pathItem[method];
|
|
142
|
-
if (!op) continue;
|
|
143
|
-
const ver = versions.find((v) => path.startsWith(v.prefix));
|
|
144
|
-
const version = ver?.name || "";
|
|
145
|
-
const stripped = ver ? path.slice(ver.prefix.length).replace(/^\//, "") : path.slice(1);
|
|
146
|
-
const rawSegments = stripped.split("/").filter(Boolean);
|
|
147
|
-
const segments = rawSegments.map((s) => {
|
|
148
|
-
const isParam = s.startsWith("{") && s.endsWith("}");
|
|
149
|
-
return { value: isParam ? s.slice(1, -1) : s, isParam };
|
|
150
|
-
});
|
|
151
|
-
const tag = op.tags?.[0] || "";
|
|
152
|
-
routes.push({ path, method, summary: op.summary || "", version, tag, segments });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
156
|
-
const resourceDescriptions = {};
|
|
157
|
-
if (spec.tags) {
|
|
158
|
-
const stripHtml = (s) => s.replace(/<[^>]+>/g, "").trim();
|
|
159
|
-
for (const t of spec.tags) {
|
|
160
|
-
if (t.name && t.description) {
|
|
161
|
-
const first = stripHtml(t.description).split(/\.\s/)[0];
|
|
162
|
-
resourceDescriptions[t.name] = first.endsWith(".") ? first : first + ".";
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
const description = config.description || spec.info?.description || "";
|
|
167
|
-
const specVersion = spec.info?.version || "";
|
|
168
|
-
const versionNames = versions.map((v) => v.name).join(", ");
|
|
169
|
-
process.stderr.write(`Parsed ${routes.length} routes (versions: ${versionNames || "none"})
|
|
170
|
-
`);
|
|
171
|
-
return { name, description, specVersion, config, versions, resourceDescriptions, routes };
|
|
172
|
-
}
|
|
173
|
-
function resolveVersions(config, paths) {
|
|
174
|
-
if (config.versions?.length) {
|
|
175
|
-
return config.versions.map((v) => ({
|
|
176
|
-
prefix: v.prefix.endsWith("/") ? v.prefix.slice(0, -1) : v.prefix,
|
|
177
|
-
name: v.name || v.prefix.replace(/^\//, "")
|
|
178
|
-
}));
|
|
179
|
-
}
|
|
180
|
-
if (config.prefix) {
|
|
181
|
-
const prefix = config.prefix.endsWith("/") ? config.prefix.slice(0, -1) : config.prefix;
|
|
182
|
-
return [{ prefix, name: prefix.replace(/^\//, "") }];
|
|
183
|
-
}
|
|
184
|
-
const versionPattern = /^(\/(?:api\/)?v\d+)/;
|
|
185
|
-
const detected = /* @__PURE__ */ new Map();
|
|
186
|
-
for (const p of paths) {
|
|
187
|
-
const match = p.match(versionPattern);
|
|
188
|
-
if (match) detected.set(match[1], (detected.get(match[1]) || 0) + 1);
|
|
189
|
-
}
|
|
190
|
-
if (detected.size) {
|
|
191
|
-
return [...detected.entries()].sort((a, b) => b[1] - a[1]).map(([prefix]) => ({ prefix, name: prefix.replace(/^\//, "") }));
|
|
192
|
-
}
|
|
193
|
-
return [{ prefix: "", name: "" }];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// src/spec.ts
|
|
197
|
-
var parsers = {
|
|
198
|
-
api: parseOpenApi,
|
|
199
|
-
graphql: parseGraphQL,
|
|
200
|
-
mcp: parseMcp
|
|
201
|
-
};
|
|
202
|
-
async function parseSpec(name, config) {
|
|
203
|
-
const parser = parsers[config.type];
|
|
204
|
-
if (!parser) throw new Error(`Unknown type "${config.type}" - supported: ${Object.keys(parsers).join(", ")}`);
|
|
205
|
-
return parser(name, config);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// src/config.ts
|
|
209
|
-
var GODMODE_HOME = process.platform === "linux" && process.env.XDG_CONFIG_HOME ? resolve(process.env.XDG_CONFIG_HOME, "godmode") : resolve(homedir(), ".godmode");
|
|
210
|
-
var APIS_DIR = resolve(GODMODE_HOME, "apis");
|
|
211
|
-
async function ensureDirs() {
|
|
212
|
-
await mkdir(APIS_DIR, { recursive: true });
|
|
213
|
-
}
|
|
214
|
-
async function exists(path) {
|
|
215
|
-
try {
|
|
216
|
-
await access(path);
|
|
217
|
-
return true;
|
|
218
|
-
} catch {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
async function loadManifestFromDir(dir) {
|
|
223
|
-
for (const ext of [".yaml", ".yml", ".json"]) {
|
|
224
|
-
const filePath = resolve(dir, `manifest${ext}`);
|
|
225
|
-
if (await exists(filePath)) {
|
|
226
|
-
const text = await readFile(filePath, "utf-8");
|
|
227
|
-
const config = ext === ".json" ? JSON.parse(text) : parseYaml2(text);
|
|
228
|
-
return { config, ext };
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
async function resolveConfig(input) {
|
|
234
|
-
const asPath = resolve(process.cwd(), input);
|
|
235
|
-
const fromPath = await loadManifestFromDir(asPath);
|
|
236
|
-
if (fromPath) return { name: fromPath.config.slug || basename(asPath), config: fromPath.config, dir: asPath };
|
|
237
|
-
try {
|
|
238
|
-
const pkgDir = resolve(import.meta.dirname, "..", "..", "..", "adapters", input);
|
|
239
|
-
const fromPkg = await loadManifestFromDir(pkgDir);
|
|
240
|
-
if (fromPkg) return { name: fromPkg.config.slug || input, config: fromPkg.config, dir: pkgDir };
|
|
241
|
-
} catch {
|
|
242
|
-
}
|
|
243
|
-
throw new Error(`No config found: ${input}/manifest.yaml or @godmode-cli/${input}`);
|
|
244
|
-
}
|
|
245
|
-
async function addApi(input) {
|
|
246
|
-
await ensureDirs();
|
|
247
|
-
const resolved = await resolveConfig(input).catch(() => null);
|
|
248
|
-
if (resolved) {
|
|
249
|
-
const { name: name2, config } = resolved;
|
|
250
|
-
const type = config.type;
|
|
251
|
-
if (type && ["api", "graphql", "mcp"].includes(type)) {
|
|
252
|
-
if (type === "api" && !config.spec) throw new Error('Config missing "spec" (OpenAPI spec URL or path)');
|
|
253
|
-
if (type === "graphql" && !config.spec && !config.url) throw new Error('GraphQL config needs "spec" (SDL) or "url" (for introspection)');
|
|
254
|
-
if (type === "mcp" && !config.url) throw new Error('MCP config needs "url" (MCP endpoint)');
|
|
255
|
-
if (!config.url && type === "api") process.stderr.write('Warning: no "url" in config - will try to detect from spec\n');
|
|
256
|
-
const manifest = await parseSpec(name2, config);
|
|
257
|
-
await writeFile(resolve(APIS_DIR, `${name2}.json`), JSON.stringify(manifest, null, 2));
|
|
258
|
-
const urlNote = manifest.config.url ? ` at ${manifest.config.url}` : "";
|
|
259
|
-
process.stderr.write(`Registered "${name2}" - ${manifest.routes.length} routes${urlNote}
|
|
260
|
-
`);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const name = resolved?.name || input;
|
|
265
|
-
const installTarget = resolved?.dir && await exists(resolve(resolved.dir, "package.json")) ? resolved.dir : input.startsWith("@") ? input : `@godmode-cli/${input}`;
|
|
266
|
-
process.stderr.write(`Installing ${name}...
|
|
267
|
-
`);
|
|
268
|
-
try {
|
|
269
|
-
execSync(`npm install ${installTarget} --prefix ${GODMODE_HOME}`, { stdio: "pipe" });
|
|
270
|
-
} catch (e) {
|
|
271
|
-
throw new Error(`Failed to install ${name}: ${e.stderr?.toString().trim() || e.message}`);
|
|
272
|
-
}
|
|
273
|
-
const pkgName = `@godmode-cli/${name}`;
|
|
274
|
-
const mcpConfigPath = resolve(GODMODE_HOME, "node_modules", pkgName, ".mcp.json");
|
|
275
|
-
if (await exists(mcpConfigPath)) {
|
|
276
|
-
process.stderr.write(`Installed "${name}" (MCP server adapter)
|
|
277
|
-
`);
|
|
278
|
-
} else {
|
|
279
|
-
process.stderr.write(`Installed "${name}"
|
|
280
|
-
`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
async function updateApi(name) {
|
|
284
|
-
await ensureDirs();
|
|
285
|
-
const manifest = await loadManifestFromDir(name);
|
|
286
|
-
process.stderr.write(`Updating "${name}"...
|
|
287
|
-
`);
|
|
288
|
-
const updated = await parseSpec(name, manifest.config);
|
|
289
|
-
await writeFile(resolve(APIS_DIR, `${name}.json`), JSON.stringify(updated, null, 2));
|
|
290
|
-
process.stderr.write(`Updated "${name}" - ${updated.routes.length} routes
|
|
291
|
-
`);
|
|
292
|
-
}
|
|
293
|
-
async function removeApi(name) {
|
|
294
|
-
try {
|
|
295
|
-
await unlink(resolve(APIS_DIR, `${name}.json`));
|
|
296
|
-
process.stderr.write(`Removed "${name}"
|
|
297
|
-
`);
|
|
298
|
-
} catch {
|
|
299
|
-
process.stderr.write(`API "${name}" not found
|
|
300
|
-
`);
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
async function listApis() {
|
|
305
|
-
await ensureDirs();
|
|
306
|
-
const files = await readdir(APIS_DIR);
|
|
307
|
-
const apis = files.filter((f) => f.endsWith(".json"));
|
|
308
|
-
if (!apis.length) {
|
|
309
|
-
console.log("No APIs registered. Create a <name>.yaml config and run: godmode add <name>");
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
for (const file of apis) {
|
|
313
|
-
const m = JSON.parse(await readFile(resolve(APIS_DIR, file), "utf-8"));
|
|
314
|
-
const ver = m.specVersion ? ` v${m.specVersion}` : "";
|
|
315
|
-
const desc = m.description ? ` ${m.description}` : "";
|
|
316
|
-
const url = m.config.url || "(local)";
|
|
317
|
-
console.log(` ${m.name}${ver} ${url} ${m.routes.length} routes${desc}`);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async function loadManifest(name) {
|
|
321
|
-
await ensureDirs();
|
|
322
|
-
try {
|
|
323
|
-
return JSON.parse(await readFile(resolve(APIS_DIR, `${name}.json`), "utf-8"));
|
|
324
|
-
} catch {
|
|
325
|
-
process.stderr.write(`API "${name}" not found. Create ${name}.yaml and run: godmode add ${name}
|
|
326
|
-
`);
|
|
327
|
-
process.exit(1);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export {
|
|
332
|
-
validateGraphQLFlags,
|
|
333
|
-
GODMODE_HOME,
|
|
334
|
-
addApi,
|
|
335
|
-
updateApi,
|
|
336
|
-
removeApi,
|
|
337
|
-
listApis,
|
|
338
|
-
loadManifest
|
|
339
|
-
};
|