@kurtel/cli 0.1.13 → 0.1.15

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.
@@ -9,7 +9,7 @@ export async function onboardCommand(opts = {}) {
9
9
  const root = repoRoot();
10
10
  // 1. Index structurel — local, déterministe, 0 token.
11
11
  const spin = opts.json ? null : new Spinner("Indexing codebase (local, deterministic)…").start();
12
- const index = buildIndex(root, (n) => {
12
+ const index = await buildIndex(root, (n) => {
13
13
  spin?.update(`Indexing codebase… ${n} files`);
14
14
  });
15
15
  saveIndex(root, index);
@@ -0,0 +1,580 @@
1
+ import { createRequire } from "node:module";
2
+ import { dirname, join } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ const require = createRequire(import.meta.url);
5
+ let initFailed = false;
6
+ let parser = null;
7
+ const languages = new Map(); // grammarName → Language
8
+ const missing = new Set();
9
+ function grammarFor(ext) {
10
+ switch (ext) {
11
+ case ".ts":
12
+ case ".mts":
13
+ case ".cts": return "typescript";
14
+ case ".tsx": return "tsx";
15
+ case ".js":
16
+ case ".jsx":
17
+ case ".mjs":
18
+ case ".cjs": return "javascript"; // jsx couvert par la grammaire JS
19
+ case ".py": return "python";
20
+ case ".java": return "java";
21
+ case ".go": return "go";
22
+ case ".rs": return "rust";
23
+ case ".cs": return "c_sharp";
24
+ default: return null;
25
+ }
26
+ }
27
+ async function getParser(ext) {
28
+ if (initFailed)
29
+ return null;
30
+ const grammar = grammarFor(ext);
31
+ if (!grammar || missing.has(grammar))
32
+ return null;
33
+ try {
34
+ if (!parser) {
35
+ const Parser = require("web-tree-sitter");
36
+ await Parser.init();
37
+ parser = new Parser();
38
+ getParser.ParserClass = Parser;
39
+ }
40
+ let lang = languages.get(grammar);
41
+ if (!lang) {
42
+ const wasmDir = join(dirname(require.resolve("tree-sitter-wasms/package.json")), "out");
43
+ const wasmPath = join(wasmDir, `tree-sitter-${grammar}.wasm`);
44
+ if (!existsSync(wasmPath)) {
45
+ missing.add(grammar);
46
+ return null;
47
+ }
48
+ const Parser = getParser.ParserClass;
49
+ lang = await Parser.Language.load(wasmPath);
50
+ languages.set(grammar, lang);
51
+ }
52
+ return { parser, lang };
53
+ }
54
+ catch {
55
+ initFailed = true; // environnement sans WASM: fallback regex global, une seule tentative
56
+ return null;
57
+ }
58
+ }
59
+ // ── Helpers communs ──────────────────────────────────────────────────────────
60
+ const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options", "all"]);
61
+ const ROUTE_OBJECTS = new Set(["app", "router", "server", "api", "fastify"]);
62
+ const NEST_DECORATORS = new Set(["Get", "Post", "Put", "Patch", "Delete", "Head", "Options"]);
63
+ function stripQuotes(s) {
64
+ return s.replace(/^["'`]|["'`]$/g, "");
65
+ }
66
+ function line(n) { return n.startPosition.row + 1; }
67
+ /** Ligne de la définition nommée englobante (0 = top-level / handler anonyme → "(module)").
68
+ * Renvoie la ligne du NŒUD PORTEUR DU NOM (def enregistrée dans facts.defs), pour
69
+ * que l'attribution par `owner` matche `byLine` côté buildSymbols. */
70
+ function enclosingDefLine(node, defKinds, nameOf) {
71
+ let cur = node.parent;
72
+ while (cur) {
73
+ if (defKinds.has(cur.type)) {
74
+ const name = nameOf(cur);
75
+ if (name) {
76
+ // La def est enregistrée à la ligne du déclarateur/méthode, pas de l'arrow.
77
+ if (cur.type === "arrow_function" || cur.type === "function" || cur.type === "function_expression") {
78
+ const carrier = cur.parent; // variable_declarator | public_field_definition
79
+ return line(carrier ?? cur);
80
+ }
81
+ return line(cur);
82
+ }
83
+ // fonction anonyme (callback inline): on continue de remonter
84
+ }
85
+ cur = cur.parent;
86
+ }
87
+ return 0;
88
+ }
89
+ function emptyFacts(rel, src) {
90
+ return {
91
+ rel, loc: src.split("\n").length, exports: [], importSpecs: [], routes: [],
92
+ defs: [], namedImports: {}, rawCalls: [],
93
+ };
94
+ }
95
+ // ── TypeScript / JavaScript ──────────────────────────────────────────────────
96
+ const JS_DEF_KINDS = new Set(["function_declaration", "generator_function_declaration", "method_definition", "arrow_function", "function", "function_expression"]);
97
+ function jsDefName(n) {
98
+ if (n.type === "function_declaration" || n.type === "generator_function_declaration" || n.type === "method_definition") {
99
+ return n.childForFieldName("name");
100
+ }
101
+ // arrow/function expression: nommée seulement si assignée directement à un déclarateur
102
+ const p = n.parent;
103
+ if (p?.type === "variable_declarator")
104
+ return p.childForFieldName("name");
105
+ if (p?.type === "public_field_definition")
106
+ return p.childForFieldName("name");
107
+ return null;
108
+ }
109
+ function extractJs(rel, src, root) {
110
+ const facts = emptyFacts(rel, src);
111
+ // Imports (statiques + re-exports + require)
112
+ for (const imp of root.descendantsOfType("import_statement")) {
113
+ const source = imp.childForFieldName("source");
114
+ if (!source)
115
+ continue;
116
+ const spec = stripQuotes(source.text);
117
+ facts.importSpecs.push(spec);
118
+ for (const clause of imp.descendantsOfType("import_clause")) {
119
+ for (const child of clause.namedChildren) {
120
+ if (child.type === "identifier") {
121
+ facts.namedImports[child.text] = spec; // default import
122
+ }
123
+ else if (child.type === "namespace_import") {
124
+ const id = child.descendantsOfType("identifier")[0]; // import * as utils
125
+ if (id)
126
+ facts.namedImports[id.text] = spec;
127
+ }
128
+ else if (child.type === "named_imports") {
129
+ for (const isp of child.descendantsOfType("import_specifier")) {
130
+ // local = alias si présent, sinon name. On ignore les imports de type.
131
+ if (isp.children.some((c) => c.type === "type"))
132
+ continue;
133
+ const nameNode = isp.childForFieldName("name");
134
+ const aliasNode = isp.childForFieldName("alias");
135
+ const local = (aliasNode ?? nameNode)?.text;
136
+ const orig = nameNode?.text;
137
+ if (local && orig)
138
+ facts.namedImports[local] = local === orig ? spec : { spec, orig };
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ for (const exp of root.descendantsOfType("export_statement")) {
145
+ const source = exp.childForFieldName("source");
146
+ if (source)
147
+ facts.importSpecs.push(stripQuotes(source.text)); // re-export = arête
148
+ const decl = exp.childForFieldName("declaration");
149
+ const name = decl?.childForFieldName("name");
150
+ if (name)
151
+ facts.exports.push(name.text);
152
+ for (const spec of exp.descendantsOfType("export_specifier")) {
153
+ const n = (spec.childForFieldName("alias") ?? spec.childForFieldName("name"))?.text;
154
+ if (n)
155
+ facts.exports.push(n);
156
+ }
157
+ }
158
+ // Définitions
159
+ for (const fn of root.descendantsOfType(["function_declaration", "generator_function_declaration"])) {
160
+ const name = fn.childForFieldName("name");
161
+ if (name)
162
+ facts.defs.push({ name: name.text, line: line(fn) });
163
+ }
164
+ for (const decl of root.descendantsOfType("variable_declarator")) {
165
+ const value = decl.childForFieldName("value");
166
+ const name = decl.childForFieldName("name");
167
+ if (name?.type === "identifier" && value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression")) {
168
+ facts.defs.push({ name: name.text, line: line(decl) });
169
+ }
170
+ }
171
+ for (const m of root.descendantsOfType("method_definition")) {
172
+ const name = m.childForFieldName("name");
173
+ if (name && name.text !== "constructor")
174
+ facts.defs.push({ name: name.text, line: line(m) });
175
+ }
176
+ for (const f of root.descendantsOfType("public_field_definition")) {
177
+ const value = f.childForFieldName("value");
178
+ const name = f.childForFieldName("name");
179
+ if (name && value?.type === "arrow_function")
180
+ facts.defs.push({ name: name.text, line: line(f) });
181
+ }
182
+ for (const c of root.descendantsOfType("class_declaration")) {
183
+ const name = c.childForFieldName("name");
184
+ if (name)
185
+ facts.exports.push(name.text);
186
+ }
187
+ // Appels + routes
188
+ for (const call of root.descendantsOfType("call_expression")) {
189
+ const fn = call.childForFieldName("function");
190
+ if (!fn)
191
+ continue;
192
+ if (fn.type === "identifier") {
193
+ if (fn.text === "require") {
194
+ const arg = call.childForFieldName("arguments")?.namedChildren[0];
195
+ if (arg && (arg.type === "string" || arg.type === "template_string"))
196
+ facts.importSpecs.push(stripQuotes(arg.text));
197
+ continue;
198
+ }
199
+ facts.rawCalls.push({ name: fn.text, line: line(call), owner: enclosingDefLine(call, JS_DEF_KINDS, jsDefName) });
200
+ }
201
+ else if (fn.type === "member_expression") {
202
+ const obj = fn.childForFieldName("object");
203
+ const prop = fn.childForFieldName("property");
204
+ if (!obj || !prop)
205
+ continue;
206
+ // Route express-like: app.get("/x", ...)
207
+ if (obj.type === "identifier" && ROUTE_OBJECTS.has(obj.text) && HTTP_METHODS.has(prop.text)) {
208
+ const arg = call.childForFieldName("arguments")?.namedChildren[0];
209
+ if (arg && (arg.type === "string" || arg.type === "template_string")) {
210
+ facts.routes.push({ method: prop.text.toUpperCase(), path: stripQuotes(arg.text), file: rel, line: line(call), framework: "express-like" });
211
+ }
212
+ }
213
+ // Appel qualifié résoluble: ImportéOuClasse.method(...)
214
+ if (obj.type === "identifier") {
215
+ facts.rawCalls.push({ name: obj.text + ".", line: line(call), owner: enclosingDefLine(call, JS_DEF_KINDS, jsDefName) });
216
+ }
217
+ }
218
+ }
219
+ // Décorateurs Nest: @Get("/x")
220
+ for (const dec of root.descendantsOfType("decorator")) {
221
+ const call = dec.namedChildren[0];
222
+ if (call?.type !== "call_expression")
223
+ continue;
224
+ const fn = call.childForFieldName("function");
225
+ if (fn?.type === "identifier" && NEST_DECORATORS.has(fn.text)) {
226
+ const arg = call.childForFieldName("arguments")?.namedChildren[0];
227
+ facts.routes.push({
228
+ method: fn.text.toUpperCase(),
229
+ path: arg && (arg.type === "string") ? stripQuotes(arg.text) : "",
230
+ file: rel, line: line(dec), framework: "nest",
231
+ });
232
+ }
233
+ }
234
+ return facts;
235
+ }
236
+ // ── Python ───────────────────────────────────────────────────────────────────
237
+ const PY_DEF_KINDS = new Set(["function_definition"]);
238
+ const pyDefName = (n) => n.childForFieldName("name");
239
+ /** "from .sub import x" → spec JS-style "./sub" résoluble par le résolveur existant. */
240
+ function pyRelativeSpec(dots, mod) {
241
+ const up = dots <= 1 ? "./" : "../".repeat(dots - 1);
242
+ return up + mod.replace(/\./g, "/");
243
+ }
244
+ function extractPy(rel, src, root) {
245
+ const facts = emptyFacts(rel, src);
246
+ for (const imp of root.descendantsOfType("import_from_statement")) {
247
+ // forme: from <relative_import|dotted_name> import a, b as c | from . import service
248
+ const modNode = imp.childForFieldName("module_name");
249
+ let spec = "";
250
+ let relative = false;
251
+ if (modNode) {
252
+ if (modNode.type === "relative_import") {
253
+ relative = true;
254
+ const dots = (modNode.text.match(/^\.+/) ?? ["."])[0].length;
255
+ const mod = modNode.text.slice(dots);
256
+ spec = pyRelativeSpec(dots, mod);
257
+ }
258
+ else {
259
+ spec = modNode.text;
260
+ }
261
+ }
262
+ const names = [];
263
+ for (const child of imp.namedChildren) {
264
+ if (child === modNode)
265
+ continue;
266
+ if (child.type === "dotted_name")
267
+ names.push(child.text);
268
+ if (child.type === "aliased_import") {
269
+ const alias = child.childForFieldName("alias");
270
+ if (alias)
271
+ names.push(alias.text);
272
+ }
273
+ }
274
+ if (relative && !spec.replace(/^\.\/|\.\.\//g, "")) {
275
+ // "from . import service" → chaque nom est un module frère
276
+ for (const n of names) {
277
+ const s = pyRelativeSpec(1, n);
278
+ facts.importSpecs.push(s);
279
+ facts.namedImports[n] = s;
280
+ }
281
+ }
282
+ else if (spec) {
283
+ facts.importSpecs.push(spec);
284
+ for (const n of names)
285
+ facts.namedImports[n] = spec;
286
+ }
287
+ }
288
+ for (const imp of root.descendantsOfType("import_statement")) {
289
+ for (const d of imp.descendantsOfType("dotted_name"))
290
+ facts.importSpecs.push(d.text);
291
+ }
292
+ for (const fn of root.descendantsOfType("function_definition")) {
293
+ const name = fn.childForFieldName("name");
294
+ if (name) {
295
+ facts.defs.push({ name: name.text, line: line(fn) });
296
+ facts.exports.push(name.text);
297
+ }
298
+ }
299
+ for (const cls of root.descendantsOfType("class_definition")) {
300
+ const name = cls.childForFieldName("name");
301
+ if (name)
302
+ facts.exports.push(name.text);
303
+ }
304
+ for (const call of root.descendantsOfType("call")) {
305
+ const fn = call.childForFieldName("function");
306
+ if (!fn)
307
+ continue;
308
+ if (fn.type === "identifier") {
309
+ facts.rawCalls.push({ name: fn.text, line: line(call), owner: enclosingDefLine(call, PY_DEF_KINDS, pyDefName) });
310
+ }
311
+ else if (fn.type === "attribute") {
312
+ const obj = fn.childForFieldName("object");
313
+ const attr = fn.childForFieldName("attribute");
314
+ if (obj?.type === "identifier" && attr) {
315
+ // Route FastAPI/Flask: app.get("/x") en position de décorateur
316
+ if (call.parent?.type === "decorator" && (HTTP_METHODS.has(attr.text) || attr.text === "route")) {
317
+ const arg = call.childForFieldName("arguments")?.namedChildren[0];
318
+ if (arg?.type === "string") {
319
+ facts.routes.push({
320
+ method: attr.text === "route" ? "*" : attr.text.toUpperCase(),
321
+ path: stripQuotes(arg.text), file: rel, line: line(call), framework: "python",
322
+ });
323
+ }
324
+ }
325
+ facts.rawCalls.push({ name: obj.text + ".", line: line(call), owner: enclosingDefLine(call, PY_DEF_KINDS, pyDefName) });
326
+ }
327
+ }
328
+ }
329
+ return facts;
330
+ }
331
+ // ── Java ─────────────────────────────────────────────────────────────────────
332
+ const JAVA_DEF_KINDS = new Set(["method_declaration", "constructor_declaration"]);
333
+ const SPRING_MAPPINGS = {
334
+ GetMapping: "GET", PostMapping: "POST", PutMapping: "PUT",
335
+ DeleteMapping: "DELETE", PatchMapping: "PATCH", RequestMapping: "*",
336
+ };
337
+ function extractJava(rel, src, root) {
338
+ const facts = emptyFacts(rel, src);
339
+ for (const imp of root.descendantsOfType("import_declaration")) {
340
+ const id = imp.descendantsOfType("scoped_identifier")[0] ?? imp.namedChildren[0];
341
+ if (id) {
342
+ const full = id.text;
343
+ const cls = full.split(".").pop();
344
+ facts.importSpecs.push(full);
345
+ if (cls && /^[A-Z]/.test(cls))
346
+ facts.namedImports[cls] = full;
347
+ }
348
+ }
349
+ for (const m of root.descendantsOfType(["method_declaration", "constructor_declaration"])) {
350
+ const name = m.childForFieldName("name");
351
+ if (name) {
352
+ facts.defs.push({ name: name.text, line: line(m) });
353
+ facts.exports.push(name.text);
354
+ }
355
+ for (const ann of m.descendantsOfType(["annotation", "marker_annotation"])) {
356
+ const an = ann.childForFieldName("name")?.text;
357
+ if (an && SPRING_MAPPINGS[an]) {
358
+ const arg = ann.descendantsOfType("string_literal")[0];
359
+ facts.routes.push({ method: SPRING_MAPPINGS[an], path: arg ? stripQuotes(arg.text) : "", file: rel, line: line(ann), framework: "spring" });
360
+ }
361
+ }
362
+ }
363
+ for (const c of root.descendantsOfType(["class_declaration", "interface_declaration", "enum_declaration"])) {
364
+ const name = c.childForFieldName("name");
365
+ if (name)
366
+ facts.exports.push(name.text);
367
+ }
368
+ for (const call of root.descendantsOfType("method_invocation")) {
369
+ const name = call.childForFieldName("name");
370
+ const obj = call.childForFieldName("object");
371
+ const owner = enclosingDefLine(call, JAVA_DEF_KINDS, (n) => n.childForFieldName("name"));
372
+ if (obj && /^[A-Z]/.test(obj.text))
373
+ facts.rawCalls.push({ name: obj.text + ".", line: line(call), owner });
374
+ if (name)
375
+ facts.rawCalls.push({ name: name.text, line: line(call), owner });
376
+ }
377
+ return facts;
378
+ }
379
+ // ── Go ───────────────────────────────────────────────────────────────────────
380
+ const GO_DEF_KINDS = new Set(["function_declaration", "method_declaration"]);
381
+ function extractGo(rel, src, root) {
382
+ const facts = emptyFacts(rel, src);
383
+ for (const spec of root.descendantsOfType("import_spec")) {
384
+ const path = spec.childForFieldName("path");
385
+ if (path) {
386
+ const p = stripQuotes(path.text);
387
+ facts.importSpecs.push(p);
388
+ const pkg = p.split("/").pop();
389
+ if (pkg)
390
+ facts.namedImports[pkg] = p;
391
+ }
392
+ }
393
+ for (const fn of root.descendantsOfType(["function_declaration", "method_declaration"])) {
394
+ const name = fn.childForFieldName("name");
395
+ if (name) {
396
+ facts.defs.push({ name: name.text, line: line(fn) });
397
+ if (/^[A-Z]/.test(name.text))
398
+ facts.exports.push(name.text);
399
+ }
400
+ }
401
+ for (const ts of root.descendantsOfType("type_spec")) {
402
+ const name = ts.childForFieldName("name");
403
+ if (name && /^[A-Z]/.test(name.text))
404
+ facts.exports.push(name.text);
405
+ }
406
+ for (const call of root.descendantsOfType("call_expression")) {
407
+ const fn = call.childForFieldName("function");
408
+ const owner = enclosingDefLine(call, GO_DEF_KINDS, (n) => n.childForFieldName("name"));
409
+ if (!fn)
410
+ continue;
411
+ if (fn.type === "identifier") {
412
+ facts.rawCalls.push({ name: fn.text, line: line(call), owner });
413
+ }
414
+ else if (fn.type === "selector_expression") {
415
+ const operand = fn.childForFieldName("operand");
416
+ if (operand?.type === "identifier")
417
+ facts.rawCalls.push({ name: operand.text + ".", line: line(call), owner });
418
+ }
419
+ }
420
+ return facts;
421
+ }
422
+ // ── Rust ─────────────────────────────────────────────────────────────────────
423
+ const RUST_DEF_KINDS = new Set(["function_item"]);
424
+ function extractRust(rel, src, root) {
425
+ const facts = emptyFacts(rel, src);
426
+ for (const use of root.descendantsOfType("use_declaration")) {
427
+ const full = use.text.replace(/^use\s+/, "").replace(/;$/, "").trim();
428
+ const parts = full.split("::");
429
+ if (parts.length >= 2) {
430
+ const name = parts[parts.length - 1].replace(/[{}\s].*$/, "");
431
+ const path = parts.slice(0, -1).join("/").replace(/^crate\//, "src/").replace(/^crate$/, "src");
432
+ facts.importSpecs.push(path);
433
+ if (/^[a-z_]\w*$/i.test(name))
434
+ facts.namedImports[name] = path;
435
+ }
436
+ }
437
+ for (const fn of root.descendantsOfType("function_item")) {
438
+ const name = fn.childForFieldName("name");
439
+ if (name) {
440
+ facts.defs.push({ name: name.text, line: line(fn) });
441
+ if (fn.children.some((c) => c.type === "visibility_modifier"))
442
+ facts.exports.push(name.text);
443
+ }
444
+ }
445
+ for (const s of root.descendantsOfType(["struct_item", "enum_item", "trait_item"])) {
446
+ const name = s.childForFieldName("name");
447
+ if (name)
448
+ facts.exports.push(name.text);
449
+ }
450
+ for (const call of root.descendantsOfType("call_expression")) {
451
+ const fn = call.childForFieldName("function");
452
+ const owner = enclosingDefLine(call, RUST_DEF_KINDS, (n) => n.childForFieldName("name"));
453
+ if (!fn)
454
+ continue;
455
+ if (fn.type === "identifier") {
456
+ facts.rawCalls.push({ name: fn.text, line: line(call), owner });
457
+ }
458
+ else if (fn.type === "scoped_identifier") {
459
+ const name = fn.descendantsOfType("identifier").pop();
460
+ if (name)
461
+ facts.rawCalls.push({ name: name.text, line: line(call), owner });
462
+ }
463
+ else if (fn.type === "field_expression") {
464
+ const field = fn.childForFieldName("field");
465
+ if (field)
466
+ facts.rawCalls.push({ name: field.text, line: line(call), owner });
467
+ }
468
+ }
469
+ return facts;
470
+ }
471
+ // ── C# ───────────────────────────────────────────────────────────────────────
472
+ const CS_DEF_KINDS = new Set(["method_declaration", "constructor_declaration"]);
473
+ const CS_HTTP_ATTRS = {
474
+ HttpGet: "GET", HttpPost: "POST", HttpPut: "PUT", HttpDelete: "DELETE", HttpPatch: "PATCH", Route: "*",
475
+ };
476
+ function extractCSharp(rel, src, root) {
477
+ const facts = emptyFacts(rel, src);
478
+ for (const u of root.descendantsOfType("using_directive")) {
479
+ const name = u.descendantsOfType("qualified_name")[0] ?? u.descendantsOfType("identifier")[0];
480
+ if (name)
481
+ facts.importSpecs.push(name.text);
482
+ }
483
+ for (const m of root.descendantsOfType(["method_declaration", "constructor_declaration"])) {
484
+ const name = m.childForFieldName("name");
485
+ if (name) {
486
+ facts.defs.push({ name: name.text, line: line(m) });
487
+ facts.exports.push(name.text);
488
+ }
489
+ for (const attr of m.descendantsOfType("attribute")) {
490
+ const an = attr.childForFieldName("name")?.text;
491
+ if (an && CS_HTTP_ATTRS[an]) {
492
+ const arg = attr.descendantsOfType("string_literal")[0];
493
+ facts.routes.push({ method: CS_HTTP_ATTRS[an], path: arg ? stripQuotes(arg.text) : "", file: rel, line: line(attr), framework: "aspnet" });
494
+ }
495
+ }
496
+ }
497
+ for (const c of root.descendantsOfType(["class_declaration", "interface_declaration", "record_declaration"])) {
498
+ const name = c.childForFieldName("name");
499
+ if (name)
500
+ facts.exports.push(name.text);
501
+ }
502
+ for (const call of root.descendantsOfType("invocation_expression")) {
503
+ const fn = call.childForFieldName("function");
504
+ const owner = enclosingDefLine(call, CS_DEF_KINDS, (n) => n.childForFieldName("name"));
505
+ if (!fn)
506
+ continue;
507
+ if (fn.type === "identifier") {
508
+ facts.rawCalls.push({ name: fn.text, line: line(call), owner });
509
+ }
510
+ else if (fn.type === "member_access_expression") {
511
+ const obj = fn.childForFieldName("expression");
512
+ const name = fn.childForFieldName("name");
513
+ if (obj?.type === "identifier" && /^[A-Z]/.test(obj.text))
514
+ facts.rawCalls.push({ name: obj.text + ".", line: line(call), owner });
515
+ if (name)
516
+ facts.rawCalls.push({ name: name.text, line: line(call), owner });
517
+ }
518
+ }
519
+ return facts;
520
+ }
521
+ // ── Point d'entrée ───────────────────────────────────────────────────────────
522
+ const MAX_DEFS = 40;
523
+ const MAX_CALLS = 1000;
524
+ /** Extraction AST. Retourne null si tree-sitter indisponible pour ce fichier → fallback regex. */
525
+ export async function extractFileAst(rel, src, ext) {
526
+ const ctx = await getParser(ext);
527
+ if (!ctx)
528
+ return null;
529
+ let tree = null;
530
+ try {
531
+ ctx.parser.setLanguage(ctx.lang);
532
+ tree = ctx.parser.parse(src);
533
+ const root = tree.rootNode;
534
+ let facts;
535
+ switch (ext) {
536
+ case ".py":
537
+ facts = extractPy(rel, src, root);
538
+ break;
539
+ case ".java":
540
+ facts = extractJava(rel, src, root);
541
+ break;
542
+ case ".go":
543
+ facts = extractGo(rel, src, root);
544
+ break;
545
+ case ".rs":
546
+ facts = extractRust(rel, src, root);
547
+ break;
548
+ case ".cs":
549
+ facts = extractCSharp(rel, src, root);
550
+ break;
551
+ default:
552
+ facts = extractJs(rel, src, root);
553
+ break; // ts/tsx/js/jsx
554
+ }
555
+ // Bornes + dédupe defs (première occurrence par nom), tri par ligne — comme la voie regex.
556
+ const seen = new Set();
557
+ facts.defs = facts.defs
558
+ .sort((a, b) => a.line - b.line)
559
+ .filter((d) => (seen.has(d.name) ? false : (seen.add(d.name), true)))
560
+ .slice(0, MAX_DEFS);
561
+ facts.rawCalls = facts.rawCalls.slice(0, MAX_CALLS).sort((a, b) => a.line - b.line);
562
+ facts.routes = facts.routes.slice(0, 200);
563
+ facts.exports = [...new Set(facts.exports)];
564
+ return facts;
565
+ }
566
+ catch {
567
+ return null; // fichier illisible par l'AST → regex
568
+ }
569
+ finally {
570
+ tree?.delete(); // mémoire WASM: indispensable sur des milliers de fichiers
571
+ }
572
+ }
573
+ /** Routes par convention de fichier Next.js — indépendant du parseur, partagé. */
574
+ export function nextRouteFor(rel) {
575
+ const nm = rel.replace(/\\/g, "/").match(/(^|\/)(pages|app)\/(.+?)\/?(route|page|index)?\.(ts|tsx|js|jsx)$/);
576
+ if (!nm)
577
+ return null;
578
+ const urlPath = "/" + nm[3].replace(/\[(\w+)\]/g, ":$1").replace(/\/index$/, "");
579
+ return { method: "*", path: urlPath, file: rel, line: 1, framework: "next" };
580
+ }
@@ -2,12 +2,13 @@ import { readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { join, relative, extname, dirname } from "node:path";
3
3
  import { execSync } from "node:child_process";
4
4
  import { repoFullName } from "./store.js";
5
+ import { extractFileAst, nextRouteFor } from "./ast.js";
5
6
  // ─────────────────────────────────────────────────────────────────────────────
6
7
  // Indexeur structurel — 100% local, 0 token, déterministe (même input → même
7
8
  // output). Heuristiques regex pragmatiques pour TS/JS/Python ; le point
8
9
  // d'extension propre pour passer à tree-sitter plus tard est extractFile().
9
10
  // ─────────────────────────────────────────────────────────────────────────────
10
- const CODE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"]);
11
+ const CODE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".java", ".go", ".rs", ".cs"]);
11
12
  const IGNORE_DIRS = new Set([
12
13
  "node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "coverage",
13
14
  "vendor", "__pycache__", ".venv", "venv", ".kurtel", ".claude", ".idea", ".vscode",
@@ -40,7 +41,6 @@ const PY_ROUTE_PATTERNS = [
40
41
  path: (m) => m[2],
41
42
  },
42
43
  ];
43
- const NEXT_ROUTE_FILE = /(^|\/)(pages|app)\/(.+?)\/?(route|page|index)?\.(ts|tsx|js|jsx)$/;
44
44
  const JS_KEYWORDS = new Set([
45
45
  "if", "for", "while", "switch", "catch", "return", "function", "new", "typeof",
46
46
  "await", "async", "import", "export", "require", "console", "constructor",
@@ -154,12 +154,6 @@ function extractFile(root, rel, src) {
154
154
  });
155
155
  }
156
156
  }
157
- // Routes par convention de fichier (Next.js app/pages router).
158
- const nm = rel.replace(/\\/g, "/").match(NEXT_ROUTE_FILE);
159
- if (nm) {
160
- const urlPath = "/" + nm[3].replace(/\[(\w+)\]/g, ":$1").replace(/\/index$/, "");
161
- facts.routes.push({ method: "*", path: urlPath, file: rel, line: 1, framework: "next" });
162
- }
163
157
  return facts;
164
158
  }
165
159
  // ── Résolution d'imports internes (TS/JS relatifs + Python par module path) ──
@@ -221,22 +215,30 @@ function buildSymbols(all, resolve) {
221
215
  const name = qualified ? raw.slice(0, -1) : raw;
222
216
  if (!qualified && locals.has(name))
223
217
  return `${rel}::${name}`;
224
- const spec = f.namedImports[name];
225
- if (spec) {
218
+ const entry = f.namedImports[name];
219
+ if (entry) {
220
+ const spec = typeof entry === "string" ? entry : entry.spec;
221
+ const orig = typeof entry === "string" ? name : entry.orig; // alias → nom exporté côté cible
226
222
  const file = resolve(rel, spec);
227
- if (file && file !== rel && (defNames.get(file)?.has(name) || exportNames.get(file)?.has(name))) {
228
- return `${file}::${name}`;
223
+ if (file && file !== rel && (defNames.get(file)?.has(orig) || exportNames.get(file)?.has(orig))) {
224
+ return `${file}::${orig}`;
229
225
  }
230
226
  }
231
227
  return null;
232
228
  };
229
+ const byLine = new Map(symbols.map((s) => [s.line, s]));
233
230
  for (const call of f.rawCalls) {
234
231
  let caller = topLevel;
235
- for (const s of symbols) {
236
- if (s.line <= call.line)
237
- caller = s;
238
- else
239
- break;
232
+ if (call.owner !== undefined) {
233
+ caller = call.owner === 0 ? topLevel : (byLine.get(call.owner) ?? topLevel);
234
+ }
235
+ else {
236
+ for (const s of symbols) {
237
+ if (s.line <= call.line)
238
+ caller = s;
239
+ else
240
+ break;
241
+ }
240
242
  }
241
243
  if (caller.name === call.name || caller.calls.length >= MAX_CALLS_PER_SYMBOL)
242
244
  continue;
@@ -281,13 +283,24 @@ function* walk(dir, root) {
281
283
  }
282
284
  }
283
285
  // ── Construction de l'index ─────────────────────────────────────────────────
284
- export function buildIndex(root, onProgress) {
286
+ export async function buildIndex(root, onProgress) {
285
287
  const files = new Map();
286
288
  let n = 0;
289
+ let regexFallbacks = 0;
287
290
  for (const rel of walk(root, root)) {
288
291
  try {
289
292
  const src = readFileSync(join(root, rel), "utf8");
290
- files.set(rel, extractFile(root, rel, src));
293
+ const ast = await extractFileAst(rel, src, extname(rel));
294
+ if (ast) {
295
+ files.set(rel, ast);
296
+ }
297
+ else {
298
+ files.set(rel, extractFile(root, rel, src));
299
+ regexFallbacks++;
300
+ }
301
+ const next = nextRouteFor(rel);
302
+ if (next)
303
+ files.get(rel).routes.push(next);
291
304
  if (onProgress && ++n % 50 === 0)
292
305
  onProgress(n);
293
306
  }
@@ -338,6 +351,7 @@ export function buildIndex(root, onProgress) {
338
351
  catch { /* pas un repo git */ }
339
352
  return {
340
353
  version: 1,
354
+ parser: { engine: regexFallbacks === files.size ? "regex" : "tree-sitter", regex_fallbacks: regexFallbacks },
341
355
  repo: repoFullName(root),
342
356
  commit,
343
357
  generated_at: new Date().toISOString(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurtel/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Launch self-improving coding agents in the cloud — the Kurtel CLI.",
5
5
  "type": "module",
6
6
  "bin": {