@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,691 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
const MUTATE_RE = /\b(write|delete|remove|rm\b|push|deploy|destroy|drop|insert|update|mutate|create|purge|truncate|kill|prune|uninstall|erase)\b/i;
|
|
4
|
+
const SKIP_DIRS = new Set([
|
|
5
|
+
"node_modules",
|
|
6
|
+
".git",
|
|
7
|
+
"dist",
|
|
8
|
+
"build",
|
|
9
|
+
"vendor",
|
|
10
|
+
"__pycache__",
|
|
11
|
+
".venv",
|
|
12
|
+
"venv",
|
|
13
|
+
"target",
|
|
14
|
+
".turbo",
|
|
15
|
+
".next",
|
|
16
|
+
"coverage",
|
|
17
|
+
]);
|
|
18
|
+
const MAX_FILES = 400;
|
|
19
|
+
function sanitizePart(s) {
|
|
20
|
+
return s
|
|
21
|
+
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
22
|
+
.replace(/^_+|_+$/g, "")
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
function toolName(kind, relFile, symbol) {
|
|
26
|
+
const stem = sanitizePart(relFile.replace(/\.[^.]+$/, "").split(sep).pop() ?? "file");
|
|
27
|
+
const sym = sanitizePart(symbol);
|
|
28
|
+
return `code_${kind}_${stem}_${sym}`.replace(/_+/g, "_");
|
|
29
|
+
}
|
|
30
|
+
function suggestsMutating(name, description) {
|
|
31
|
+
return MUTATE_RE.test(`${name} ${description}`);
|
|
32
|
+
}
|
|
33
|
+
function tsTypeToJsonSchema(typeText) {
|
|
34
|
+
const t = (typeText ?? "").trim().replace(/\s+/g, " ");
|
|
35
|
+
if (!t || t === "void" || t === "undefined")
|
|
36
|
+
return { type: "string" };
|
|
37
|
+
if (/\bboolean\b/i.test(t))
|
|
38
|
+
return { type: "boolean" };
|
|
39
|
+
if (/\bnumber\b/i.test(t) || /\bint\b/i.test(t) || /\bfloat\b/i.test(t))
|
|
40
|
+
return { type: "number" };
|
|
41
|
+
if (/\[\]|Array<|ReadonlyArray</i.test(t))
|
|
42
|
+
return { type: "array", items: { type: "string" } };
|
|
43
|
+
if (/\bRecord<|\bobject\b|\bObject\b/i.test(t))
|
|
44
|
+
return { type: "object", additionalProperties: true };
|
|
45
|
+
return { type: "string", description: t.length < 80 ? t : undefined };
|
|
46
|
+
}
|
|
47
|
+
function pyHintToJsonSchema(hint) {
|
|
48
|
+
const h = (hint ?? "str").trim();
|
|
49
|
+
if (/^bool\b/i.test(h))
|
|
50
|
+
return { type: "boolean" };
|
|
51
|
+
if (/^(int|float)\b/i.test(h))
|
|
52
|
+
return { type: "number" };
|
|
53
|
+
if (/^(list|List|tuple|set)\b/i.test(h))
|
|
54
|
+
return { type: "array", items: { type: "string" } };
|
|
55
|
+
if (/^(dict|Dict|mapping)\b/i.test(h))
|
|
56
|
+
return { type: "object", additionalProperties: true };
|
|
57
|
+
if (/Optional\[(.+)\]/i.test(h))
|
|
58
|
+
return pyHintToJsonSchema(h.replace(/Optional\[(.+)\]/i, "$1"));
|
|
59
|
+
return { type: "string", description: h.length < 60 ? h : undefined };
|
|
60
|
+
}
|
|
61
|
+
function buildInputSchema(params) {
|
|
62
|
+
const properties = {};
|
|
63
|
+
const required = [];
|
|
64
|
+
for (const p of params) {
|
|
65
|
+
const base = p.type
|
|
66
|
+
? p.name.match(/^[a-z]/i)
|
|
67
|
+
? pyHintToJsonSchema(p.type)
|
|
68
|
+
: tsTypeToJsonSchema(p.type)
|
|
69
|
+
: { type: "string" };
|
|
70
|
+
properties[p.name] = {
|
|
71
|
+
...base,
|
|
72
|
+
description: p.description ?? p.type,
|
|
73
|
+
};
|
|
74
|
+
if (p.defaultValue !== undefined)
|
|
75
|
+
properties[p.name].default = p.defaultValue;
|
|
76
|
+
if (p.required)
|
|
77
|
+
required.push(p.name);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties,
|
|
82
|
+
required: required.length > 0 ? required : undefined,
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function entryToTool(entry) {
|
|
87
|
+
const mutates = suggestsMutating(entry.name, entry.description);
|
|
88
|
+
const desc = entry.description.length > 500
|
|
89
|
+
? `${entry.description.slice(0, 497)}...`
|
|
90
|
+
: entry.description;
|
|
91
|
+
return {
|
|
92
|
+
name: toolName(entry.kind, entry.sourceFile, entry.name),
|
|
93
|
+
description: `[${entry.kind}] ${desc} (${entry.sourceFile})`,
|
|
94
|
+
inputSchema: buildInputSchema(entry.params),
|
|
95
|
+
mutates,
|
|
96
|
+
annotations: {
|
|
97
|
+
readOnlyHint: !mutates,
|
|
98
|
+
destructiveHint: mutates,
|
|
99
|
+
idempotentHint: !mutates,
|
|
100
|
+
openWorldHint: entry.kind === "py" || entry.kind === "sh",
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function walkSourceFiles(repoRoot, extensions, max = MAX_FILES) {
|
|
105
|
+
const out = [];
|
|
106
|
+
const stack = [repoRoot];
|
|
107
|
+
while (stack.length > 0 && out.length < max) {
|
|
108
|
+
const dir = stack.pop();
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
entries = readdirSync(dir);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
for (const name of entries) {
|
|
117
|
+
if (out.length >= max)
|
|
118
|
+
break;
|
|
119
|
+
if (name.startsWith(".") && name !== ".github")
|
|
120
|
+
continue;
|
|
121
|
+
const full = join(dir, name);
|
|
122
|
+
let st;
|
|
123
|
+
try {
|
|
124
|
+
st = statSync(full);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (st.isDirectory()) {
|
|
130
|
+
if (SKIP_DIRS.has(name))
|
|
131
|
+
continue;
|
|
132
|
+
stack.push(full);
|
|
133
|
+
}
|
|
134
|
+
else if (extensions.test(name)) {
|
|
135
|
+
out.push(full);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function parseJsDocBlock(block) {
|
|
142
|
+
const lines = block
|
|
143
|
+
.replace(/^\/\*\*?|\*\/$/g, "")
|
|
144
|
+
.split(/\r?\n/)
|
|
145
|
+
.map((l) => l.replace(/^\s*\*\s?/, "").trim());
|
|
146
|
+
const params = new Map();
|
|
147
|
+
const descLines = [];
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
const param = line.match(/^@param\s+(?:\{[^}]+\}\s+)?(\w+)\s*-?\s*(.*)$/);
|
|
150
|
+
if (param) {
|
|
151
|
+
params.set(param[1], param[2].trim());
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (line.startsWith("@"))
|
|
155
|
+
continue;
|
|
156
|
+
if (line)
|
|
157
|
+
descLines.push(line);
|
|
158
|
+
}
|
|
159
|
+
return { description: descLines.join(" ").trim(), params };
|
|
160
|
+
}
|
|
161
|
+
function splitParamList(raw) {
|
|
162
|
+
const parts = [];
|
|
163
|
+
let depth = 0;
|
|
164
|
+
let cur = "";
|
|
165
|
+
for (const ch of raw) {
|
|
166
|
+
if (ch === "<" || ch === "(" || ch === "[" || ch === "{")
|
|
167
|
+
depth++;
|
|
168
|
+
if (ch === ">" || ch === ")" || ch === "]" || ch === "}")
|
|
169
|
+
depth--;
|
|
170
|
+
if (ch === "," && depth === 0) {
|
|
171
|
+
if (cur.trim())
|
|
172
|
+
parts.push(cur.trim());
|
|
173
|
+
cur = "";
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
cur += ch;
|
|
177
|
+
}
|
|
178
|
+
if (cur.trim())
|
|
179
|
+
parts.push(cur.trim());
|
|
180
|
+
return parts;
|
|
181
|
+
}
|
|
182
|
+
function parseTsJsParam(segment) {
|
|
183
|
+
const cleaned = segment.replace(/^\.\.\./, "").trim();
|
|
184
|
+
if (!cleaned || cleaned === "...")
|
|
185
|
+
return null;
|
|
186
|
+
const m = cleaned.match(/^([a-zA-Z_$][\w$]*)\s*(?::\s*([^=]+?))?(?:\s*=\s*(.+))?$/);
|
|
187
|
+
if (!m)
|
|
188
|
+
return null;
|
|
189
|
+
const name = m[1];
|
|
190
|
+
const type = m[2]?.trim();
|
|
191
|
+
const def = m[3]?.trim();
|
|
192
|
+
return {
|
|
193
|
+
name,
|
|
194
|
+
type,
|
|
195
|
+
required: def === undefined && !segment.includes("?") && !type?.includes("undefined"),
|
|
196
|
+
defaultValue: def,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const EXPORT_FN_RE = /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][\w$]*)\s*\(([^)]*)\)(?:\s*:\s*([^{;\n]+))?/g;
|
|
200
|
+
const EXPORT_CONST_FN_RE = /(?:^|\n)\s*export\s+const\s+([a-zA-Z_$][\w$]*)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*([^=]+))?\s*=>/g;
|
|
201
|
+
const EXPORT_DECL_FN_RE = /(?:^|\n)\s*export\s+(?:async\s+)?function\s+([a-zA-Z_$][\w$]*)\s*\(([^)]*)\)/g;
|
|
202
|
+
function precedingJsDoc(text, index) {
|
|
203
|
+
const before = text.slice(0, index);
|
|
204
|
+
const m = before.match(/(\/\*\*[\s\S]*?\*\/)\s*$/);
|
|
205
|
+
return m?.[1] ?? null;
|
|
206
|
+
}
|
|
207
|
+
export function parseTsJsSource(text, relPath) {
|
|
208
|
+
const kind = /\.(tsx?|mts|cts)$/i.test(relPath) ? "ts" : "js";
|
|
209
|
+
const entries = [];
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
const consider = (exportName, paramsRaw, index, returnType) => {
|
|
212
|
+
if (exportName.startsWith("_") || seen.has(exportName))
|
|
213
|
+
return;
|
|
214
|
+
seen.add(exportName);
|
|
215
|
+
const jsdoc = precedingJsDoc(text, index);
|
|
216
|
+
const doc = jsdoc ? parseJsDocBlock(jsdoc) : { description: "", params: new Map() };
|
|
217
|
+
const params = splitParamList(paramsRaw)
|
|
218
|
+
.map(parseTsJsParam)
|
|
219
|
+
.filter((p) => p !== null)
|
|
220
|
+
.map((p) => ({
|
|
221
|
+
...p,
|
|
222
|
+
description: doc.params.get(p.name) ?? p.description,
|
|
223
|
+
}));
|
|
224
|
+
const desc = doc.description ||
|
|
225
|
+
`Invoke exported function ${exportName}${returnType ? ` → ${returnType.trim()}` : ""}`;
|
|
226
|
+
entries.push({
|
|
227
|
+
kind,
|
|
228
|
+
name: exportName,
|
|
229
|
+
description: desc,
|
|
230
|
+
params,
|
|
231
|
+
sourceFile: relPath,
|
|
232
|
+
invoke: { kind, exportName, filePath: relPath },
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
let m;
|
|
236
|
+
EXPORT_DECL_FN_RE.lastIndex = 0;
|
|
237
|
+
while ((m = EXPORT_DECL_FN_RE.exec(text)) !== null) {
|
|
238
|
+
consider(m[1], m[2], m.index, undefined);
|
|
239
|
+
}
|
|
240
|
+
EXPORT_FN_RE.lastIndex = 0;
|
|
241
|
+
while ((m = EXPORT_FN_RE.exec(text)) !== null) {
|
|
242
|
+
if (!m[0].includes("export"))
|
|
243
|
+
continue;
|
|
244
|
+
consider(m[1], m[2], m.index, m[3]?.trim());
|
|
245
|
+
}
|
|
246
|
+
EXPORT_CONST_FN_RE.lastIndex = 0;
|
|
247
|
+
while ((m = EXPORT_CONST_FN_RE.exec(text)) !== null) {
|
|
248
|
+
consider(m[1], m[2], m.index, m[3]?.trim());
|
|
249
|
+
}
|
|
250
|
+
return entries;
|
|
251
|
+
}
|
|
252
|
+
function parsePythonDefParams(raw) {
|
|
253
|
+
const parts = splitParamList(raw);
|
|
254
|
+
const params = [];
|
|
255
|
+
for (const seg of parts) {
|
|
256
|
+
const cleaned = seg.replace(/^\*\*?/, "").trim();
|
|
257
|
+
if (!cleaned || cleaned === "self" || cleaned === "cls")
|
|
258
|
+
continue;
|
|
259
|
+
const m = cleaned.match(/^(\w+)\s*(?::\s*([^=]+))?(?:\s*=\s*(.+))?$/);
|
|
260
|
+
if (!m)
|
|
261
|
+
continue;
|
|
262
|
+
params.push({
|
|
263
|
+
name: m[1],
|
|
264
|
+
type: m[2]?.trim(),
|
|
265
|
+
defaultValue: m[3]?.trim(),
|
|
266
|
+
required: m[3] === undefined,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return params;
|
|
270
|
+
}
|
|
271
|
+
function pythonDocstringAfterDef(text, defEndIndex) {
|
|
272
|
+
const rest = text.slice(defEndIndex);
|
|
273
|
+
const m = rest.match(/^\s*(?:"""([\s\S]*?)"""|'''([\s\S]*?)''')/);
|
|
274
|
+
return (m?.[1] ?? m?.[2] ?? "").trim().split(/\r?\n/)[0] ?? "";
|
|
275
|
+
}
|
|
276
|
+
const PY_DEF_RE = /(?:^|\n)(async\s+)?def\s+([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*(?:->\s*([^:\n]+))?/g;
|
|
277
|
+
const CLICK_OPTION_RE = /@click\.option\s*\(\s*["']([^"']+)["'](?:[^)]*help\s*=\s*["']([^"']*)["'])?[^)]*\)/g;
|
|
278
|
+
const CLICK_COMMAND_RE = /@click\.command\s*\(\s*(?:[^)]*name\s*=\s*["']([^"']+)["'])?[^)]*\)/g;
|
|
279
|
+
const TYPER_OPTION_RE = /(\w+)\s*:\s*[^=]+\s*=\s*typer\.Option\s*\(([^)]*)\)/g;
|
|
280
|
+
const ARGPARSE_RE = /\.add_argument\s*\(\s*["']([^"']+)["']([^)]*)\)/g;
|
|
281
|
+
export function parsePythonSource(text, relPath) {
|
|
282
|
+
const entries = [];
|
|
283
|
+
const seen = new Set();
|
|
284
|
+
PY_DEF_RE.lastIndex = 0;
|
|
285
|
+
let m;
|
|
286
|
+
while ((m = PY_DEF_RE.exec(text)) !== null) {
|
|
287
|
+
const name = m[2];
|
|
288
|
+
if (name.startsWith("_"))
|
|
289
|
+
continue;
|
|
290
|
+
const params = parsePythonDefParams(m[3]);
|
|
291
|
+
const hasHintsOrDoc = params.some((p) => p.type) ||
|
|
292
|
+
pythonDocstringAfterDef(text, m.index + m[0].length).length > 0;
|
|
293
|
+
if (!hasHintsOrDoc && params.length > 0)
|
|
294
|
+
continue;
|
|
295
|
+
const key = `fn:${name}`;
|
|
296
|
+
if (seen.has(key))
|
|
297
|
+
continue;
|
|
298
|
+
seen.add(key);
|
|
299
|
+
const doc = pythonDocstringAfterDef(text, m.index + m[0].length);
|
|
300
|
+
entries.push({
|
|
301
|
+
kind: "py",
|
|
302
|
+
name,
|
|
303
|
+
description: doc || `Python function ${name}`,
|
|
304
|
+
params,
|
|
305
|
+
sourceFile: relPath,
|
|
306
|
+
invoke: { kind: "py", mode: "function", filePath: relPath, entryName: name },
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
CLICK_COMMAND_RE.lastIndex = 0;
|
|
310
|
+
while ((m = CLICK_COMMAND_RE.exec(text)) !== null) {
|
|
311
|
+
const clickName = m[1] ?? "main";
|
|
312
|
+
const sliceStart = m.index;
|
|
313
|
+
const slice = text.slice(sliceStart, sliceStart + 2000);
|
|
314
|
+
const opts = [];
|
|
315
|
+
CLICK_OPTION_RE.lastIndex = 0;
|
|
316
|
+
let om;
|
|
317
|
+
while ((om = CLICK_OPTION_RE.exec(slice)) !== null) {
|
|
318
|
+
const flag = om[1].replace(/^-+/, "");
|
|
319
|
+
opts.push({
|
|
320
|
+
name: sanitizePart(flag),
|
|
321
|
+
description: om[2] || `Click option ${om[1]}`,
|
|
322
|
+
required: false,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const key = `click:${clickName}`;
|
|
326
|
+
if (seen.has(key))
|
|
327
|
+
continue;
|
|
328
|
+
seen.add(key);
|
|
329
|
+
entries.push({
|
|
330
|
+
kind: "py",
|
|
331
|
+
name: clickName,
|
|
332
|
+
description: `Click command ${clickName}`,
|
|
333
|
+
params: opts,
|
|
334
|
+
sourceFile: relPath,
|
|
335
|
+
invoke: {
|
|
336
|
+
kind: "py",
|
|
337
|
+
mode: "click",
|
|
338
|
+
filePath: relPath,
|
|
339
|
+
entryName: clickName,
|
|
340
|
+
clickName,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
TYPER_OPTION_RE.lastIndex = 0;
|
|
345
|
+
const typerParams = [];
|
|
346
|
+
while ((m = TYPER_OPTION_RE.exec(text)) !== null) {
|
|
347
|
+
typerParams.push({
|
|
348
|
+
name: m[1],
|
|
349
|
+
description: m[2].trim().slice(0, 120),
|
|
350
|
+
required: !/default\s*=/i.test(m[2]),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (typerParams.length > 0 || /typer\.Typer\s*\(/i.test(text)) {
|
|
354
|
+
const appName = "typer_app";
|
|
355
|
+
const key = `typer:${relPath}`;
|
|
356
|
+
if (!seen.has(key)) {
|
|
357
|
+
seen.add(key);
|
|
358
|
+
entries.push({
|
|
359
|
+
kind: "py",
|
|
360
|
+
name: appName,
|
|
361
|
+
description: `Typer CLI in ${relPath}`,
|
|
362
|
+
params: typerParams,
|
|
363
|
+
sourceFile: relPath,
|
|
364
|
+
invoke: { kind: "py", mode: "typer", filePath: relPath, entryName: appName },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
ARGPARSE_RE.lastIndex = 0;
|
|
369
|
+
const argParams = [];
|
|
370
|
+
while ((m = ARGPARSE_RE.exec(text)) !== null) {
|
|
371
|
+
const flag = m[1];
|
|
372
|
+
const rest = m[2];
|
|
373
|
+
const isPositional = !flag.startsWith("-");
|
|
374
|
+
const propName = isPositional
|
|
375
|
+
? flag
|
|
376
|
+
: sanitizePart(flag.replace(/^--+/, ""));
|
|
377
|
+
const typeM = rest.match(/type\s*=\s*(\w+)/);
|
|
378
|
+
const helpM = rest.match(/help\s*=\s*["']([^"']*)["']/);
|
|
379
|
+
const actionM = rest.match(/action\s*=\s*["'](\w+)["']/);
|
|
380
|
+
argParams.push({
|
|
381
|
+
name: propName,
|
|
382
|
+
type: typeM?.[1],
|
|
383
|
+
description: helpM?.[1],
|
|
384
|
+
required: /required\s*=\s*True/i.test(rest),
|
|
385
|
+
...(actionM?.[1] === "store_true" ? { type: "bool" } : {}),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (argParams.length > 0) {
|
|
389
|
+
const key = `argparse:${relPath}`;
|
|
390
|
+
if (!seen.has(key)) {
|
|
391
|
+
seen.add(key);
|
|
392
|
+
entries.push({
|
|
393
|
+
kind: "py",
|
|
394
|
+
name: "argparse_main",
|
|
395
|
+
description: `argparse CLI in ${relPath}`,
|
|
396
|
+
params: argParams,
|
|
397
|
+
sourceFile: relPath,
|
|
398
|
+
invoke: { kind: "py", mode: "argparse", filePath: relPath, entryName: "main" },
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return entries;
|
|
403
|
+
}
|
|
404
|
+
const GO_FLAG_RE = /flag\.(String|Int|Bool|Float64)\s*\(\s*["']([^"']+)["']\s*,\s*[^,]*,\s*["']([^"']*)["']\s*\)/g;
|
|
405
|
+
const COBRA_USE_RE = /&cobra\.Command\s*\{[^}]*Use:\s*["']([^"']+)["'][^}]*Short:\s*["']([^"']*)["']/g;
|
|
406
|
+
export function parseGoSource(text, relPath) {
|
|
407
|
+
const entries = [];
|
|
408
|
+
let m;
|
|
409
|
+
GO_FLAG_RE.lastIndex = 0;
|
|
410
|
+
while ((m = GO_FLAG_RE.exec(text)) !== null) {
|
|
411
|
+
const varName = m[2];
|
|
412
|
+
entries.push({
|
|
413
|
+
kind: "go",
|
|
414
|
+
name: varName,
|
|
415
|
+
description: m[3] || `Go flag ${varName}`,
|
|
416
|
+
params: [{ name: varName, type: m[1].toLowerCase(), required: false }],
|
|
417
|
+
sourceFile: relPath,
|
|
418
|
+
invoke: {
|
|
419
|
+
kind: "go",
|
|
420
|
+
mode: "flag",
|
|
421
|
+
filePath: relPath,
|
|
422
|
+
symbol: varName,
|
|
423
|
+
flagName: varName,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
COBRA_USE_RE.lastIndex = 0;
|
|
428
|
+
while ((m = COBRA_USE_RE.exec(text)) !== null) {
|
|
429
|
+
const use = m[1];
|
|
430
|
+
entries.push({
|
|
431
|
+
kind: "go",
|
|
432
|
+
name: use,
|
|
433
|
+
description: m[2] || `Cobra command ${use}`,
|
|
434
|
+
params: [],
|
|
435
|
+
sourceFile: relPath,
|
|
436
|
+
invoke: { kind: "go", mode: "cobra", filePath: relPath, symbol: use },
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return entries;
|
|
440
|
+
}
|
|
441
|
+
const RUST_STRUCT_RE = /struct\s+(\w+)\s*\{([^}]+)\}/g;
|
|
442
|
+
const RUST_ARG_RE = /#\[arg\(([^\]]*)\)\]\s*\n\s*pub\s+(\w+)\s*:\s*([^,\n]+)/g;
|
|
443
|
+
function parseRustArgAttrs(attrs) {
|
|
444
|
+
const longM = attrs.match(/long\s*=\s*["']([^"']+)["']/);
|
|
445
|
+
const shortM = attrs.match(/short\s*=\s*['"](.)['"]/);
|
|
446
|
+
const longBare = attrs.match(/\blong\b/);
|
|
447
|
+
return {
|
|
448
|
+
long: longM?.[1] ?? (longBare ? undefined : undefined),
|
|
449
|
+
short: shortM?.[1],
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
export function parseRustSource(text, relPath) {
|
|
453
|
+
const entries = [];
|
|
454
|
+
let m;
|
|
455
|
+
RUST_ARG_RE.lastIndex = 0;
|
|
456
|
+
while ((m = RUST_ARG_RE.exec(text)) !== null) {
|
|
457
|
+
const attrs = parseRustArgAttrs(m[1]);
|
|
458
|
+
const field = m[2];
|
|
459
|
+
const ty = m[3].trim();
|
|
460
|
+
const structCtx = text.slice(0, m.index).match(/struct\s+(\w+)\s*\{[^}]*$/);
|
|
461
|
+
const structName = structCtx?.[1] ?? "Cli";
|
|
462
|
+
entries.push({
|
|
463
|
+
kind: "rust",
|
|
464
|
+
name: `${structName}_${field}`,
|
|
465
|
+
description: `clap field ${field} on ${structName}`,
|
|
466
|
+
params: [
|
|
467
|
+
{
|
|
468
|
+
name: attrs.long ?? field,
|
|
469
|
+
type: ty,
|
|
470
|
+
required: !ty.includes("Option"),
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
sourceFile: relPath,
|
|
474
|
+
invoke: {
|
|
475
|
+
kind: "rust",
|
|
476
|
+
filePath: relPath,
|
|
477
|
+
structName,
|
|
478
|
+
fieldName: field,
|
|
479
|
+
long: attrs.long,
|
|
480
|
+
short: attrs.short,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
if (entries.length === 0) {
|
|
485
|
+
RUST_STRUCT_RE.lastIndex = 0;
|
|
486
|
+
while ((m = RUST_STRUCT_RE.exec(text)) !== null) {
|
|
487
|
+
if (!/Parser|Args|Cli/i.test(m[1]))
|
|
488
|
+
continue;
|
|
489
|
+
const body = m[2];
|
|
490
|
+
const fieldRe = /#\[arg[^\]]*\]\s*\n\s*pub\s+(\w+)/g;
|
|
491
|
+
let fm;
|
|
492
|
+
while ((fm = fieldRe.exec(body)) !== null) {
|
|
493
|
+
entries.push({
|
|
494
|
+
kind: "rust",
|
|
495
|
+
name: `${m[1]}_${fm[1]}`,
|
|
496
|
+
description: `clap ${fm[1]} on ${m[1]}`,
|
|
497
|
+
params: [{ name: fm[1], required: true }],
|
|
498
|
+
sourceFile: relPath,
|
|
499
|
+
invoke: {
|
|
500
|
+
kind: "rust",
|
|
501
|
+
filePath: relPath,
|
|
502
|
+
structName: m[1],
|
|
503
|
+
fieldName: fm[1],
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return entries;
|
|
510
|
+
}
|
|
511
|
+
const SH_PARAM_RE = /^\s*#\s*@param\s+(\w+)(?:\s+(\S+))?(?:\s+(.*))?$/gm;
|
|
512
|
+
const SH_FN_RE = /^(?:function\s+)?([a-zA-Z_][\w]*)\s*\(\)\s*\{/gm;
|
|
513
|
+
export function parseShellSource(text, relPath) {
|
|
514
|
+
const entries = [];
|
|
515
|
+
const params = [];
|
|
516
|
+
let m;
|
|
517
|
+
SH_PARAM_RE.lastIndex = 0;
|
|
518
|
+
while ((m = SH_PARAM_RE.exec(text)) !== null) {
|
|
519
|
+
params.push({
|
|
520
|
+
name: m[1],
|
|
521
|
+
type: m[2],
|
|
522
|
+
description: m[3]?.trim(),
|
|
523
|
+
required: true,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const fnNames = [];
|
|
527
|
+
SH_FN_RE.lastIndex = 0;
|
|
528
|
+
while ((m = SH_FN_RE.exec(text)) !== null) {
|
|
529
|
+
if (!m[1].startsWith("_"))
|
|
530
|
+
fnNames.push(m[1]);
|
|
531
|
+
}
|
|
532
|
+
if (params.length > 0) {
|
|
533
|
+
const label = fnNames[0] ?? relPath.split(sep).pop() ?? "script";
|
|
534
|
+
entries.push({
|
|
535
|
+
kind: "sh",
|
|
536
|
+
name: label.replace(/\.sh$/i, ""),
|
|
537
|
+
description: `Shell script ${relPath} (${params.length} @param)`,
|
|
538
|
+
params,
|
|
539
|
+
sourceFile: relPath,
|
|
540
|
+
invoke: { kind: "sh", filePath: relPath, functionName: fnNames[0] },
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return entries;
|
|
544
|
+
}
|
|
545
|
+
export function resolvePythonLauncher(repoRoot) {
|
|
546
|
+
if (existsSync(join(repoRoot, "pyproject.toml")))
|
|
547
|
+
return "uv";
|
|
548
|
+
if (existsSync(join(repoRoot, "uv.lock")))
|
|
549
|
+
return "uv";
|
|
550
|
+
return "python3";
|
|
551
|
+
}
|
|
552
|
+
export function buildPythonArgv(repoRoot, invoke, args) {
|
|
553
|
+
const via = resolvePythonLauncher(repoRoot);
|
|
554
|
+
const absScript = join(repoRoot, invoke.filePath);
|
|
555
|
+
const prefix = via === "uv" ? ["uv", "run", "python"] : via === "python3" ? ["python3"] : ["python"];
|
|
556
|
+
const argv = [...prefix, absScript];
|
|
557
|
+
if (invoke.mode === "click" || invoke.mode === "typer" || invoke.mode === "argparse") {
|
|
558
|
+
for (const [k, v] of Object.entries(args)) {
|
|
559
|
+
if (v === undefined || v === null)
|
|
560
|
+
continue;
|
|
561
|
+
if (typeof v === "boolean") {
|
|
562
|
+
if (v)
|
|
563
|
+
argv.push(`--${k}`);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
argv.push(`--${k}`, String(v));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
argv.push("-c", `_repomcp_bridge_${invoke.entryName}`, JSON.stringify(args));
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
planned: true,
|
|
575
|
+
via,
|
|
576
|
+
argv,
|
|
577
|
+
script: invoke.filePath,
|
|
578
|
+
entry: invoke.entryName,
|
|
579
|
+
args,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function makeHandler(repoRoot, entry) {
|
|
583
|
+
return async (args) => {
|
|
584
|
+
switch (entry.invoke.kind) {
|
|
585
|
+
case "py":
|
|
586
|
+
return buildPythonArgv(repoRoot, entry.invoke, args);
|
|
587
|
+
case "ts":
|
|
588
|
+
case "js": {
|
|
589
|
+
const modPath = join(repoRoot, entry.invoke.filePath);
|
|
590
|
+
return {
|
|
591
|
+
planned: true,
|
|
592
|
+
via: "node",
|
|
593
|
+
argv: ["node", modPath],
|
|
594
|
+
exportName: entry.invoke.exportName,
|
|
595
|
+
args,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
case "go": {
|
|
599
|
+
const iv = entry.invoke;
|
|
600
|
+
if (iv.mode === "flag" && iv.flagName) {
|
|
601
|
+
const val = args[iv.flagName];
|
|
602
|
+
return {
|
|
603
|
+
planned: true,
|
|
604
|
+
via: "go",
|
|
605
|
+
argv: ["go", "run", join(repoRoot, iv.filePath), `-${iv.flagName}`, String(val ?? "")],
|
|
606
|
+
args,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
planned: true,
|
|
611
|
+
via: "go",
|
|
612
|
+
argv: ["go", "run", join(repoRoot, iv.filePath), iv.symbol],
|
|
613
|
+
args,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
case "rust": {
|
|
617
|
+
const iv = entry.invoke;
|
|
618
|
+
const flag = iv.long ?? iv.fieldName;
|
|
619
|
+
const val = args[flag] ?? args[iv.fieldName];
|
|
620
|
+
return {
|
|
621
|
+
planned: true,
|
|
622
|
+
via: "cargo",
|
|
623
|
+
argv: ["cargo", "run", "--", `--${flag}`, String(val ?? "")],
|
|
624
|
+
crate: iv.structName,
|
|
625
|
+
args,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
case "sh": {
|
|
629
|
+
const iv = entry.invoke;
|
|
630
|
+
const envArgs = Object.entries(args).flatMap(([k, v]) => [`${k}=${String(v)}`]);
|
|
631
|
+
return {
|
|
632
|
+
planned: true,
|
|
633
|
+
via: "sh",
|
|
634
|
+
argv: ["sh", join(repoRoot, iv.filePath), ...envArgs],
|
|
635
|
+
functionName: iv.functionName,
|
|
636
|
+
args,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
default: {
|
|
640
|
+
const _never = entry.invoke;
|
|
641
|
+
return _never;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
export function discoverPolyglotCode(repoRoot) {
|
|
647
|
+
const tools = [];
|
|
648
|
+
const handlers = {};
|
|
649
|
+
const usedNames = new Set();
|
|
650
|
+
const rel = (abs) => relative(repoRoot, abs).split(sep).join("/");
|
|
651
|
+
const ingest = (entry) => {
|
|
652
|
+
const tool = entryToTool(entry);
|
|
653
|
+
let name = tool.name;
|
|
654
|
+
let n = 2;
|
|
655
|
+
while (usedNames.has(name)) {
|
|
656
|
+
name = `${tool.name}_${n}`;
|
|
657
|
+
n++;
|
|
658
|
+
}
|
|
659
|
+
usedNames.add(name);
|
|
660
|
+
tool.name = name;
|
|
661
|
+
tools.push(tool);
|
|
662
|
+
handlers[name] = makeHandler(repoRoot, entry);
|
|
663
|
+
};
|
|
664
|
+
for (const file of walkSourceFiles(repoRoot, /\.(tsx?|mts|cts|jsx?|mjs|cjs)$/i)) {
|
|
665
|
+
const text = readFileSync(file, "utf8");
|
|
666
|
+
for (const e of parseTsJsSource(text, rel(file)))
|
|
667
|
+
ingest(e);
|
|
668
|
+
}
|
|
669
|
+
for (const file of walkSourceFiles(repoRoot, /\.py$/i)) {
|
|
670
|
+
const text = readFileSync(file, "utf8");
|
|
671
|
+
for (const e of parsePythonSource(text, rel(file)))
|
|
672
|
+
ingest(e);
|
|
673
|
+
}
|
|
674
|
+
for (const file of walkSourceFiles(repoRoot, /\.go$/i)) {
|
|
675
|
+
const text = readFileSync(file, "utf8");
|
|
676
|
+
for (const e of parseGoSource(text, rel(file)))
|
|
677
|
+
ingest(e);
|
|
678
|
+
}
|
|
679
|
+
for (const file of walkSourceFiles(repoRoot, /\.rs$/i)) {
|
|
680
|
+
const text = readFileSync(file, "utf8");
|
|
681
|
+
for (const e of parseRustSource(text, rel(file)))
|
|
682
|
+
ingest(e);
|
|
683
|
+
}
|
|
684
|
+
for (const file of walkSourceFiles(repoRoot, /\.sh$/i)) {
|
|
685
|
+
const text = readFileSync(file, "utf8");
|
|
686
|
+
for (const e of parseShellSource(text, rel(file)))
|
|
687
|
+
ingest(e);
|
|
688
|
+
}
|
|
689
|
+
return { tools, handlers };
|
|
690
|
+
}
|
|
691
|
+
//# sourceMappingURL=polyglot-code.js.map
|