@abdulmunimjemal/codescope 0.1.0
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 +153 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1321 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +337 -0
- package/dist/index.js +1136 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { mkdirSync } from "fs";
|
|
5
|
+
import { resolve as resolve2 } from "path";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
|
|
8
|
+
// src/format.ts
|
|
9
|
+
function symbolLine(s) {
|
|
10
|
+
const loc = `${s.file}:${s.startRow + 1}`;
|
|
11
|
+
const container = s.container ? `${s.container}.` : "";
|
|
12
|
+
const exp = s.exported ? "export " : "";
|
|
13
|
+
const sig = s.signature ? ` \xB7 ${s.signature}` : "";
|
|
14
|
+
return `${s.kind} ${exp}${container}${s.name} \u2014 ${loc}${sig}`;
|
|
15
|
+
}
|
|
16
|
+
function formatSymbols(rows) {
|
|
17
|
+
if (rows.length === 0) return "No matching symbols.";
|
|
18
|
+
return rows.map(symbolLine).join("\n");
|
|
19
|
+
}
|
|
20
|
+
function formatRefs(rows) {
|
|
21
|
+
if (rows.length === 0) return "No references.";
|
|
22
|
+
return rows.map((r) => {
|
|
23
|
+
const where = r.fromSymbol ? `${r.fromSymbol}` : "(top level)";
|
|
24
|
+
return `${where} \u2192 ${r.name} [${r.kind}] \u2014 ${r.file}:${r.startRow + 1}`;
|
|
25
|
+
}).join("\n");
|
|
26
|
+
}
|
|
27
|
+
function formatNeighborhood(n) {
|
|
28
|
+
const lines = [`neighbourhood of ${n.root}:`];
|
|
29
|
+
lines.push("", "definitions:");
|
|
30
|
+
if (n.nodes.length === 0) lines.push(" (none indexed)");
|
|
31
|
+
else for (const s of n.nodes) lines.push(` ${symbolLine(s)}`);
|
|
32
|
+
if (n.edges.length > 0) {
|
|
33
|
+
lines.push("", "call edges:");
|
|
34
|
+
for (const e of n.edges) lines.push(` ${e.from} \u2192 ${e.to}`);
|
|
35
|
+
}
|
|
36
|
+
if (n.unresolved.length > 0) {
|
|
37
|
+
lines.push("", `unresolved (referenced, not defined in index): ${n.unresolved.join(", ")}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
function formatStats(s) {
|
|
42
|
+
const kinds = Object.entries(s.byKind).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
43
|
+
const langs = Object.entries(s.byLang).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}=${n}`).join(" ");
|
|
44
|
+
return [
|
|
45
|
+
`files: ${s.files}`,
|
|
46
|
+
`symbols: ${s.symbols} (${kinds || "none"})`,
|
|
47
|
+
`refs: ${s.refs}`,
|
|
48
|
+
`langs: ${langs || "none"}`
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/indexer.ts
|
|
53
|
+
import { createHash } from "crypto";
|
|
54
|
+
import { readFileSync } from "fs";
|
|
55
|
+
import { readFile, stat } from "fs/promises";
|
|
56
|
+
import { relative, resolve, sep } from "path";
|
|
57
|
+
import ignore from "ignore";
|
|
58
|
+
import { glob } from "tinyglobby";
|
|
59
|
+
|
|
60
|
+
// src/languages.ts
|
|
61
|
+
var JS_CALLS = [
|
|
62
|
+
{ type: "call_expression", fnField: "function", memberTypes: ["member_expression"], memberField: "property" }
|
|
63
|
+
];
|
|
64
|
+
var typescript = {
|
|
65
|
+
id: "typescript",
|
|
66
|
+
wasm: "typescript",
|
|
67
|
+
defs: {
|
|
68
|
+
function_declaration: { kind: "function" },
|
|
69
|
+
generator_function_declaration: { kind: "function" },
|
|
70
|
+
function_signature: { kind: "function" },
|
|
71
|
+
method_definition: { kind: "method" },
|
|
72
|
+
method_signature: { kind: "method" },
|
|
73
|
+
class_declaration: { kind: "class" },
|
|
74
|
+
abstract_class_declaration: { kind: "class" },
|
|
75
|
+
interface_declaration: { kind: "interface" },
|
|
76
|
+
type_alias_declaration: { kind: "type" },
|
|
77
|
+
enum_declaration: { kind: "enum" }
|
|
78
|
+
},
|
|
79
|
+
functionBindings: /* @__PURE__ */ new Set(["variable_declarator", "public_field_definition"]),
|
|
80
|
+
nestedFunctionsAreMethods: false,
|
|
81
|
+
callRules: JS_CALLS,
|
|
82
|
+
importRules: [{ type: "import_statement", field: "source" }],
|
|
83
|
+
exportTypes: /* @__PURE__ */ new Set(["export_statement"])
|
|
84
|
+
};
|
|
85
|
+
var tsx = { ...typescript, id: "tsx", wasm: "tsx" };
|
|
86
|
+
var javascript = {
|
|
87
|
+
id: "javascript",
|
|
88
|
+
wasm: "javascript",
|
|
89
|
+
defs: {
|
|
90
|
+
function_declaration: { kind: "function" },
|
|
91
|
+
generator_function_declaration: { kind: "function" },
|
|
92
|
+
method_definition: { kind: "method" },
|
|
93
|
+
class_declaration: { kind: "class" }
|
|
94
|
+
},
|
|
95
|
+
functionBindings: /* @__PURE__ */ new Set(["variable_declarator", "field_definition"]),
|
|
96
|
+
nestedFunctionsAreMethods: false,
|
|
97
|
+
callRules: JS_CALLS,
|
|
98
|
+
importRules: [{ type: "import_statement", field: "source" }],
|
|
99
|
+
exportTypes: /* @__PURE__ */ new Set(["export_statement"])
|
|
100
|
+
};
|
|
101
|
+
var python = {
|
|
102
|
+
id: "python",
|
|
103
|
+
wasm: "python",
|
|
104
|
+
defs: {
|
|
105
|
+
function_definition: { kind: "function" },
|
|
106
|
+
class_definition: { kind: "class" }
|
|
107
|
+
},
|
|
108
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
109
|
+
nestedFunctionsAreMethods: false,
|
|
110
|
+
callRules: [{ type: "call", fnField: "function", memberTypes: ["attribute"], memberField: "attribute" }],
|
|
111
|
+
importRules: [
|
|
112
|
+
{ type: "import_statement", childTypes: ["dotted_name", "aliased_import"] },
|
|
113
|
+
{ type: "import_from_statement", field: "module_name" }
|
|
114
|
+
],
|
|
115
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
116
|
+
};
|
|
117
|
+
var go = {
|
|
118
|
+
id: "go",
|
|
119
|
+
wasm: "go",
|
|
120
|
+
defs: {
|
|
121
|
+
function_declaration: { kind: "function" },
|
|
122
|
+
method_declaration: { kind: "method" },
|
|
123
|
+
type_spec: { kind: "class" }
|
|
124
|
+
},
|
|
125
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
126
|
+
nestedFunctionsAreMethods: false,
|
|
127
|
+
callRules: [
|
|
128
|
+
{ type: "call_expression", fnField: "function", memberTypes: ["selector_expression"], memberField: "field" }
|
|
129
|
+
],
|
|
130
|
+
importRules: [{ type: "import_spec", field: "path" }],
|
|
131
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
132
|
+
};
|
|
133
|
+
var rust = {
|
|
134
|
+
id: "rust",
|
|
135
|
+
wasm: "rust",
|
|
136
|
+
defs: {
|
|
137
|
+
function_item: { kind: "function" },
|
|
138
|
+
struct_item: { kind: "class" },
|
|
139
|
+
union_item: { kind: "class" },
|
|
140
|
+
enum_item: { kind: "enum" },
|
|
141
|
+
trait_item: { kind: "interface" },
|
|
142
|
+
type_item: { kind: "type" }
|
|
143
|
+
},
|
|
144
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
145
|
+
nestedFunctionsAreMethods: false,
|
|
146
|
+
callRules: [
|
|
147
|
+
{
|
|
148
|
+
type: "call_expression",
|
|
149
|
+
fnField: "function",
|
|
150
|
+
memberTypes: ["field_expression"],
|
|
151
|
+
memberField: "field",
|
|
152
|
+
scopedTypes: ["scoped_identifier"],
|
|
153
|
+
scopedField: "name"
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
importRules: [{ type: "use_declaration", field: "argument" }],
|
|
157
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
158
|
+
};
|
|
159
|
+
var java = {
|
|
160
|
+
id: "java",
|
|
161
|
+
wasm: "java",
|
|
162
|
+
defs: {
|
|
163
|
+
class_declaration: { kind: "class" },
|
|
164
|
+
interface_declaration: { kind: "interface" },
|
|
165
|
+
enum_declaration: { kind: "enum" },
|
|
166
|
+
record_declaration: { kind: "class" },
|
|
167
|
+
method_declaration: { kind: "method" },
|
|
168
|
+
constructor_declaration: { kind: "method" }
|
|
169
|
+
},
|
|
170
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
171
|
+
nestedFunctionsAreMethods: false,
|
|
172
|
+
callRules: [{ type: "method_invocation", nameField: "name", receiverField: "object" }],
|
|
173
|
+
importRules: [{ type: "import_declaration", childTypes: ["scoped_identifier", "identifier"] }],
|
|
174
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
175
|
+
};
|
|
176
|
+
var ruby = {
|
|
177
|
+
id: "ruby",
|
|
178
|
+
wasm: "ruby",
|
|
179
|
+
defs: {
|
|
180
|
+
method: { kind: "method" },
|
|
181
|
+
singleton_method: { kind: "method" },
|
|
182
|
+
class: { kind: "class" },
|
|
183
|
+
module: { kind: "class" }
|
|
184
|
+
},
|
|
185
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
186
|
+
nestedFunctionsAreMethods: false,
|
|
187
|
+
callRules: [{ type: "call", nameField: "method", receiverField: "receiver" }],
|
|
188
|
+
importRules: [],
|
|
189
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
190
|
+
};
|
|
191
|
+
var c = {
|
|
192
|
+
id: "c",
|
|
193
|
+
wasm: "c",
|
|
194
|
+
defs: {
|
|
195
|
+
function_definition: { kind: "function", name: "c_declarator" },
|
|
196
|
+
struct_specifier: { kind: "class" },
|
|
197
|
+
union_specifier: { kind: "class" },
|
|
198
|
+
enum_specifier: { kind: "enum" }
|
|
199
|
+
},
|
|
200
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
201
|
+
nestedFunctionsAreMethods: false,
|
|
202
|
+
callRules: [
|
|
203
|
+
{ type: "call_expression", fnField: "function", memberTypes: ["field_expression"], memberField: "field" }
|
|
204
|
+
],
|
|
205
|
+
importRules: [{ type: "preproc_include", field: "path" }],
|
|
206
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
207
|
+
};
|
|
208
|
+
var cpp = {
|
|
209
|
+
id: "cpp",
|
|
210
|
+
wasm: "cpp",
|
|
211
|
+
defs: {
|
|
212
|
+
function_definition: { kind: "function", name: "c_declarator" },
|
|
213
|
+
class_specifier: { kind: "class" },
|
|
214
|
+
struct_specifier: { kind: "class" },
|
|
215
|
+
enum_specifier: { kind: "enum" }
|
|
216
|
+
},
|
|
217
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
218
|
+
nestedFunctionsAreMethods: true,
|
|
219
|
+
callRules: [
|
|
220
|
+
{ type: "call_expression", fnField: "function", memberTypes: ["field_expression"], memberField: "field" }
|
|
221
|
+
],
|
|
222
|
+
importRules: [{ type: "preproc_include", field: "path" }],
|
|
223
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
224
|
+
};
|
|
225
|
+
var csharp = {
|
|
226
|
+
id: "csharp",
|
|
227
|
+
wasm: "c_sharp",
|
|
228
|
+
defs: {
|
|
229
|
+
class_declaration: { kind: "class" },
|
|
230
|
+
struct_declaration: { kind: "class" },
|
|
231
|
+
interface_declaration: { kind: "interface" },
|
|
232
|
+
enum_declaration: { kind: "enum" },
|
|
233
|
+
record_declaration: { kind: "class" },
|
|
234
|
+
method_declaration: { kind: "method" },
|
|
235
|
+
constructor_declaration: { kind: "method" }
|
|
236
|
+
},
|
|
237
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
238
|
+
nestedFunctionsAreMethods: false,
|
|
239
|
+
callRules: [
|
|
240
|
+
{
|
|
241
|
+
type: "invocation_expression",
|
|
242
|
+
fnField: "function",
|
|
243
|
+
memberTypes: ["member_access_expression"],
|
|
244
|
+
memberField: "name"
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
importRules: [{ type: "using_directive", childTypes: ["identifier", "qualified_name"] }],
|
|
248
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
249
|
+
};
|
|
250
|
+
var php = {
|
|
251
|
+
id: "php",
|
|
252
|
+
wasm: "php",
|
|
253
|
+
defs: {
|
|
254
|
+
function_definition: { kind: "function" },
|
|
255
|
+
method_declaration: { kind: "method" },
|
|
256
|
+
class_declaration: { kind: "class" },
|
|
257
|
+
interface_declaration: { kind: "interface" },
|
|
258
|
+
trait_declaration: { kind: "class" },
|
|
259
|
+
enum_declaration: { kind: "enum" }
|
|
260
|
+
},
|
|
261
|
+
functionBindings: /* @__PURE__ */ new Set(),
|
|
262
|
+
nestedFunctionsAreMethods: false,
|
|
263
|
+
callRules: [
|
|
264
|
+
{ type: "function_call_expression", fnField: "function" },
|
|
265
|
+
{ type: "member_call_expression", nameField: "name", forceKind: "method" },
|
|
266
|
+
{ type: "scoped_call_expression", nameField: "name", forceKind: "method" }
|
|
267
|
+
],
|
|
268
|
+
importRules: [{ type: "namespace_use_declaration", childTypes: ["namespace_use_clause"] }],
|
|
269
|
+
exportTypes: /* @__PURE__ */ new Set()
|
|
270
|
+
};
|
|
271
|
+
var LANGUAGES = {
|
|
272
|
+
typescript,
|
|
273
|
+
tsx,
|
|
274
|
+
javascript,
|
|
275
|
+
python,
|
|
276
|
+
go,
|
|
277
|
+
rust,
|
|
278
|
+
java,
|
|
279
|
+
ruby,
|
|
280
|
+
c,
|
|
281
|
+
cpp,
|
|
282
|
+
csharp,
|
|
283
|
+
php
|
|
284
|
+
};
|
|
285
|
+
var EXT_TO_LANG = {
|
|
286
|
+
".ts": "typescript",
|
|
287
|
+
".mts": "typescript",
|
|
288
|
+
".cts": "typescript",
|
|
289
|
+
".tsx": "tsx",
|
|
290
|
+
".js": "javascript",
|
|
291
|
+
".mjs": "javascript",
|
|
292
|
+
".cjs": "javascript",
|
|
293
|
+
".jsx": "javascript",
|
|
294
|
+
".py": "python",
|
|
295
|
+
".pyi": "python",
|
|
296
|
+
".go": "go",
|
|
297
|
+
".rs": "rust",
|
|
298
|
+
".java": "java",
|
|
299
|
+
".rb": "ruby",
|
|
300
|
+
".c": "c",
|
|
301
|
+
".h": "c",
|
|
302
|
+
".cc": "cpp",
|
|
303
|
+
".cpp": "cpp",
|
|
304
|
+
".cxx": "cpp",
|
|
305
|
+
".hpp": "cpp",
|
|
306
|
+
".hh": "cpp",
|
|
307
|
+
".cs": "csharp",
|
|
308
|
+
".php": "php"
|
|
309
|
+
};
|
|
310
|
+
var SUPPORTED_EXTENSIONS = Object.keys(EXT_TO_LANG);
|
|
311
|
+
function languageForPath(path) {
|
|
312
|
+
const dot = path.lastIndexOf(".");
|
|
313
|
+
if (dot === -1) return void 0;
|
|
314
|
+
const ext = path.slice(dot).toLowerCase();
|
|
315
|
+
const id = EXT_TO_LANG[ext];
|
|
316
|
+
return id ? LANGUAGES[id] : void 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/parser.ts
|
|
320
|
+
import { createRequire } from "module";
|
|
321
|
+
import Parser from "web-tree-sitter";
|
|
322
|
+
var require2 = createRequire(import.meta.url);
|
|
323
|
+
var initPromise = null;
|
|
324
|
+
var parserCache = /* @__PURE__ */ new Map();
|
|
325
|
+
function grammarPath(wasm) {
|
|
326
|
+
return require2.resolve(`tree-sitter-wasms/out/tree-sitter-${wasm}.wasm`);
|
|
327
|
+
}
|
|
328
|
+
async function ensureInit() {
|
|
329
|
+
if (!initPromise) initPromise = Parser.init();
|
|
330
|
+
await initPromise;
|
|
331
|
+
}
|
|
332
|
+
async function getParser(lang) {
|
|
333
|
+
const cached = parserCache.get(lang.id);
|
|
334
|
+
if (cached) return cached;
|
|
335
|
+
await ensureInit();
|
|
336
|
+
const language = await Parser.Language.load(grammarPath(lang.wasm));
|
|
337
|
+
const parser = new Parser();
|
|
338
|
+
parser.setLanguage(language);
|
|
339
|
+
parserCache.set(lang.id, parser);
|
|
340
|
+
return parser;
|
|
341
|
+
}
|
|
342
|
+
var FUNCTION_VALUE_TYPES = /* @__PURE__ */ new Set([
|
|
343
|
+
"arrow_function",
|
|
344
|
+
"function",
|
|
345
|
+
"function_expression",
|
|
346
|
+
"generator_function"
|
|
347
|
+
]);
|
|
348
|
+
async function parseSource(langId, source) {
|
|
349
|
+
const lang = LANGUAGES[langId];
|
|
350
|
+
if (!lang) throw new Error(`Unknown language: ${langId}`);
|
|
351
|
+
const parser = await getParser(lang);
|
|
352
|
+
const tree = parser.parse(source);
|
|
353
|
+
const symbols = [];
|
|
354
|
+
const refs = [];
|
|
355
|
+
if (tree?.rootNode) {
|
|
356
|
+
walk(tree.rootNode, lang, null, null, symbols, refs);
|
|
357
|
+
tree.delete();
|
|
358
|
+
}
|
|
359
|
+
return { lang: lang.id, symbols, refs };
|
|
360
|
+
}
|
|
361
|
+
function walk(node, lang, container, containerKind, symbols, refs) {
|
|
362
|
+
let childContainer = container;
|
|
363
|
+
let childContainerKind = containerKind;
|
|
364
|
+
const rule = classify(node, lang);
|
|
365
|
+
const call = lang.callRules.find((r) => r.type === node.type);
|
|
366
|
+
const imp = lang.importRules.find((r) => r.type === node.type);
|
|
367
|
+
if (rule) {
|
|
368
|
+
const sym = buildSymbol(node, rule, container, containerKind, lang);
|
|
369
|
+
if (sym) {
|
|
370
|
+
symbols.push(sym);
|
|
371
|
+
childContainer = sym.name;
|
|
372
|
+
childContainerKind = sym.kind;
|
|
373
|
+
}
|
|
374
|
+
} else if (call) {
|
|
375
|
+
const ref = extractCall(node, call, container);
|
|
376
|
+
if (ref) refs.push(ref);
|
|
377
|
+
} else if (imp) {
|
|
378
|
+
for (const ref of extractImports(node, imp, container)) refs.push(ref);
|
|
379
|
+
}
|
|
380
|
+
for (const child of node.namedChildren) {
|
|
381
|
+
walk(child, lang, childContainer, childContainerKind, symbols, refs);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function classify(node, lang) {
|
|
385
|
+
const direct = lang.defs[node.type];
|
|
386
|
+
if (direct) return direct;
|
|
387
|
+
if (lang.functionBindings.has(node.type)) {
|
|
388
|
+
const value = node.childForFieldName("value");
|
|
389
|
+
if (value && FUNCTION_VALUE_TYPES.has(value.type)) return { kind: "function" };
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
function buildSymbol(node, rule, container, containerKind, lang) {
|
|
394
|
+
const name = symbolName(node, rule.name ?? "field");
|
|
395
|
+
if (!name) return null;
|
|
396
|
+
let kind = rule.kind;
|
|
397
|
+
if (kind === "function" && lang.nestedFunctionsAreMethods && containerKind === "class") {
|
|
398
|
+
kind = "method";
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
name,
|
|
402
|
+
kind,
|
|
403
|
+
container,
|
|
404
|
+
exported: isExported(node, lang),
|
|
405
|
+
signature: signatureOf(node),
|
|
406
|
+
startRow: node.startPosition.row,
|
|
407
|
+
startCol: node.startPosition.column,
|
|
408
|
+
endRow: node.endPosition.row,
|
|
409
|
+
endCol: node.endPosition.column,
|
|
410
|
+
startByte: node.startIndex,
|
|
411
|
+
endByte: node.endIndex
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function symbolName(node, strategy) {
|
|
415
|
+
if (strategy === "c_declarator") return cDeclaratorName(node);
|
|
416
|
+
return node.childForFieldName("name")?.text ?? null;
|
|
417
|
+
}
|
|
418
|
+
function cDeclaratorName(node) {
|
|
419
|
+
let decl = node.childForFieldName("declarator");
|
|
420
|
+
for (let i = 0; decl && i < 10; i++) {
|
|
421
|
+
if (decl.type === "identifier" || decl.type === "field_identifier") return decl.text;
|
|
422
|
+
if (decl.type === "qualified_identifier" || decl.type === "destructor_name") {
|
|
423
|
+
return decl.childForFieldName("name")?.text ?? decl.text;
|
|
424
|
+
}
|
|
425
|
+
const inner = decl.childForFieldName("declarator");
|
|
426
|
+
if (!inner) break;
|
|
427
|
+
decl = inner;
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
function isExported(node, lang) {
|
|
432
|
+
if (lang.exportTypes.size === 0) return false;
|
|
433
|
+
let cur = node.parent;
|
|
434
|
+
for (let i = 0; cur && i < 2; i++) {
|
|
435
|
+
if (lang.exportTypes.has(cur.type)) return true;
|
|
436
|
+
cur = cur.parent;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
function signatureOf(node) {
|
|
441
|
+
const body = node.childForFieldName("body");
|
|
442
|
+
const raw = body ? node.text.slice(0, body.startIndex - node.startIndex) : node.text;
|
|
443
|
+
const text = raw.replace(/\s+/g, " ").trim();
|
|
444
|
+
if (!text) return null;
|
|
445
|
+
return text.length > 240 ? `${text.slice(0, 239)}\u2026` : text;
|
|
446
|
+
}
|
|
447
|
+
function extractCall(node, rule, container) {
|
|
448
|
+
let name = null;
|
|
449
|
+
let kind = "call";
|
|
450
|
+
if (rule.nameField) {
|
|
451
|
+
name = node.childForFieldName(rule.nameField)?.text ?? null;
|
|
452
|
+
if (rule.receiverField && node.childForFieldName(rule.receiverField)) kind = "method";
|
|
453
|
+
} else if (rule.fnField) {
|
|
454
|
+
const fn = node.childForFieldName(rule.fnField);
|
|
455
|
+
if (fn) {
|
|
456
|
+
if (rule.memberTypes?.includes(fn.type)) {
|
|
457
|
+
name = fn.childForFieldName(rule.memberField ?? "")?.text ?? null;
|
|
458
|
+
kind = "method";
|
|
459
|
+
} else if (rule.scopedTypes?.includes(fn.type)) {
|
|
460
|
+
name = fn.childForFieldName(rule.scopedField ?? "")?.text ?? null;
|
|
461
|
+
} else if (fn.type === "identifier" || fn.type === "name") {
|
|
462
|
+
name = fn.text;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (rule.forceKind) kind = rule.forceKind;
|
|
467
|
+
if (!name) return null;
|
|
468
|
+
return {
|
|
469
|
+
fromSymbol: container,
|
|
470
|
+
name,
|
|
471
|
+
kind,
|
|
472
|
+
startRow: node.startPosition.row,
|
|
473
|
+
startCol: node.startPosition.column
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function extractImports(node, rule, container) {
|
|
477
|
+
const out = [];
|
|
478
|
+
const add = (spec) => {
|
|
479
|
+
const name = spec ? unquote(spec) : "";
|
|
480
|
+
if (name) {
|
|
481
|
+
out.push({
|
|
482
|
+
fromSymbol: container,
|
|
483
|
+
name,
|
|
484
|
+
kind: "import",
|
|
485
|
+
startRow: node.startPosition.row,
|
|
486
|
+
startCol: node.startPosition.column
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
if (rule.field) {
|
|
491
|
+
add(node.childForFieldName(rule.field)?.text);
|
|
492
|
+
}
|
|
493
|
+
if (rule.childTypes) {
|
|
494
|
+
for (const child of node.namedChildren) {
|
|
495
|
+
if (!rule.childTypes.includes(child.type)) continue;
|
|
496
|
+
if (child.type === "aliased_import") add(child.childForFieldName("name")?.text);
|
|
497
|
+
else add(child.text);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
502
|
+
function unquote(text) {
|
|
503
|
+
const t = text.trim();
|
|
504
|
+
if (t.length >= 2) {
|
|
505
|
+
const first = t[0];
|
|
506
|
+
const last = t[t.length - 1];
|
|
507
|
+
if ((first === '"' || first === "'" || first === "`") && first === last) {
|
|
508
|
+
return t.slice(1, -1);
|
|
509
|
+
}
|
|
510
|
+
if (first === "<" && last === ">") return t.slice(1, -1);
|
|
511
|
+
}
|
|
512
|
+
return t;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/indexer.ts
|
|
516
|
+
var DEFAULT_IGNORES = [
|
|
517
|
+
"**/node_modules/**",
|
|
518
|
+
"**/.git/**",
|
|
519
|
+
"**/dist/**",
|
|
520
|
+
"**/build/**",
|
|
521
|
+
"**/coverage/**",
|
|
522
|
+
"**/.codescope/**",
|
|
523
|
+
"**/.next/**",
|
|
524
|
+
"**/out/**",
|
|
525
|
+
"**/target/**",
|
|
526
|
+
"**/.venv/**",
|
|
527
|
+
"**/venv/**",
|
|
528
|
+
"**/vendor/**",
|
|
529
|
+
"**/__pycache__/**"
|
|
530
|
+
];
|
|
531
|
+
var Indexer = class {
|
|
532
|
+
constructor(store, root) {
|
|
533
|
+
this.store = store;
|
|
534
|
+
this.root = resolve(root);
|
|
535
|
+
}
|
|
536
|
+
store;
|
|
537
|
+
root;
|
|
538
|
+
/** Index every supported, non-ignored file and prune deleted ones. */
|
|
539
|
+
async indexAll(opts = {}) {
|
|
540
|
+
const start = Date.now();
|
|
541
|
+
const result = {
|
|
542
|
+
indexed: 0,
|
|
543
|
+
skipped: 0,
|
|
544
|
+
removed: 0,
|
|
545
|
+
errors: [],
|
|
546
|
+
durationMs: 0
|
|
547
|
+
};
|
|
548
|
+
const files = await this.listSourceFiles(opts);
|
|
549
|
+
const present = /* @__PURE__ */ new Set();
|
|
550
|
+
for (const abs of files) {
|
|
551
|
+
present.add(this.rel(abs));
|
|
552
|
+
try {
|
|
553
|
+
const outcome = await this.indexFile(abs, start);
|
|
554
|
+
if (outcome === "indexed") result.indexed++;
|
|
555
|
+
else if (outcome === "skipped") result.skipped++;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
result.errors.push({ file: this.rel(abs), error: errorMessage(err) });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
for (const known of this.store.listFiles()) {
|
|
561
|
+
if (!present.has(known) && this.store.removeFile(known)) result.removed++;
|
|
562
|
+
}
|
|
563
|
+
result.durationMs = Date.now() - start;
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
/** Index a single file by absolute path. Cheap when the content is unchanged. */
|
|
567
|
+
async indexFile(abs, now = Date.now()) {
|
|
568
|
+
const lang = languageForPath(abs);
|
|
569
|
+
if (!lang) return "unsupported";
|
|
570
|
+
const rel = this.rel(abs);
|
|
571
|
+
const content = await readFile(abs, "utf8");
|
|
572
|
+
const hash = sha1(content);
|
|
573
|
+
if (this.store.getFileHash(rel) === hash) return "skipped";
|
|
574
|
+
const mtime = await fileMtime(abs, now);
|
|
575
|
+
const { symbols, refs } = await parseSource(lang.id, content);
|
|
576
|
+
this.store.replaceFile(
|
|
577
|
+
{ path: rel, lang: lang.id, hash, size: content.length, mtime },
|
|
578
|
+
symbols,
|
|
579
|
+
refs,
|
|
580
|
+
now
|
|
581
|
+
);
|
|
582
|
+
return "indexed";
|
|
583
|
+
}
|
|
584
|
+
/** Drop a file from the graph by absolute path. */
|
|
585
|
+
removeFile(abs) {
|
|
586
|
+
return this.store.removeFile(this.rel(abs));
|
|
587
|
+
}
|
|
588
|
+
/** Repo-relative, POSIX-separated path used as the stable graph key. */
|
|
589
|
+
rel(abs) {
|
|
590
|
+
return relative(this.root, resolve(abs)).split(sep).join("/");
|
|
591
|
+
}
|
|
592
|
+
async listSourceFiles(opts) {
|
|
593
|
+
const patterns = SUPPORTED_EXTENSIONS.map((ext) => `**/*${ext}`);
|
|
594
|
+
const files = await glob(patterns, {
|
|
595
|
+
cwd: this.root,
|
|
596
|
+
absolute: true,
|
|
597
|
+
ignore: [...DEFAULT_IGNORES, ...opts.ignore ?? []],
|
|
598
|
+
dot: false
|
|
599
|
+
});
|
|
600
|
+
if (opts.gitignore === false) return files;
|
|
601
|
+
const ig = this.loadGitignore();
|
|
602
|
+
if (!ig) return files;
|
|
603
|
+
return files.filter((f) => {
|
|
604
|
+
const rel = this.rel(f);
|
|
605
|
+
return rel.length > 0 && !ig.ignores(rel);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
loadGitignore() {
|
|
609
|
+
try {
|
|
610
|
+
const content = readFileSync(resolve(this.root, ".gitignore"), "utf8");
|
|
611
|
+
return ignore().add(content);
|
|
612
|
+
} catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
function sha1(content) {
|
|
618
|
+
return createHash("sha1").update(content).digest("hex");
|
|
619
|
+
}
|
|
620
|
+
async function fileMtime(abs, fallback) {
|
|
621
|
+
try {
|
|
622
|
+
const st = await stat(abs);
|
|
623
|
+
return Math.floor(st.mtimeMs);
|
|
624
|
+
} catch {
|
|
625
|
+
return fallback;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function errorMessage(err) {
|
|
629
|
+
return err instanceof Error ? err.message : String(err);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/mcp.ts
|
|
633
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
634
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
635
|
+
import { z } from "zod";
|
|
636
|
+
|
|
637
|
+
// src/version.ts
|
|
638
|
+
var VERSION = "0.1.0";
|
|
639
|
+
|
|
640
|
+
// src/mcp.ts
|
|
641
|
+
var KIND = z.enum(["function", "method", "class", "interface", "type", "enum", "variable"]);
|
|
642
|
+
function textResult(text) {
|
|
643
|
+
return { content: [{ type: "text", text }] };
|
|
644
|
+
}
|
|
645
|
+
function createServer(store) {
|
|
646
|
+
const server = new McpServer({ name: "codescope", version: VERSION });
|
|
647
|
+
server.registerTool(
|
|
648
|
+
"search_symbols",
|
|
649
|
+
{
|
|
650
|
+
title: "Search code symbols",
|
|
651
|
+
description: "Fuzzy-search definitions (functions, classes, methods, interfaces, types, enums) by name across the whole repo. Prefer this over grep/glob/read when locating where something is defined \u2014 it returns exact file:line locations and signatures in a few tokens.",
|
|
652
|
+
inputSchema: {
|
|
653
|
+
query: z.string().describe("substring to match against symbol names"),
|
|
654
|
+
kind: KIND.optional().describe("restrict to one symbol kind"),
|
|
655
|
+
limit: z.number().int().positive().max(500).optional()
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
async ({ query, kind, limit }) => textResult(formatSymbols(store.searchSymbols(query, { kind, limit })))
|
|
659
|
+
);
|
|
660
|
+
server.registerTool(
|
|
661
|
+
"get_symbol",
|
|
662
|
+
{
|
|
663
|
+
title: "Get a symbol definition",
|
|
664
|
+
description: "Look up a definition by its exact name. Returns each matching definition's kind, file:line, and signature. Use this to jump straight to a definition instead of reading files.",
|
|
665
|
+
inputSchema: {
|
|
666
|
+
name: z.string().describe("exact symbol name"),
|
|
667
|
+
limit: z.number().int().positive().max(500).optional()
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
async ({ name, limit }) => textResult(formatSymbols(store.getSymbol(name, { limit })))
|
|
671
|
+
);
|
|
672
|
+
server.registerTool(
|
|
673
|
+
"find_callers",
|
|
674
|
+
{
|
|
675
|
+
title: "Find callers",
|
|
676
|
+
description: "List the symbols that call a given function/method name, with file:line. Use this to trace impact and call sites without scanning files.",
|
|
677
|
+
inputSchema: {
|
|
678
|
+
name: z.string().describe("the called function/method name"),
|
|
679
|
+
limit: z.number().int().positive().max(500).optional()
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
async ({ name, limit }) => textResult(formatRefs(store.findCallers(name, { limit })))
|
|
683
|
+
);
|
|
684
|
+
server.registerTool(
|
|
685
|
+
"find_references",
|
|
686
|
+
{
|
|
687
|
+
title: "Find references",
|
|
688
|
+
description: "List all references (calls and imports) to a name. Useful for understanding how widely something is used.",
|
|
689
|
+
inputSchema: {
|
|
690
|
+
name: z.string(),
|
|
691
|
+
kind: z.enum(["call", "method", "import"]).optional(),
|
|
692
|
+
limit: z.number().int().positive().max(500).optional()
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
async ({ name, kind, limit }) => textResult(formatRefs(store.findReferences(name, { kind, limit })))
|
|
696
|
+
);
|
|
697
|
+
server.registerTool(
|
|
698
|
+
"file_outline",
|
|
699
|
+
{
|
|
700
|
+
title: "Outline a file",
|
|
701
|
+
description: "List every symbol defined in a file, in source order, with signatures. A compact alternative to reading the whole file when you only need its shape.",
|
|
702
|
+
inputSchema: {
|
|
703
|
+
path: z.string().describe("repo-relative file path")
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
async ({ path }) => textResult(formatSymbols(store.fileOutline(path)))
|
|
707
|
+
);
|
|
708
|
+
server.registerTool(
|
|
709
|
+
"neighborhood",
|
|
710
|
+
{
|
|
711
|
+
title: "Call neighbourhood",
|
|
712
|
+
description: "Return the call neighbourhood around a symbol \u2014 its callers and callees expanded a few hops \u2014 as a compact subgraph. This is the high-leverage tool: it gives you the relevant slice of the codebase for a change without reading dozens of files.",
|
|
713
|
+
inputSchema: {
|
|
714
|
+
name: z.string(),
|
|
715
|
+
depth: z.number().int().min(1).max(5).optional().describe("hops to expand (default 2)"),
|
|
716
|
+
limit: z.number().int().positive().max(200).optional()
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
async ({ name, depth, limit }) => textResult(formatNeighborhood(store.neighborhood(name, { depth, limit })))
|
|
720
|
+
);
|
|
721
|
+
server.registerTool(
|
|
722
|
+
"stats",
|
|
723
|
+
{
|
|
724
|
+
title: "Graph stats",
|
|
725
|
+
description: "Summary counts for the indexed graph (files, symbols, refs, by kind and language).",
|
|
726
|
+
inputSchema: {}
|
|
727
|
+
},
|
|
728
|
+
async () => textResult(formatStats(store.stats()))
|
|
729
|
+
);
|
|
730
|
+
return server;
|
|
731
|
+
}
|
|
732
|
+
async function runStdioServer(store) {
|
|
733
|
+
const server = createServer(store);
|
|
734
|
+
const transport = new StdioServerTransport();
|
|
735
|
+
await server.connect(transport);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/store.ts
|
|
739
|
+
import Database from "better-sqlite3";
|
|
740
|
+
var SCHEMA = `
|
|
741
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
742
|
+
id INTEGER PRIMARY KEY,
|
|
743
|
+
path TEXT NOT NULL UNIQUE,
|
|
744
|
+
lang TEXT NOT NULL,
|
|
745
|
+
hash TEXT NOT NULL,
|
|
746
|
+
size INTEGER NOT NULL,
|
|
747
|
+
mtime INTEGER NOT NULL,
|
|
748
|
+
indexed_at INTEGER NOT NULL
|
|
749
|
+
);
|
|
750
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
751
|
+
id INTEGER PRIMARY KEY,
|
|
752
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
753
|
+
name TEXT NOT NULL,
|
|
754
|
+
kind TEXT NOT NULL,
|
|
755
|
+
container TEXT,
|
|
756
|
+
exported INTEGER NOT NULL DEFAULT 0,
|
|
757
|
+
signature TEXT,
|
|
758
|
+
start_row INTEGER NOT NULL,
|
|
759
|
+
start_col INTEGER NOT NULL,
|
|
760
|
+
end_row INTEGER NOT NULL,
|
|
761
|
+
end_col INTEGER NOT NULL,
|
|
762
|
+
start_byte INTEGER NOT NULL,
|
|
763
|
+
end_byte INTEGER NOT NULL
|
|
764
|
+
);
|
|
765
|
+
CREATE TABLE IF NOT EXISTS refs (
|
|
766
|
+
id INTEGER PRIMARY KEY,
|
|
767
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
768
|
+
from_symbol TEXT,
|
|
769
|
+
name TEXT NOT NULL,
|
|
770
|
+
kind TEXT NOT NULL,
|
|
771
|
+
start_row INTEGER NOT NULL,
|
|
772
|
+
start_col INTEGER NOT NULL
|
|
773
|
+
);
|
|
774
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
|
|
775
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id);
|
|
776
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
|
|
777
|
+
CREATE INDEX IF NOT EXISTS idx_refs_name ON refs(name);
|
|
778
|
+
CREATE INDEX IF NOT EXISTS idx_refs_file ON refs(file_id);
|
|
779
|
+
CREATE INDEX IF NOT EXISTS idx_refs_from ON refs(from_symbol);
|
|
780
|
+
|
|
781
|
+
-- Trigram FTS index for fast substring symbol search at scale. Kept in sync
|
|
782
|
+
-- manually (rowid = symbols.id) so it survives the per-file replace/delete path.
|
|
783
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(name, tokenize='trigram');
|
|
784
|
+
`;
|
|
785
|
+
var SYMBOL_COLUMNS = `
|
|
786
|
+
s.id, f.path AS file, s.name, s.kind, s.container, s.exported, s.signature,
|
|
787
|
+
s.start_row, s.start_col, s.end_row, s.end_col`;
|
|
788
|
+
var GraphStore = class {
|
|
789
|
+
db;
|
|
790
|
+
constructor(location = ":memory:") {
|
|
791
|
+
this.db = new Database(location);
|
|
792
|
+
this.db.pragma("journal_mode = WAL");
|
|
793
|
+
this.db.pragma("foreign_keys = ON");
|
|
794
|
+
this.db.exec(SCHEMA);
|
|
795
|
+
}
|
|
796
|
+
/** The content hash of an already-indexed file, if present. */
|
|
797
|
+
getFileHash(path) {
|
|
798
|
+
const row = this.db.prepare("SELECT hash FROM files WHERE path = ?").get(path);
|
|
799
|
+
return row?.hash;
|
|
800
|
+
}
|
|
801
|
+
/** All indexed file paths. */
|
|
802
|
+
listFiles() {
|
|
803
|
+
return this.db.prepare("SELECT path FROM files ORDER BY path").all().map((r) => r.path);
|
|
804
|
+
}
|
|
805
|
+
/** Insert or replace a file and all of its symbols/refs in one transaction. */
|
|
806
|
+
replaceFile(meta, symbols, refs, now) {
|
|
807
|
+
this.transaction(() => {
|
|
808
|
+
this.dropFtsForFile(meta.path);
|
|
809
|
+
this.db.prepare("DELETE FROM files WHERE path = ?").run(meta.path);
|
|
810
|
+
const fileId = Number(
|
|
811
|
+
this.db.prepare(
|
|
812
|
+
"INSERT INTO files (path, lang, hash, size, mtime, indexed_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
813
|
+
).run(meta.path, meta.lang, meta.hash, meta.size, meta.mtime, now).lastInsertRowid
|
|
814
|
+
);
|
|
815
|
+
const insSym = this.db.prepare(
|
|
816
|
+
`INSERT INTO symbols
|
|
817
|
+
(file_id, name, kind, container, exported, signature,
|
|
818
|
+
start_row, start_col, end_row, end_col, start_byte, end_byte)
|
|
819
|
+
VALUES (@file_id, @name, @kind, @container, @exported, @signature,
|
|
820
|
+
@start_row, @start_col, @end_row, @end_col, @start_byte, @end_byte)`
|
|
821
|
+
);
|
|
822
|
+
const insFts = this.db.prepare("INSERT INTO symbols_fts(rowid, name) VALUES (?, ?)");
|
|
823
|
+
for (const s of symbols) {
|
|
824
|
+
const symId = insSym.run({
|
|
825
|
+
file_id: fileId,
|
|
826
|
+
name: s.name,
|
|
827
|
+
kind: s.kind,
|
|
828
|
+
container: s.container,
|
|
829
|
+
exported: s.exported ? 1 : 0,
|
|
830
|
+
signature: s.signature,
|
|
831
|
+
start_row: s.startRow,
|
|
832
|
+
start_col: s.startCol,
|
|
833
|
+
end_row: s.endRow,
|
|
834
|
+
end_col: s.endCol,
|
|
835
|
+
start_byte: s.startByte,
|
|
836
|
+
end_byte: s.endByte
|
|
837
|
+
}).lastInsertRowid;
|
|
838
|
+
insFts.run(symId, s.name);
|
|
839
|
+
}
|
|
840
|
+
const insRef = this.db.prepare(
|
|
841
|
+
`INSERT INTO refs (file_id, from_symbol, name, kind, start_row, start_col)
|
|
842
|
+
VALUES (@file_id, @from_symbol, @name, @kind, @start_row, @start_col)`
|
|
843
|
+
);
|
|
844
|
+
for (const r of refs) {
|
|
845
|
+
insRef.run({
|
|
846
|
+
file_id: fileId,
|
|
847
|
+
from_symbol: r.fromSymbol,
|
|
848
|
+
name: r.name,
|
|
849
|
+
kind: r.kind,
|
|
850
|
+
start_row: r.startRow,
|
|
851
|
+
start_col: r.startCol
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
/** Remove a file and its symbols/refs. Returns true if anything was deleted. */
|
|
857
|
+
removeFile(path) {
|
|
858
|
+
return this.transactionResult(() => {
|
|
859
|
+
this.dropFtsForFile(path);
|
|
860
|
+
return this.db.prepare("DELETE FROM files WHERE path = ?").run(path).changes > 0;
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
/** Remove the FTS rows for a file's current symbols (call before deleting it). */
|
|
864
|
+
dropFtsForFile(path) {
|
|
865
|
+
const ids = this.db.prepare(
|
|
866
|
+
"SELECT s.id FROM symbols s JOIN files f ON f.id = s.file_id WHERE f.path = ?"
|
|
867
|
+
).all(path);
|
|
868
|
+
if (ids.length === 0) return;
|
|
869
|
+
const del = this.db.prepare("DELETE FROM symbols_fts WHERE rowid = ?");
|
|
870
|
+
for (const { id } of ids) del.run(id);
|
|
871
|
+
}
|
|
872
|
+
// ── Queries ────────────────────────────────────────────────────────────
|
|
873
|
+
/**
|
|
874
|
+
* Fuzzy substring search over symbol names. Queries of 3+ characters use the
|
|
875
|
+
* trigram FTS index (fast at scale); shorter queries fall back to LIKE since
|
|
876
|
+
* trigram matching needs at least three characters.
|
|
877
|
+
*/
|
|
878
|
+
searchSymbols(query, opts = {}) {
|
|
879
|
+
const limit = clampLimit(opts.limit);
|
|
880
|
+
if (query.trim().length >= 3) {
|
|
881
|
+
const match = `"${query.replace(/"/g, '""')}"`;
|
|
882
|
+
const rows2 = opts.kind ? this.db.prepare(
|
|
883
|
+
`SELECT ${SYMBOL_COLUMNS} FROM symbols_fts ft
|
|
884
|
+
JOIN symbols s ON s.id = ft.rowid JOIN files f ON f.id = s.file_id
|
|
885
|
+
WHERE symbols_fts MATCH ? AND s.kind = ?
|
|
886
|
+
ORDER BY s.exported DESC, length(s.name), s.name LIMIT ?`
|
|
887
|
+
).all(match, opts.kind, limit) : this.db.prepare(
|
|
888
|
+
`SELECT ${SYMBOL_COLUMNS} FROM symbols_fts ft
|
|
889
|
+
JOIN symbols s ON s.id = ft.rowid JOIN files f ON f.id = s.file_id
|
|
890
|
+
WHERE symbols_fts MATCH ?
|
|
891
|
+
ORDER BY s.exported DESC, length(s.name), s.name LIMIT ?`
|
|
892
|
+
).all(match, limit);
|
|
893
|
+
return rows2.map(toSymbolRow);
|
|
894
|
+
}
|
|
895
|
+
const like = `%${escapeLike(query)}%`;
|
|
896
|
+
const rows = opts.kind ? this.db.prepare(
|
|
897
|
+
`SELECT ${SYMBOL_COLUMNS} FROM symbols s JOIN files f ON f.id = s.file_id
|
|
898
|
+
WHERE s.name LIKE ? ESCAPE '\\' AND s.kind = ?
|
|
899
|
+
ORDER BY s.exported DESC, length(s.name), s.name LIMIT ?`
|
|
900
|
+
).all(like, opts.kind, limit) : this.db.prepare(
|
|
901
|
+
`SELECT ${SYMBOL_COLUMNS} FROM symbols s JOIN files f ON f.id = s.file_id
|
|
902
|
+
WHERE s.name LIKE ? ESCAPE '\\'
|
|
903
|
+
ORDER BY s.exported DESC, length(s.name), s.name LIMIT ?`
|
|
904
|
+
).all(like, limit);
|
|
905
|
+
return rows.map(toSymbolRow);
|
|
906
|
+
}
|
|
907
|
+
/** Exact-name definition lookup. */
|
|
908
|
+
getSymbol(name, opts = {}) {
|
|
909
|
+
return this.db.prepare(
|
|
910
|
+
`SELECT ${SYMBOL_COLUMNS} FROM symbols s JOIN files f ON f.id = s.file_id
|
|
911
|
+
WHERE s.name = ? ORDER BY s.exported DESC, f.path LIMIT ?`
|
|
912
|
+
).all(name, clampLimit(opts.limit)).map(toSymbolRow);
|
|
913
|
+
}
|
|
914
|
+
/** Symbols that call a given name (both bare `foo()` and `x.foo()`). */
|
|
915
|
+
findCallers(name, opts = {}) {
|
|
916
|
+
return this.db.prepare(
|
|
917
|
+
`SELECT r.id, f.path AS file, r.from_symbol, r.name, r.kind, r.start_row, r.start_col
|
|
918
|
+
FROM refs r JOIN files f ON f.id = r.file_id
|
|
919
|
+
WHERE r.name = ? AND r.kind IN ('call', 'method')
|
|
920
|
+
ORDER BY f.path, r.start_row LIMIT ?`
|
|
921
|
+
).all(name, clampLimit(opts.limit)).map(toRefRow);
|
|
922
|
+
}
|
|
923
|
+
/** All references (calls + imports) to a name. */
|
|
924
|
+
findReferences(name, opts = {}) {
|
|
925
|
+
const limit = clampLimit(opts.limit);
|
|
926
|
+
const rows = opts.kind ? this.db.prepare(
|
|
927
|
+
`SELECT r.id, f.path AS file, r.from_symbol, r.name, r.kind, r.start_row, r.start_col
|
|
928
|
+
FROM refs r JOIN files f ON f.id = r.file_id
|
|
929
|
+
WHERE r.name = ? AND r.kind = ? ORDER BY f.path, r.start_row LIMIT ?`
|
|
930
|
+
).all(name, opts.kind, limit) : this.db.prepare(
|
|
931
|
+
`SELECT r.id, f.path AS file, r.from_symbol, r.name, r.kind, r.start_row, r.start_col
|
|
932
|
+
FROM refs r JOIN files f ON f.id = r.file_id
|
|
933
|
+
WHERE r.name = ? ORDER BY f.path, r.start_row LIMIT ?`
|
|
934
|
+
).all(name, limit);
|
|
935
|
+
return rows.map(toRefRow);
|
|
936
|
+
}
|
|
937
|
+
/** The symbols defined in a file, in source order. */
|
|
938
|
+
fileOutline(path) {
|
|
939
|
+
return this.db.prepare(
|
|
940
|
+
`SELECT ${SYMBOL_COLUMNS} FROM symbols s JOIN files f ON f.id = s.file_id
|
|
941
|
+
WHERE f.path = ? ORDER BY s.start_row, s.start_col`
|
|
942
|
+
).all(path).map(toSymbolRow);
|
|
943
|
+
}
|
|
944
|
+
/** Distinct (callee, callKind) pairs invoked from inside symbols named `from`. */
|
|
945
|
+
calleesOf(from) {
|
|
946
|
+
return this.db.prepare(
|
|
947
|
+
"SELECT DISTINCT name, kind FROM refs WHERE from_symbol = ? AND kind IN ('call','method')"
|
|
948
|
+
).all(from);
|
|
949
|
+
}
|
|
950
|
+
/** Distinct caller symbol names that invoke `name`. */
|
|
951
|
+
callersOf(name) {
|
|
952
|
+
return this.db.prepare(
|
|
953
|
+
"SELECT DISTINCT from_symbol FROM refs WHERE name = ? AND kind IN ('call','method') AND from_symbol IS NOT NULL"
|
|
954
|
+
).all(name).map((r) => r.from_symbol);
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Resolve a callee name to project definitions, honouring how it was called:
|
|
958
|
+
* a bare `foo()` resolves to non-method symbols, an `x.foo()` resolves to
|
|
959
|
+
* methods. Ambiguous names (more than `ambiguityCap` definitions — typically
|
|
960
|
+
* library-ish names like `push`/`map`) are treated as unresolved so they
|
|
961
|
+
* don't blow up the neighbourhood. Returns `null` when nothing resolves.
|
|
962
|
+
*/
|
|
963
|
+
resolveCallee(name, callKind, ambiguityCap) {
|
|
964
|
+
const defs = this.getSymbol(name, { limit: ambiguityCap + 1 }).filter(
|
|
965
|
+
(d) => callKind === "method" ? d.kind === "method" : d.kind !== "method"
|
|
966
|
+
);
|
|
967
|
+
if (defs.length === 0 || defs.length > ambiguityCap) return null;
|
|
968
|
+
return defs;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* A bounded call neighbourhood around `name`: callers and callees expanded
|
|
972
|
+
* breadth-first up to `depth`. Only edges between *resolvable project
|
|
973
|
+
* symbols* are followed, so the result is the relevant slice of the codebase
|
|
974
|
+
* — the payload a coding agent reads instead of grepping the whole repo.
|
|
975
|
+
*/
|
|
976
|
+
neighborhood(name, opts = {}) {
|
|
977
|
+
const depth = Math.max(1, Math.min(opts.depth ?? 2, 5));
|
|
978
|
+
const limit = clampLimit(opts.limit, 200);
|
|
979
|
+
const maxFanout = opts.maxFanout ?? 25;
|
|
980
|
+
const ambiguityCap = opts.ambiguityCap ?? 4;
|
|
981
|
+
const seen = /* @__PURE__ */ new Set([name]);
|
|
982
|
+
const edges = [];
|
|
983
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
984
|
+
let frontier = [name];
|
|
985
|
+
for (let d = 0; d < depth && frontier.length > 0 && seen.size < limit; d++) {
|
|
986
|
+
const next = /* @__PURE__ */ new Set();
|
|
987
|
+
for (const node of frontier) {
|
|
988
|
+
let fanout = 0;
|
|
989
|
+
for (const callee of this.calleesOf(node)) {
|
|
990
|
+
if (fanout >= maxFanout) break;
|
|
991
|
+
if (!this.resolveCallee(callee.name, callee.kind, ambiguityCap)) continue;
|
|
992
|
+
fanout++;
|
|
993
|
+
addEdge(edges, edgeKeys, node, callee.name);
|
|
994
|
+
if (!seen.has(callee.name) && seen.size < limit) {
|
|
995
|
+
seen.add(callee.name);
|
|
996
|
+
next.add(callee.name);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
let callerFanout = 0;
|
|
1000
|
+
for (const caller of this.callersOf(node)) {
|
|
1001
|
+
if (callerFanout >= maxFanout) break;
|
|
1002
|
+
callerFanout++;
|
|
1003
|
+
addEdge(edges, edgeKeys, caller, node);
|
|
1004
|
+
if (!seen.has(caller) && seen.size < limit) {
|
|
1005
|
+
seen.add(caller);
|
|
1006
|
+
next.add(caller);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
frontier = [...next];
|
|
1011
|
+
}
|
|
1012
|
+
const nodes = [];
|
|
1013
|
+
const unresolved = [];
|
|
1014
|
+
for (const n of seen) {
|
|
1015
|
+
const defs = this.getSymbol(n, { limit: 5 });
|
|
1016
|
+
if (defs.length > 0) nodes.push(...defs);
|
|
1017
|
+
else unresolved.push(n);
|
|
1018
|
+
}
|
|
1019
|
+
return { root: name, nodes, edges, unresolved };
|
|
1020
|
+
}
|
|
1021
|
+
/** Aggregate counts for the whole graph. */
|
|
1022
|
+
stats() {
|
|
1023
|
+
const files = this.count("SELECT COUNT(*) AS n FROM files");
|
|
1024
|
+
const symbols = this.count("SELECT COUNT(*) AS n FROM symbols");
|
|
1025
|
+
const refs = this.count("SELECT COUNT(*) AS n FROM refs");
|
|
1026
|
+
const byKind = {};
|
|
1027
|
+
for (const r of this.db.prepare(
|
|
1028
|
+
"SELECT kind, COUNT(*) AS n FROM symbols GROUP BY kind"
|
|
1029
|
+
).all()) {
|
|
1030
|
+
byKind[r.kind] = r.n;
|
|
1031
|
+
}
|
|
1032
|
+
const byLang = {};
|
|
1033
|
+
for (const r of this.db.prepare(
|
|
1034
|
+
"SELECT lang, COUNT(*) AS n FROM files GROUP BY lang"
|
|
1035
|
+
).all()) {
|
|
1036
|
+
byLang[r.lang] = r.n;
|
|
1037
|
+
}
|
|
1038
|
+
return { files, symbols, refs, byKind, byLang };
|
|
1039
|
+
}
|
|
1040
|
+
close() {
|
|
1041
|
+
this.db.close();
|
|
1042
|
+
}
|
|
1043
|
+
count(sql) {
|
|
1044
|
+
return this.db.prepare(sql).get()?.n ?? 0;
|
|
1045
|
+
}
|
|
1046
|
+
transaction(fn) {
|
|
1047
|
+
this.db.transaction(fn)();
|
|
1048
|
+
}
|
|
1049
|
+
transactionResult(fn) {
|
|
1050
|
+
return this.db.transaction(fn)();
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
function addEdge(edges, keys, from, to) {
|
|
1054
|
+
const key = `${from}\0${to}`;
|
|
1055
|
+
if (!keys.has(key)) {
|
|
1056
|
+
keys.add(key);
|
|
1057
|
+
edges.push({ from, to });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function clampLimit(limit, max = 500) {
|
|
1061
|
+
if (!limit || limit <= 0) return 50;
|
|
1062
|
+
return Math.min(limit, max);
|
|
1063
|
+
}
|
|
1064
|
+
function escapeLike(s) {
|
|
1065
|
+
return s.replace(/[\\%_]/g, (c2) => `\\${c2}`);
|
|
1066
|
+
}
|
|
1067
|
+
function toSymbolRow(r) {
|
|
1068
|
+
return {
|
|
1069
|
+
id: r.id,
|
|
1070
|
+
file: r.file,
|
|
1071
|
+
name: r.name,
|
|
1072
|
+
kind: r.kind,
|
|
1073
|
+
container: r.container,
|
|
1074
|
+
exported: r.exported === 1,
|
|
1075
|
+
signature: r.signature,
|
|
1076
|
+
startRow: r.start_row,
|
|
1077
|
+
startCol: r.start_col,
|
|
1078
|
+
endRow: r.end_row,
|
|
1079
|
+
endCol: r.end_col
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function toRefRow(r) {
|
|
1083
|
+
return {
|
|
1084
|
+
id: r.id,
|
|
1085
|
+
file: r.file,
|
|
1086
|
+
fromSymbol: r.from_symbol,
|
|
1087
|
+
name: r.name,
|
|
1088
|
+
kind: r.kind,
|
|
1089
|
+
startRow: r.start_row,
|
|
1090
|
+
startCol: r.start_col
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/watcher.ts
|
|
1095
|
+
import { watch as chokidarWatch } from "chokidar";
|
|
1096
|
+
var IGNORED = /(?:^|[\\/])(?:node_modules|\.git|dist|build|coverage|\.codescope|target|__pycache__)(?:[\\/]|$)/;
|
|
1097
|
+
function watch(indexer, events = {}, opts = {}) {
|
|
1098
|
+
const watcher = chokidarWatch(indexer.root, {
|
|
1099
|
+
ignoreInitial: true,
|
|
1100
|
+
ignored: (path) => IGNORED.test(path),
|
|
1101
|
+
usePolling: opts.usePolling,
|
|
1102
|
+
interval: opts.interval
|
|
1103
|
+
});
|
|
1104
|
+
const onUpsert = (abs) => {
|
|
1105
|
+
if (!languageForPath(abs)) return;
|
|
1106
|
+
indexer.indexFile(abs).then((outcome) => {
|
|
1107
|
+
if (outcome === "indexed") events.onChange?.(indexer.rel(abs), "indexed");
|
|
1108
|
+
}).catch((err) => events.onError?.(err));
|
|
1109
|
+
};
|
|
1110
|
+
watcher.on("add", onUpsert).on("change", onUpsert).on("unlink", (abs) => {
|
|
1111
|
+
if (indexer.removeFile(abs)) events.onChange?.(indexer.rel(abs), "removed");
|
|
1112
|
+
}).on("error", (err) => events.onError?.(err)).on("ready", () => events.onReady?.());
|
|
1113
|
+
return {
|
|
1114
|
+
close: () => watcher.close()
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/cli.ts
|
|
1119
|
+
var HELP = `codescope ${VERSION} \u2014 local-first codebase knowledge-graph MCP server
|
|
1120
|
+
|
|
1121
|
+
Usage:
|
|
1122
|
+
codescope <command> [path] [options]
|
|
1123
|
+
|
|
1124
|
+
Commands:
|
|
1125
|
+
mcp [path] Index, watch for changes, and serve the graph over MCP (stdio).
|
|
1126
|
+
This is what you wire into Claude Code / Cursor / Codex.
|
|
1127
|
+
index [path] Build (or refresh) the on-disk graph and print stats.
|
|
1128
|
+
watch [path] Index, then keep the graph fresh as files change (logs updates).
|
|
1129
|
+
stats [path] Show counts for the indexed graph.
|
|
1130
|
+
search <query> [path] Fuzzy-search symbol names.
|
|
1131
|
+
get <name> [path] Look up a definition by exact name.
|
|
1132
|
+
callers <name> [path] List callers of a function/method.
|
|
1133
|
+
neighborhood <name> Show the call neighbourhood around a symbol.
|
|
1134
|
+
|
|
1135
|
+
Options:
|
|
1136
|
+
--path <dir> Repository root (default: current directory or the positional path).
|
|
1137
|
+
--db <file> SQLite graph location (default: <root>/.codescope/graph.db).
|
|
1138
|
+
--memory Use an in-memory graph (not persisted).
|
|
1139
|
+
--kind <kind> Restrict search to: function|method|class|interface|type|enum|variable.
|
|
1140
|
+
--limit <n> Max results (default 50).
|
|
1141
|
+
--depth <n> neighbourhood hops (default 2).
|
|
1142
|
+
-h, --help Show this help.
|
|
1143
|
+
-v, --version Show version.
|
|
1144
|
+
|
|
1145
|
+
Examples:
|
|
1146
|
+
codescope index .
|
|
1147
|
+
codescope search useState
|
|
1148
|
+
codescope neighborhood handleRequest --depth 3
|
|
1149
|
+
codescope mcp . # add this command to your agent's MCP config
|
|
1150
|
+
`;
|
|
1151
|
+
function parseArgs(argv) {
|
|
1152
|
+
const flags = { positional: [], memory: false };
|
|
1153
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1154
|
+
const arg = argv[i];
|
|
1155
|
+
switch (arg) {
|
|
1156
|
+
case "--memory":
|
|
1157
|
+
flags.memory = true;
|
|
1158
|
+
break;
|
|
1159
|
+
case "--path":
|
|
1160
|
+
flags.path = argv[++i];
|
|
1161
|
+
break;
|
|
1162
|
+
case "--db":
|
|
1163
|
+
flags.db = argv[++i];
|
|
1164
|
+
break;
|
|
1165
|
+
case "--kind":
|
|
1166
|
+
flags.kind = argv[++i];
|
|
1167
|
+
break;
|
|
1168
|
+
case "--limit":
|
|
1169
|
+
flags.limit = Number(argv[++i]);
|
|
1170
|
+
break;
|
|
1171
|
+
case "--depth":
|
|
1172
|
+
flags.depth = Number(argv[++i]);
|
|
1173
|
+
break;
|
|
1174
|
+
default:
|
|
1175
|
+
flags.positional.push(arg);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return flags;
|
|
1179
|
+
}
|
|
1180
|
+
function rootDir(flags, positionalRootIndex = 0) {
|
|
1181
|
+
return resolve2(flags.path ?? flags.positional[positionalRootIndex] ?? ".");
|
|
1182
|
+
}
|
|
1183
|
+
function openStore(root, flags) {
|
|
1184
|
+
if (flags.memory) return new GraphStore(":memory:");
|
|
1185
|
+
const dir = resolve2(root, ".codescope");
|
|
1186
|
+
mkdirSync(dir, { recursive: true });
|
|
1187
|
+
return new GraphStore(flags.db ?? resolve2(dir, "graph.db"));
|
|
1188
|
+
}
|
|
1189
|
+
async function ensureIndexed(indexer, store) {
|
|
1190
|
+
if (store.stats().files === 0) {
|
|
1191
|
+
await indexer.indexAll();
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
async function cmdIndex(root, flags) {
|
|
1195
|
+
const store = openStore(root, flags);
|
|
1196
|
+
const indexer = new Indexer(store, root);
|
|
1197
|
+
process.stderr.write(pc.dim(`Indexing ${root} \u2026
|
|
1198
|
+
`));
|
|
1199
|
+
const result = await indexer.indexAll();
|
|
1200
|
+
const { files, symbols, refs } = store.stats();
|
|
1201
|
+
process.stdout.write(
|
|
1202
|
+
`${pc.green("\u2713")} indexed ${pc.bold(String(result.indexed))} files (${result.skipped} unchanged, ${result.removed} removed) in ${result.durationMs}ms
|
|
1203
|
+
${files} files \xB7 ${symbols} symbols \xB7 ${refs} refs
|
|
1204
|
+
`
|
|
1205
|
+
);
|
|
1206
|
+
if (result.errors.length > 0) {
|
|
1207
|
+
process.stderr.write(pc.yellow(` ${result.errors.length} file(s) failed to parse
|
|
1208
|
+
`));
|
|
1209
|
+
}
|
|
1210
|
+
store.close();
|
|
1211
|
+
}
|
|
1212
|
+
async function cmdStats(root, flags) {
|
|
1213
|
+
const store = openStore(root, flags);
|
|
1214
|
+
const indexer = new Indexer(store, root);
|
|
1215
|
+
await ensureIndexed(indexer, store);
|
|
1216
|
+
process.stdout.write(`${formatStats(store.stats())}
|
|
1217
|
+
`);
|
|
1218
|
+
store.close();
|
|
1219
|
+
}
|
|
1220
|
+
async function cmdQuery(command, root, flags) {
|
|
1221
|
+
const term = flags.positional[0];
|
|
1222
|
+
if (!term) fail(`'${command}' needs an argument. See --help.`);
|
|
1223
|
+
const store = openStore(root, flags);
|
|
1224
|
+
const indexer = new Indexer(store, root);
|
|
1225
|
+
await ensureIndexed(indexer, store);
|
|
1226
|
+
let out;
|
|
1227
|
+
switch (command) {
|
|
1228
|
+
case "search":
|
|
1229
|
+
out = formatSymbols(
|
|
1230
|
+
store.searchSymbols(term, { kind: flags.kind, limit: flags.limit })
|
|
1231
|
+
);
|
|
1232
|
+
break;
|
|
1233
|
+
case "get":
|
|
1234
|
+
out = formatSymbols(store.getSymbol(term, { limit: flags.limit }));
|
|
1235
|
+
break;
|
|
1236
|
+
case "callers":
|
|
1237
|
+
out = formatRefs(store.findCallers(term, { limit: flags.limit }));
|
|
1238
|
+
break;
|
|
1239
|
+
case "neighborhood":
|
|
1240
|
+
out = formatNeighborhood(store.neighborhood(term, { depth: flags.depth, limit: flags.limit }));
|
|
1241
|
+
break;
|
|
1242
|
+
default:
|
|
1243
|
+
out = "";
|
|
1244
|
+
}
|
|
1245
|
+
process.stdout.write(`${out}
|
|
1246
|
+
`);
|
|
1247
|
+
store.close();
|
|
1248
|
+
}
|
|
1249
|
+
async function cmdWatch(root, flags) {
|
|
1250
|
+
const store = openStore(root, flags);
|
|
1251
|
+
const indexer = new Indexer(store, root);
|
|
1252
|
+
process.stderr.write(pc.dim(`Indexing ${root} \u2026
|
|
1253
|
+
`));
|
|
1254
|
+
await indexer.indexAll();
|
|
1255
|
+
process.stderr.write(`${pc.green("\u2713")} watching for changes (ctrl-c to stop)
|
|
1256
|
+
`);
|
|
1257
|
+
watch(indexer, {
|
|
1258
|
+
onChange: (rel, action) => process.stderr.write(`${action === "indexed" ? pc.cyan("\u21BB") : pc.red("\u2717")} ${rel}
|
|
1259
|
+
`),
|
|
1260
|
+
onError: (err) => process.stderr.write(pc.yellow(`watch error: ${String(err)}
|
|
1261
|
+
`))
|
|
1262
|
+
});
|
|
1263
|
+
await new Promise(() => {
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
async function cmdMcp(root, flags) {
|
|
1267
|
+
const store = openStore(root, flags);
|
|
1268
|
+
const indexer = new Indexer(store, root);
|
|
1269
|
+
process.stderr.write(pc.dim(`codescope: indexing ${root} \u2026
|
|
1270
|
+
`));
|
|
1271
|
+
const result = await indexer.indexAll();
|
|
1272
|
+
process.stderr.write(
|
|
1273
|
+
pc.dim(`codescope: ${result.indexed} files indexed, watching for changes
|
|
1274
|
+
`)
|
|
1275
|
+
);
|
|
1276
|
+
watch(indexer, {
|
|
1277
|
+
onChange: (rel, action) => process.stderr.write(pc.dim(`codescope: ${action} ${rel}
|
|
1278
|
+
`)),
|
|
1279
|
+
onError: (err) => process.stderr.write(pc.yellow(`codescope: watch error ${String(err)}
|
|
1280
|
+
`))
|
|
1281
|
+
});
|
|
1282
|
+
await runStdioServer(store);
|
|
1283
|
+
}
|
|
1284
|
+
function fail(message) {
|
|
1285
|
+
process.stderr.write(`${pc.red("error:")} ${message}
|
|
1286
|
+
`);
|
|
1287
|
+
process.exit(1);
|
|
1288
|
+
}
|
|
1289
|
+
async function main() {
|
|
1290
|
+
const argv = process.argv.slice(2);
|
|
1291
|
+
if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help") {
|
|
1292
|
+
process.stdout.write(HELP);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (argv[0] === "-v" || argv[0] === "--version" || argv[0] === "version") {
|
|
1296
|
+
process.stdout.write(`${VERSION}
|
|
1297
|
+
`);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
const command = argv[0];
|
|
1301
|
+
const flags = parseArgs(argv.slice(1));
|
|
1302
|
+
switch (command) {
|
|
1303
|
+
case "index":
|
|
1304
|
+
return cmdIndex(rootDir(flags), flags);
|
|
1305
|
+
case "stats":
|
|
1306
|
+
return cmdStats(rootDir(flags), flags);
|
|
1307
|
+
case "search":
|
|
1308
|
+
case "get":
|
|
1309
|
+
case "callers":
|
|
1310
|
+
case "neighborhood":
|
|
1311
|
+
return cmdQuery(command, resolve2(flags.path ?? flags.positional[1] ?? "."), flags);
|
|
1312
|
+
case "watch":
|
|
1313
|
+
return cmdWatch(rootDir(flags), flags);
|
|
1314
|
+
case "mcp":
|
|
1315
|
+
return cmdMcp(rootDir(flags), flags);
|
|
1316
|
+
default:
|
|
1317
|
+
fail(`unknown command '${command}'. See --help.`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));
|
|
1321
|
+
//# sourceMappingURL=cli.js.map
|