@kanchingyg/repomcp 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +114 -2
- package/SKILL.md +10 -0
- package/dist/adapters/api-routes.d.ts +31 -0
- package/dist/adapters/api-routes.d.ts.map +1 -0
- package/dist/adapters/api-routes.js +680 -0
- package/dist/adapters/api-routes.js.map +1 -0
- package/dist/adapters/cli-crawler.d.ts +28 -0
- package/dist/adapters/cli-crawler.d.ts.map +1 -0
- package/dist/adapters/cli-crawler.js +467 -0
- package/dist/adapters/cli-crawler.js.map +1 -0
- package/dist/adapters/data-sources.d.ts +6 -0
- package/dist/adapters/data-sources.d.ts.map +1 -0
- package/dist/adapters/data-sources.js +672 -0
- package/dist/adapters/data-sources.js.map +1 -0
- package/dist/adapters/docs-bm25.d.ts +50 -0
- package/dist/adapters/docs-bm25.d.ts.map +1 -0
- package/dist/adapters/docs-bm25.js +484 -0
- package/dist/adapters/docs-bm25.js.map +1 -0
- package/dist/adapters/index.d.ts +18 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +66 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/polyglot-code.d.ts +65 -0
- package/dist/adapters/polyglot-code.d.ts.map +1 -0
- package/dist/adapters/polyglot-code.js +691 -0
- package/dist/adapters/polyglot-code.js.map +1 -0
- package/dist/adapters/remote-target.d.ts +30 -0
- package/dist/adapters/remote-target.d.ts.map +1 -0
- package/dist/adapters/remote-target.js +559 -0
- package/dist/adapters/remote-target.js.map +1 -0
- package/dist/adapters/task-runners.d.ts +8 -0
- package/dist/adapters/task-runners.d.ts.map +1 -0
- package/dist/adapters/task-runners.js +468 -0
- package/dist/adapters/task-runners.js.map +1 -0
- package/dist/adapters/types.d.ts +9 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +21 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/bridge.d.ts +136 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +488 -0
- package/dist/bridge.js.map +1 -0
- package/dist/checkpoints.d.ts +41 -0
- package/dist/checkpoints.d.ts.map +1 -0
- package/dist/checkpoints.js +208 -0
- package/dist/checkpoints.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +124 -0
- package/dist/cli.js.map +1 -0
- package/dist/dirty-parser.d.ts +10 -0
- package/dist/dirty-parser.d.ts.map +1 -0
- package/dist/dirty-parser.js +220 -0
- package/dist/dirty-parser.js.map +1 -0
- package/dist/harnesses.d.ts +74 -0
- package/dist/harnesses.d.ts.map +1 -0
- package/dist/harnesses.js +204 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/introspect.d.ts +18 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +125 -0
- package/dist/introspect.js.map +1 -0
- package/dist/path-containment.d.ts +2 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +20 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/providers.d.ts +17 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +127 -0
- package/dist/providers.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/jsr.json +24 -0
- package/package.json +57 -4
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
const HTTP_METHODS = new Set([
|
|
4
|
+
"GET",
|
|
5
|
+
"POST",
|
|
6
|
+
"PUT",
|
|
7
|
+
"PATCH",
|
|
8
|
+
"DELETE",
|
|
9
|
+
"HEAD",
|
|
10
|
+
"OPTIONS",
|
|
11
|
+
]);
|
|
12
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
13
|
+
const SKIP_DIRS = new Set([
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist",
|
|
16
|
+
".git",
|
|
17
|
+
"coverage",
|
|
18
|
+
"build",
|
|
19
|
+
".next",
|
|
20
|
+
"vendor",
|
|
21
|
+
]);
|
|
22
|
+
const OPENAPI_NAMES = /^(openapi|swagger)(\.(json|ya?ml))?$/i;
|
|
23
|
+
const MAX_FILES = 4000;
|
|
24
|
+
function sanitizeToolPart(s) {
|
|
25
|
+
return s
|
|
26
|
+
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
27
|
+
.replace(/^_+|_+$/g, "")
|
|
28
|
+
.toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
function toolNameFromHttp(method, path, suffix) {
|
|
31
|
+
const pathPart = sanitizeToolPart(path
|
|
32
|
+
.replace(/^\//, "")
|
|
33
|
+
.replace(/\{([^}]+)\}/g, "$1")
|
|
34
|
+
.replace(/\//g, "_") || "root");
|
|
35
|
+
const base = `api_${method.toLowerCase()}_${pathPart}`;
|
|
36
|
+
return suffix ? `${base}_${suffix}` : base;
|
|
37
|
+
}
|
|
38
|
+
function methodFlags(method) {
|
|
39
|
+
const upper = method.toUpperCase();
|
|
40
|
+
const mutates = MUTATING_METHODS.has(upper);
|
|
41
|
+
return {
|
|
42
|
+
mutates,
|
|
43
|
+
destructive: upper === "DELETE",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function annotationsFor(method) {
|
|
47
|
+
const { mutates, destructive } = methodFlags(method);
|
|
48
|
+
return {
|
|
49
|
+
readOnlyHint: !mutates,
|
|
50
|
+
destructiveHint: destructive,
|
|
51
|
+
idempotentHint: method.toUpperCase() === "GET" || method.toUpperCase() === "HEAD",
|
|
52
|
+
openWorldHint: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function walkFiles(dir, predicate, max = MAX_FILES) {
|
|
56
|
+
const out = [];
|
|
57
|
+
if (!existsSync(dir))
|
|
58
|
+
return out;
|
|
59
|
+
const stack = [dir];
|
|
60
|
+
while (stack.length > 0 && out.length < max) {
|
|
61
|
+
const current = stack.pop();
|
|
62
|
+
if (!current)
|
|
63
|
+
break;
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = readdirSync(current);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (out.length >= max)
|
|
73
|
+
break;
|
|
74
|
+
if (SKIP_DIRS.has(entry))
|
|
75
|
+
continue;
|
|
76
|
+
const abs = join(current, entry);
|
|
77
|
+
let st;
|
|
78
|
+
try {
|
|
79
|
+
st = statSync(abs);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (st.isDirectory()) {
|
|
85
|
+
stack.push(abs);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const rel = relative(dir, abs).split(sep).join("/");
|
|
89
|
+
if (predicate(entry, rel))
|
|
90
|
+
out.push(abs);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
function readText(path) {
|
|
96
|
+
try {
|
|
97
|
+
return readFileSync(path, "utf8");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function stripJsonc(text) {
|
|
104
|
+
let out = "";
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < text.length) {
|
|
107
|
+
const ch = text[i];
|
|
108
|
+
const next = text[i + 1];
|
|
109
|
+
if (ch === "/" && next === "/") {
|
|
110
|
+
i += 2;
|
|
111
|
+
while (i < text.length && text[i] !== "\n")
|
|
112
|
+
i++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (ch === "/" && next === "*") {
|
|
116
|
+
i += 2;
|
|
117
|
+
while (i < text.length - 1 && !(text[i] === "*" && text[i + 1] === "/"))
|
|
118
|
+
i++;
|
|
119
|
+
i += 2;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (ch === '"') {
|
|
123
|
+
out += ch;
|
|
124
|
+
i++;
|
|
125
|
+
while (i < text.length) {
|
|
126
|
+
out += text[i];
|
|
127
|
+
if (text[i] === "\\") {
|
|
128
|
+
i++;
|
|
129
|
+
if (i < text.length) {
|
|
130
|
+
out += text[i];
|
|
131
|
+
i++;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (text[i] === '"') {
|
|
136
|
+
i++;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
i++;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
out += ch;
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function pathParamNames(routePath) {
|
|
149
|
+
const names = [];
|
|
150
|
+
const re = /\{([^}]+)\}/g;
|
|
151
|
+
let m;
|
|
152
|
+
while ((m = re.exec(routePath)) !== null) {
|
|
153
|
+
names.push(m[1]);
|
|
154
|
+
}
|
|
155
|
+
return names;
|
|
156
|
+
}
|
|
157
|
+
function applyPathParams(routePath, args) {
|
|
158
|
+
return routePath.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
159
|
+
const v = args[key];
|
|
160
|
+
if (v === undefined || v === null)
|
|
161
|
+
return `{${key}}`;
|
|
162
|
+
return encodeURIComponent(String(v));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function schemaWithPathParams(routePath, extra, includeBody = false) {
|
|
166
|
+
const properties = { ...extra };
|
|
167
|
+
for (const p of pathParamNames(routePath)) {
|
|
168
|
+
properties[p] = { type: "string", description: `Path parameter ${p}` };
|
|
169
|
+
}
|
|
170
|
+
if (includeBody) {
|
|
171
|
+
properties.body = {
|
|
172
|
+
type: "object",
|
|
173
|
+
description: "JSON request body",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const required = pathParamNames(routePath).filter((p) => properties[p]);
|
|
177
|
+
return {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties,
|
|
180
|
+
required: required.length > 0 ? required : undefined,
|
|
181
|
+
additionalProperties: false,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function makeRequestHandler(spec, baseUrl) {
|
|
185
|
+
return async (args) => {
|
|
186
|
+
const path = applyPathParams(spec.path, args);
|
|
187
|
+
const body = spec.buildBody !== undefined
|
|
188
|
+
? spec.buildBody(args)
|
|
189
|
+
: args.body !== undefined
|
|
190
|
+
? args.body
|
|
191
|
+
: undefined;
|
|
192
|
+
const request = {
|
|
193
|
+
method: spec.method,
|
|
194
|
+
path,
|
|
195
|
+
...(body !== undefined ? { body } : {}),
|
|
196
|
+
};
|
|
197
|
+
if (!baseUrl) {
|
|
198
|
+
return { request, planned: true };
|
|
199
|
+
}
|
|
200
|
+
const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
201
|
+
const init = { method: spec.method };
|
|
202
|
+
if (body !== undefined && spec.method !== "GET" && spec.method !== "HEAD") {
|
|
203
|
+
init.headers = { "content-type": "application/json" };
|
|
204
|
+
init.body = JSON.stringify(body);
|
|
205
|
+
}
|
|
206
|
+
const res = await fetch(url, init);
|
|
207
|
+
const responseBody = await res.text();
|
|
208
|
+
return {
|
|
209
|
+
request,
|
|
210
|
+
status: res.status,
|
|
211
|
+
responseBody,
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function registerRoute(specs, seen, spec) {
|
|
216
|
+
let name = spec.name;
|
|
217
|
+
let n = 2;
|
|
218
|
+
while (seen.has(name)) {
|
|
219
|
+
name = `${spec.name}_${n}`;
|
|
220
|
+
n++;
|
|
221
|
+
}
|
|
222
|
+
seen.add(name);
|
|
223
|
+
specs.push({ ...spec, name });
|
|
224
|
+
}
|
|
225
|
+
function openApiRoutesFromJson(text, fileLabel) {
|
|
226
|
+
let doc;
|
|
227
|
+
try {
|
|
228
|
+
doc = JSON.parse(text);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
if (!doc.paths || typeof doc.paths !== "object")
|
|
234
|
+
return [];
|
|
235
|
+
const specs = [];
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
for (const [path, methods] of Object.entries(doc.paths)) {
|
|
238
|
+
if (!methods || typeof methods !== "object")
|
|
239
|
+
continue;
|
|
240
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
241
|
+
const upper = method.toUpperCase();
|
|
242
|
+
if (!HTTP_METHODS.has(upper))
|
|
243
|
+
continue;
|
|
244
|
+
const operation = op ?? {};
|
|
245
|
+
const { mutates, destructive } = methodFlags(upper);
|
|
246
|
+
const opId = operation.operationId
|
|
247
|
+
? sanitizeToolPart(operation.operationId)
|
|
248
|
+
: undefined;
|
|
249
|
+
const name = toolNameFromHttp(upper, path, opId);
|
|
250
|
+
const desc = operation.summary ??
|
|
251
|
+
operation.description ??
|
|
252
|
+
`${upper} ${path} (from ${fileLabel})`;
|
|
253
|
+
registerRoute(specs, seen, {
|
|
254
|
+
name,
|
|
255
|
+
method: upper,
|
|
256
|
+
path,
|
|
257
|
+
source: "openapi",
|
|
258
|
+
description: `[openapi] ${desc}`,
|
|
259
|
+
inputSchema: schemaWithPathParams(path, undefined, mutates && upper !== "GET"),
|
|
260
|
+
mutates,
|
|
261
|
+
destructive,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return specs;
|
|
266
|
+
}
|
|
267
|
+
function openApiRoutesFromYaml(text, fileLabel) {
|
|
268
|
+
const trimmed = text.trim();
|
|
269
|
+
if (trimmed.startsWith("{")) {
|
|
270
|
+
return openApiRoutesFromJson(trimmed, fileLabel);
|
|
271
|
+
}
|
|
272
|
+
const specs = [];
|
|
273
|
+
const seen = new Set();
|
|
274
|
+
const lines = text.split(/\r?\n/);
|
|
275
|
+
let inPaths = false;
|
|
276
|
+
let currentPath = null;
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
if (/^paths:\s*$/i.test(line)) {
|
|
279
|
+
inPaths = true;
|
|
280
|
+
currentPath = null;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (inPaths && /^[^\s#]/.test(line) && !/^paths:/i.test(line)) {
|
|
284
|
+
inPaths = false;
|
|
285
|
+
currentPath = null;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (!inPaths)
|
|
289
|
+
continue;
|
|
290
|
+
const pathMatch = line.match(/^ {2}(\/[^\s:]+):\s*$/);
|
|
291
|
+
if (pathMatch) {
|
|
292
|
+
currentPath = pathMatch[1];
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!currentPath)
|
|
296
|
+
continue;
|
|
297
|
+
const methodMatch = line.match(/^ {4}(get|post|put|patch|delete|head|options):\s*$/i);
|
|
298
|
+
if (!methodMatch)
|
|
299
|
+
continue;
|
|
300
|
+
const upper = methodMatch[1].toUpperCase();
|
|
301
|
+
const { mutates, destructive } = methodFlags(upper);
|
|
302
|
+
registerRoute(specs, seen, {
|
|
303
|
+
name: toolNameFromHttp(upper, currentPath),
|
|
304
|
+
method: upper,
|
|
305
|
+
path: currentPath,
|
|
306
|
+
source: "openapi",
|
|
307
|
+
description: `[openapi] ${upper} ${currentPath} (from ${fileLabel})`,
|
|
308
|
+
inputSchema: schemaWithPathParams(currentPath, undefined, mutates && upper !== "GET"),
|
|
309
|
+
mutates,
|
|
310
|
+
destructive,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return specs;
|
|
314
|
+
}
|
|
315
|
+
function discoverOpenApiSpecs(repoRoot) {
|
|
316
|
+
const files = walkFiles(repoRoot, (name, rel) => {
|
|
317
|
+
if (OPENAPI_NAMES.test(name))
|
|
318
|
+
return true;
|
|
319
|
+
return /openapi|swagger/i.test(rel) && /\.(json|ya?ml)$/i.test(name);
|
|
320
|
+
});
|
|
321
|
+
const specs = [];
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
for (const file of files) {
|
|
324
|
+
const text = readText(file);
|
|
325
|
+
if (!text)
|
|
326
|
+
continue;
|
|
327
|
+
const label = relative(repoRoot, file).split(sep).join("/");
|
|
328
|
+
const lower = file.toLowerCase();
|
|
329
|
+
const parsed = lower.endsWith(".json") || text.trim().startsWith("{")
|
|
330
|
+
? openApiRoutesFromJson(text, label)
|
|
331
|
+
: openApiRoutesFromYaml(text, label);
|
|
332
|
+
for (const s of parsed) {
|
|
333
|
+
registerRoute(specs, seen, s);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return specs;
|
|
337
|
+
}
|
|
338
|
+
function parseGraphQLBlock(content, typeName) {
|
|
339
|
+
const re = new RegExp(`type\\s+${typeName}\\s*\\{([^}]*)\\}`, "is");
|
|
340
|
+
const block = content.match(re);
|
|
341
|
+
if (!block)
|
|
342
|
+
return [];
|
|
343
|
+
const fields = [];
|
|
344
|
+
const fieldRe = /^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\(|:)/gm;
|
|
345
|
+
let m;
|
|
346
|
+
while ((m = fieldRe.exec(block[1])) !== null) {
|
|
347
|
+
fields.push(m[1]);
|
|
348
|
+
}
|
|
349
|
+
return fields;
|
|
350
|
+
}
|
|
351
|
+
function discoverGraphQL(repoRoot) {
|
|
352
|
+
const files = walkFiles(repoRoot, (name) => /\.(graphql|gql)$/i.test(name) || name === "schema.graphql");
|
|
353
|
+
const specs = [];
|
|
354
|
+
const seen = new Set();
|
|
355
|
+
for (const file of files) {
|
|
356
|
+
const text = readText(file);
|
|
357
|
+
if (!text)
|
|
358
|
+
continue;
|
|
359
|
+
const label = relative(repoRoot, file).split(sep).join("/");
|
|
360
|
+
for (const field of parseGraphQLBlock(text, "Query")) {
|
|
361
|
+
registerRoute(specs, seen, {
|
|
362
|
+
name: `graphql_query_${sanitizeToolPart(field)}`,
|
|
363
|
+
method: "POST",
|
|
364
|
+
path: "/graphql",
|
|
365
|
+
source: "graphql",
|
|
366
|
+
description: `[graphql] Query.${field} (from ${label})`,
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {
|
|
370
|
+
variables: {
|
|
371
|
+
type: "object",
|
|
372
|
+
description: "GraphQL variables",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
additionalProperties: false,
|
|
376
|
+
},
|
|
377
|
+
mutates: false,
|
|
378
|
+
destructive: false,
|
|
379
|
+
buildBody: (args) => ({
|
|
380
|
+
query: `query { ${field} }`,
|
|
381
|
+
...(args.variables !== undefined ? { variables: args.variables } : {}),
|
|
382
|
+
}),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
for (const field of parseGraphQLBlock(text, "Mutation")) {
|
|
386
|
+
registerRoute(specs, seen, {
|
|
387
|
+
name: `graphql_mutation_${sanitizeToolPart(field)}`,
|
|
388
|
+
method: "POST",
|
|
389
|
+
path: "/graphql",
|
|
390
|
+
source: "graphql",
|
|
391
|
+
description: `[graphql] Mutation.${field} (from ${label})`,
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: {
|
|
395
|
+
variables: {
|
|
396
|
+
type: "object",
|
|
397
|
+
description: "GraphQL variables",
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
},
|
|
402
|
+
mutates: true,
|
|
403
|
+
destructive: false,
|
|
404
|
+
buildBody: (args) => ({
|
|
405
|
+
query: `mutation { ${field} }`,
|
|
406
|
+
...(args.variables !== undefined ? { variables: args.variables } : {}),
|
|
407
|
+
}),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return specs;
|
|
412
|
+
}
|
|
413
|
+
const STATIC_ROUTE_RE = /(?:^|[^\w])(?:app|router|api|server|fastify)\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gim;
|
|
414
|
+
const FASTAPI_ROUTE_RE = /@(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gim;
|
|
415
|
+
function scanSourceRoutes(text, fileLabel) {
|
|
416
|
+
const specs = [];
|
|
417
|
+
const seen = new Set();
|
|
418
|
+
const apply = (method, path) => {
|
|
419
|
+
const upper = method.toUpperCase();
|
|
420
|
+
if (upper === "ALL") {
|
|
421
|
+
for (const m of ["GET", "POST", "PUT", "PATCH", "DELETE"]) {
|
|
422
|
+
applySingle(m, path);
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
applySingle(upper, path);
|
|
427
|
+
};
|
|
428
|
+
const applySingle = (upper, path) => {
|
|
429
|
+
if (!HTTP_METHODS.has(upper))
|
|
430
|
+
return;
|
|
431
|
+
const { mutates, destructive } = methodFlags(upper);
|
|
432
|
+
registerRoute(specs, seen, {
|
|
433
|
+
name: toolNameFromHttp(upper, path),
|
|
434
|
+
method: upper,
|
|
435
|
+
path,
|
|
436
|
+
source: "static",
|
|
437
|
+
description: `[static] ${upper} ${path} (${fileLabel})`,
|
|
438
|
+
inputSchema: schemaWithPathParams(path, undefined, mutates),
|
|
439
|
+
mutates,
|
|
440
|
+
destructive,
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
STATIC_ROUTE_RE.lastIndex = 0;
|
|
444
|
+
let m;
|
|
445
|
+
while ((m = STATIC_ROUTE_RE.exec(text)) !== null) {
|
|
446
|
+
apply(m[1], m[2]);
|
|
447
|
+
}
|
|
448
|
+
FASTAPI_ROUTE_RE.lastIndex = 0;
|
|
449
|
+
while ((m = FASTAPI_ROUTE_RE.exec(text)) !== null) {
|
|
450
|
+
apply(m[1], m[2]);
|
|
451
|
+
}
|
|
452
|
+
return specs;
|
|
453
|
+
}
|
|
454
|
+
function nextRoutePathFromFile(repoRoot, absPath) {
|
|
455
|
+
const rel = relative(repoRoot, absPath).split(sep).join("/");
|
|
456
|
+
const match = rel.match(/(?:^|\/)(?:src\/)?app\/(.+)\/route\.(?:ts|tsx|js|jsx|mts)$/i) ??
|
|
457
|
+
rel.match(/app\/(.+)\/route\.(?:ts|tsx|js|jsx|mts)$/i);
|
|
458
|
+
if (!match)
|
|
459
|
+
return null;
|
|
460
|
+
let segments = match[1].split("/");
|
|
461
|
+
if (segments[0] === "api")
|
|
462
|
+
segments = segments.slice(1);
|
|
463
|
+
const parts = segments.map((seg) => seg.startsWith("[") && seg.endsWith("]")
|
|
464
|
+
? `{${seg.slice(1, -1).replace(/^\.\.\./, "")}}`
|
|
465
|
+
: seg);
|
|
466
|
+
return `/api/${parts.join("/")}`;
|
|
467
|
+
}
|
|
468
|
+
function discoverStaticRoutes(repoRoot) {
|
|
469
|
+
const codeFiles = walkFiles(repoRoot, (name, rel) => {
|
|
470
|
+
if (/route\.(ts|tsx|js|jsx|mts)$/i.test(name) && /(^|\/)app\//i.test(rel)) {
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
return /\.(ts|tsx|js|jsx|py|mts)$/i.test(name);
|
|
474
|
+
});
|
|
475
|
+
const specs = [];
|
|
476
|
+
const seen = new Set();
|
|
477
|
+
for (const file of codeFiles) {
|
|
478
|
+
const text = readText(file);
|
|
479
|
+
if (!text)
|
|
480
|
+
continue;
|
|
481
|
+
const label = relative(repoRoot, file).split(sep).join("/");
|
|
482
|
+
if (/route\.(ts|tsx|js|jsx|mts)$/i.test(file)) {
|
|
483
|
+
const routePath = nextRoutePathFromFile(repoRoot, file);
|
|
484
|
+
if (routePath) {
|
|
485
|
+
const exportRe = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g;
|
|
486
|
+
let em;
|
|
487
|
+
while ((em = exportRe.exec(text)) !== null) {
|
|
488
|
+
const upper = em[1];
|
|
489
|
+
const { mutates, destructive } = methodFlags(upper);
|
|
490
|
+
registerRoute(specs, seen, {
|
|
491
|
+
name: toolNameFromHttp(upper, routePath, "next"),
|
|
492
|
+
method: upper,
|
|
493
|
+
path: routePath,
|
|
494
|
+
source: "static",
|
|
495
|
+
description: `[static] Next.js ${upper} ${routePath} (${label})`,
|
|
496
|
+
inputSchema: schemaWithPathParams(routePath, undefined, mutates),
|
|
497
|
+
mutates,
|
|
498
|
+
destructive,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (/\.(ts|tsx|js|jsx|py|mts)$/i.test(file)) {
|
|
504
|
+
for (const s of scanSourceRoutes(text, label)) {
|
|
505
|
+
registerRoute(specs, seen, s);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return specs;
|
|
510
|
+
}
|
|
511
|
+
function parseWranglerToml(text) {
|
|
512
|
+
const out = { d1: [], kv: [], r2: [] };
|
|
513
|
+
const lines = text.split(/\r?\n/);
|
|
514
|
+
let section = null;
|
|
515
|
+
for (const line of lines) {
|
|
516
|
+
const trimmed = line.trim();
|
|
517
|
+
if (/^\[\[d1_databases\]\]/i.test(trimmed)) {
|
|
518
|
+
section = "d1";
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (/^\[\[kv_namespaces\]\]/i.test(trimmed)) {
|
|
522
|
+
section = "kv";
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (/^\[\[r2_buckets\]\]/i.test(trimmed)) {
|
|
526
|
+
section = "r2";
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (/^\[/.test(trimmed) && !/^\[\[/.test(trimmed)) {
|
|
530
|
+
section = null;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const binding = trimmed.match(/^binding\s*=\s*["']?([^"'\s#]+)["']?/i);
|
|
534
|
+
if (binding && section) {
|
|
535
|
+
out[section].push(binding[1]);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
540
|
+
function parseWranglerJson(text) {
|
|
541
|
+
const out = { d1: [], kv: [], r2: [] };
|
|
542
|
+
let doc;
|
|
543
|
+
try {
|
|
544
|
+
doc = JSON.parse(stripJsonc(text));
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
const readBindings = (key, bucket) => {
|
|
550
|
+
const arr = doc[key];
|
|
551
|
+
if (!Array.isArray(arr))
|
|
552
|
+
return;
|
|
553
|
+
for (const item of arr) {
|
|
554
|
+
if (item && typeof item === "object" && "binding" in item) {
|
|
555
|
+
const b = item.binding;
|
|
556
|
+
if (typeof b === "string" && b)
|
|
557
|
+
out[bucket].push(b);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
readBindings("d1_databases", "d1");
|
|
562
|
+
readBindings("kv_namespaces", "kv");
|
|
563
|
+
readBindings("r2_buckets", "r2");
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
function discoverWranglerBindings(repoRoot) {
|
|
567
|
+
const candidates = [
|
|
568
|
+
join(repoRoot, "wrangler.toml"),
|
|
569
|
+
join(repoRoot, "wrangler.jsonc"),
|
|
570
|
+
join(repoRoot, "wrangler.json"),
|
|
571
|
+
];
|
|
572
|
+
const specs = [];
|
|
573
|
+
const seen = new Set();
|
|
574
|
+
for (const file of candidates) {
|
|
575
|
+
if (!existsSync(file))
|
|
576
|
+
continue;
|
|
577
|
+
const text = readText(file);
|
|
578
|
+
if (!text)
|
|
579
|
+
continue;
|
|
580
|
+
const label = relative(repoRoot, file).split(sep).join("/");
|
|
581
|
+
const bindings = file.endsWith(".toml") ? parseWranglerToml(text) : parseWranglerJson(text);
|
|
582
|
+
const add = (kind, binding) => {
|
|
583
|
+
registerRoute(specs, seen, {
|
|
584
|
+
name: `cf_inspect_${kind}_${sanitizeToolPart(binding)}`,
|
|
585
|
+
method: "GET",
|
|
586
|
+
path: `/__repomcp/bindings/${kind}/${binding}`,
|
|
587
|
+
source: "wrangler",
|
|
588
|
+
description: `[wrangler] Inspect ${kind.toUpperCase()} binding "${binding}" (${label})`,
|
|
589
|
+
inputSchema: {
|
|
590
|
+
type: "object",
|
|
591
|
+
properties: {},
|
|
592
|
+
additionalProperties: false,
|
|
593
|
+
},
|
|
594
|
+
mutates: false,
|
|
595
|
+
destructive: false,
|
|
596
|
+
buildBody: () => ({
|
|
597
|
+
action: "inspect",
|
|
598
|
+
kind,
|
|
599
|
+
binding,
|
|
600
|
+
config: label,
|
|
601
|
+
}),
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
for (const b of bindings.d1)
|
|
605
|
+
add("d1", b);
|
|
606
|
+
for (const b of bindings.kv)
|
|
607
|
+
add("kv", b);
|
|
608
|
+
for (const b of bindings.r2)
|
|
609
|
+
add("r2", b);
|
|
610
|
+
}
|
|
611
|
+
return specs;
|
|
612
|
+
}
|
|
613
|
+
function specsToDiscovery(specs, baseUrl) {
|
|
614
|
+
const tools = [];
|
|
615
|
+
const handlers = {};
|
|
616
|
+
for (const spec of specs) {
|
|
617
|
+
tools.push({
|
|
618
|
+
name: spec.name,
|
|
619
|
+
description: spec.description,
|
|
620
|
+
inputSchema: spec.inputSchema,
|
|
621
|
+
mutates: spec.mutates,
|
|
622
|
+
annotations: {
|
|
623
|
+
...annotationsFor(spec.method),
|
|
624
|
+
destructiveHint: spec.destructive,
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
handlers[spec.name] = makeRequestHandler(spec, baseUrl);
|
|
628
|
+
}
|
|
629
|
+
return { tools, handlers };
|
|
630
|
+
}
|
|
631
|
+
export function summarizeApiDiscovery(discovery) {
|
|
632
|
+
const routes = discovery.tools.map((t) => {
|
|
633
|
+
const sourceMatch = t.description.match(/^\[(\w+)\]/);
|
|
634
|
+
const source = (sourceMatch?.[1] ?? "static");
|
|
635
|
+
const methodMatch = t.description.match(/\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/);
|
|
636
|
+
const pathMatch = t.description.match(/\b(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\/[^\s(]+)/);
|
|
637
|
+
return {
|
|
638
|
+
name: t.name,
|
|
639
|
+
method: methodMatch?.[1] ?? "GET",
|
|
640
|
+
path: pathMatch?.[1] ?? "/",
|
|
641
|
+
source: source === "openapi" ||
|
|
642
|
+
source === "graphql" ||
|
|
643
|
+
source === "static" ||
|
|
644
|
+
source === "wrangler"
|
|
645
|
+
? source
|
|
646
|
+
: "static",
|
|
647
|
+
mutates: Boolean(t.mutates),
|
|
648
|
+
description: t.description,
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
const bySource = {};
|
|
652
|
+
const byMethod = {};
|
|
653
|
+
for (const r of routes) {
|
|
654
|
+
bySource[r.source] = (bySource[r.source] ?? 0) + 1;
|
|
655
|
+
byMethod[r.method] = (byMethod[r.method] ?? 0) + 1;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
routeCount: routes.length,
|
|
659
|
+
bySource,
|
|
660
|
+
byMethod,
|
|
661
|
+
routes,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
export async function discoverApiRoutes(repoRoot, opts) {
|
|
665
|
+
const baseUrl = opts?.baseUrl;
|
|
666
|
+
const seen = new Set();
|
|
667
|
+
const all = [];
|
|
668
|
+
for (const batch of [
|
|
669
|
+
discoverOpenApiSpecs(repoRoot),
|
|
670
|
+
discoverGraphQL(repoRoot),
|
|
671
|
+
discoverStaticRoutes(repoRoot),
|
|
672
|
+
discoverWranglerBindings(repoRoot),
|
|
673
|
+
]) {
|
|
674
|
+
for (const spec of batch) {
|
|
675
|
+
registerRoute(all, seen, spec);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return specsToDiscovery(all, baseUrl);
|
|
679
|
+
}
|
|
680
|
+
//# sourceMappingURL=api-routes.js.map
|