@wrongstack/tools 0.7.5 → 0.7.7

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.
Files changed (48) hide show
  1. package/dist/audit.js +3 -3
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +7 -6
  4. package/dist/bash.js.map +1 -1
  5. package/dist/builtin.js +2070 -133
  6. package/dist/builtin.js.map +1 -1
  7. package/dist/codebase-index/index.d.ts +120 -0
  8. package/dist/codebase-index/index.js +1974 -0
  9. package/dist/codebase-index/index.js.map +1 -0
  10. package/dist/codebase-stats-tool-BLhQmPNc.d.ts +158 -0
  11. package/dist/diff.js +4 -4
  12. package/dist/document.js +2 -2
  13. package/dist/edit.js +2 -2
  14. package/dist/exec.js +5 -5
  15. package/dist/exec.js.map +1 -1
  16. package/dist/fetch.js +4 -3
  17. package/dist/fetch.js.map +1 -1
  18. package/dist/format.js +3 -3
  19. package/dist/format.js.map +1 -1
  20. package/dist/git.js +3 -3
  21. package/dist/glob.js +2 -2
  22. package/dist/grep.js +3 -3
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +2039 -102
  25. package/dist/index.js.map +1 -1
  26. package/dist/install.js +3 -3
  27. package/dist/install.js.map +1 -1
  28. package/dist/json.js +1 -1
  29. package/dist/lint.js +3 -3
  30. package/dist/lint.js.map +1 -1
  31. package/dist/logs.js +5 -5
  32. package/dist/logs.js.map +1 -1
  33. package/dist/outdated.js +3 -3
  34. package/dist/pack.js +2070 -133
  35. package/dist/pack.js.map +1 -1
  36. package/dist/patch.js +4 -4
  37. package/dist/process-registry.js +1 -1
  38. package/dist/read.js +2 -2
  39. package/dist/replace.js +4 -4
  40. package/dist/replace.js.map +1 -1
  41. package/dist/scaffold.js +2 -2
  42. package/dist/test.js +3 -3
  43. package/dist/test.js.map +1 -1
  44. package/dist/tree.js +2 -2
  45. package/dist/typecheck.js +3 -3
  46. package/dist/typecheck.js.map +1 -1
  47. package/dist/write.js +2 -2
  48. package/package.json +8 -4
@@ -0,0 +1,1974 @@
1
+ import * as fs2 from 'node:fs/promises';
2
+ import * as path3 from 'node:path';
3
+ import { compileGlob } from '@wrongstack/core';
4
+ import * as fs from 'node:fs';
5
+ import { mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
6
+ import { DatabaseSync } from 'node:sqlite';
7
+ import * as ts from 'typescript';
8
+ import { execSync, spawnSync } from 'node:child_process';
9
+
10
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
11
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
12
+ }) : x)(function(x) {
13
+ if (typeof require !== "undefined") return require.apply(this, arguments);
14
+ throw Error('Dynamic require of "' + x + '" is not supported');
15
+ });
16
+
17
+ // src/codebase-index/schema.ts
18
+ var SCHEMA_VERSION = 1;
19
+
20
+ // src/codebase-index/lsp-kind.ts
21
+ function lspKindToInternalKind(k) {
22
+ switch (k) {
23
+ case 5 /* Class */:
24
+ return "class";
25
+ case 6 /* Method */:
26
+ return "method";
27
+ case 7 /* Property */:
28
+ case 8 /* Field */:
29
+ return "property";
30
+ case 9 /* Constructor */:
31
+ return "class";
32
+ case 10 /* Enum */:
33
+ return "enum";
34
+ case 11 /* Interface */:
35
+ return "interface";
36
+ case 12 /* Function */:
37
+ return "function";
38
+ case 13 /* Variable */:
39
+ return "var";
40
+ case 14 /* Constant */:
41
+ return "const";
42
+ case 22 /* EnumMember */:
43
+ return "enum";
44
+ case 26 /* TypeParameter */:
45
+ return "type";
46
+ case 3 /* Namespace */:
47
+ return "namespace";
48
+ default:
49
+ return null;
50
+ }
51
+ }
52
+ function internalKindToLspKind(k) {
53
+ switch (k) {
54
+ case "class":
55
+ return 5 /* Class */;
56
+ case "method":
57
+ return 6 /* Method */;
58
+ case "property":
59
+ return 7 /* Property */;
60
+ case "function":
61
+ return 12 /* Function */;
62
+ case "var":
63
+ return 13 /* Variable */;
64
+ case "const":
65
+ return 14 /* Constant */;
66
+ case "let":
67
+ return 13 /* Variable */;
68
+ case "enum":
69
+ return 10 /* Enum */;
70
+ case "interface":
71
+ return 11 /* Interface */;
72
+ case "namespace":
73
+ return 3 /* Namespace */;
74
+ case "type":
75
+ return 26 /* TypeParameter */;
76
+ // parameter and other internal-only kinds have no LSP equivalent
77
+ default:
78
+ return null;
79
+ }
80
+ }
81
+
82
+ // src/codebase-index/writer.ts
83
+ var INDEX_DIR = ".codebase-index";
84
+ var DB_FILE = "index.db";
85
+ var IndexStore = class {
86
+ constructor(projectRoot) {
87
+ this.projectRoot = projectRoot;
88
+ const dir = path3.join(projectRoot, INDEX_DIR);
89
+ fs.mkdirSync(dir, { recursive: true });
90
+ this.db = new DatabaseSync(path3.join(dir, DB_FILE));
91
+ this.initSchema();
92
+ }
93
+ projectRoot;
94
+ db;
95
+ initSchema() {
96
+ this.db.exec(`
97
+ CREATE TABLE IF NOT EXISTS metadata (
98
+ key TEXT PRIMARY KEY,
99
+ value TEXT NOT NULL
100
+ );
101
+ CREATE TABLE IF NOT EXISTS files (
102
+ file TEXT PRIMARY KEY,
103
+ lang TEXT NOT NULL,
104
+ mtime_ms INTEGER NOT NULL,
105
+ symbol_count INTEGER NOT NULL DEFAULT 0,
106
+ last_indexed INTEGER NOT NULL
107
+ );
108
+ CREATE TABLE IF NOT EXISTS symbols (
109
+ id INTEGER PRIMARY KEY,
110
+ lang TEXT NOT NULL,
111
+ kind TEXT NOT NULL,
112
+ name TEXT NOT NULL,
113
+ file TEXT NOT NULL,
114
+ line INTEGER NOT NULL,
115
+ col INTEGER NOT NULL,
116
+ signature TEXT NOT NULL DEFAULT '',
117
+ doc_comment TEXT NOT NULL DEFAULT '',
118
+ scope TEXT NOT NULL DEFAULT '',
119
+ text TEXT NOT NULL DEFAULT '',
120
+ file_fk TEXT NOT NULL
121
+ );
122
+ `);
123
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)");
124
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)");
125
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)");
126
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
127
+ this.db.exec(`
128
+ CREATE TABLE IF NOT EXISTS refs (
129
+ id INTEGER PRIMARY KEY,
130
+ from_id INTEGER NOT NULL,
131
+ to_name TEXT NOT NULL,
132
+ to_id INTEGER,
133
+ call_type TEXT NOT NULL,
134
+ line INTEGER NOT NULL
135
+ );
136
+ `);
137
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)");
138
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
139
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
140
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
141
+ const versionRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
142
+ if (!versionRows.length) {
143
+ this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
144
+ }
145
+ }
146
+ // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
147
+ insertSymbols(symbols, nextId) {
148
+ const stmt = this.db.prepare(
149
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
150
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
151
+ );
152
+ let id = nextId;
153
+ for (const s of symbols) {
154
+ stmt.run(
155
+ id++,
156
+ s.lang,
157
+ s.kind,
158
+ s.name,
159
+ s.file,
160
+ s.line,
161
+ s.col,
162
+ s.signature,
163
+ s.docComment,
164
+ s.scope,
165
+ s.text,
166
+ s.file
167
+ );
168
+ }
169
+ return id;
170
+ }
171
+ deleteSymbolsForFile(file) {
172
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
173
+ }
174
+ deleteFile(file) {
175
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
176
+ }
177
+ // ─── File metadata ──────────────────────────────────────────────────────────
178
+ upsertFile(meta) {
179
+ this.db.prepare(
180
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
181
+ VALUES (?, ?, ?, ?, ?)
182
+ ON CONFLICT(file) DO UPDATE SET
183
+ lang = excluded.lang,
184
+ mtime_ms = excluded.mtime_ms,
185
+ symbol_count = excluded.symbol_count,
186
+ last_indexed = excluded.last_indexed`
187
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
188
+ }
189
+ getFileMeta(file) {
190
+ const rows = this.db.prepare(
191
+ "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files WHERE file = ?"
192
+ ).all(file);
193
+ if (!rows.length) return null;
194
+ const r = rows[0];
195
+ return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
196
+ }
197
+ getAllFileMetas() {
198
+ return this.db.prepare(
199
+ "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
200
+ ).all().map(
201
+ (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
202
+ );
203
+ }
204
+ // ─── Search ──────────────────────────────────────────────────────────────────
205
+ search(query, filter) {
206
+ const conditions = [];
207
+ const values = [];
208
+ let effectiveKind = filter?.kind;
209
+ if (filter?.lspKind !== void 0) {
210
+ const mapped = lspKindToInternalKind(filter.lspKind);
211
+ if (mapped !== null) {
212
+ effectiveKind = mapped;
213
+ } else {
214
+ return [];
215
+ }
216
+ }
217
+ if (effectiveKind) {
218
+ conditions.push("kind = ?");
219
+ values.push(effectiveKind);
220
+ }
221
+ if (filter?.lang) {
222
+ conditions.push("lang = ?");
223
+ values.push(filter.lang);
224
+ }
225
+ if (filter?.file) {
226
+ conditions.push("file LIKE ?");
227
+ values.push(`%${filter.file}%`);
228
+ }
229
+ if (query.trim()) {
230
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
231
+ const tokenConds = tokens.map(() => "text LIKE ?");
232
+ conditions.push(`(${tokenConds.join(" OR ")})`);
233
+ for (const t of tokens) values.push(`%${t}%`);
234
+ }
235
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
236
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}`;
237
+ const stmt = this.db.prepare(sql);
238
+ const rows = stmt.all(...values);
239
+ return rows.map((r) => ({
240
+ id: r.id,
241
+ lang: r.lang,
242
+ kind: r.kind,
243
+ name: r.name,
244
+ file: r.file,
245
+ line: r.line,
246
+ col: r.col,
247
+ signature: r.signature,
248
+ docComment: r.doc_comment,
249
+ score: 0,
250
+ snippet: "",
251
+ lspKind: filter?.lspKind
252
+ }));
253
+ }
254
+ getAllIndexable() {
255
+ return this.db.prepare("SELECT id, text FROM symbols").all().map(
256
+ ({ id, text }) => ({ id, text })
257
+ );
258
+ }
259
+ // ─── Stats ───────────────────────────────────────────────────────────────────
260
+ getStats() {
261
+ const sizeBytes = this.sizeBytes();
262
+ const lastRows = this.db.prepare(
263
+ "SELECT value FROM metadata WHERE key = 'last_indexed'"
264
+ ).all();
265
+ const lastIndexed = lastRows.length ? Number(lastRows[0].value) : null;
266
+ const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
267
+ const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
268
+ const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
269
+ const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
270
+ const langRows = this.db.prepare(
271
+ "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
272
+ ).all();
273
+ const byLang = {};
274
+ for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
275
+ const kindRows = this.db.prepare(
276
+ "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
277
+ ).all();
278
+ const byKind = {};
279
+ for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
280
+ return {
281
+ totalSymbols,
282
+ totalFiles,
283
+ byLang,
284
+ byKind,
285
+ indexPath: path3.join(this.projectRoot, INDEX_DIR),
286
+ lastIndexed,
287
+ sizeBytes,
288
+ version: SCHEMA_VERSION
289
+ };
290
+ }
291
+ setLastIndexed(ts2) {
292
+ this.db.prepare(
293
+ "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
294
+ ).run(String(ts2));
295
+ }
296
+ clearAll() {
297
+ this.db.exec("DELETE FROM symbols");
298
+ this.db.exec("DELETE FROM files");
299
+ this.db.exec("DELETE FROM refs");
300
+ }
301
+ // ─── Ref CRUD ────────────────────────────────────────────────────────────────
302
+ /**
303
+ * Insert cross-references for a given source symbol id.
304
+ * Replaces any existing refs from the same source (idempotent on re-index).
305
+ */
306
+ insertRefs(fromId, refs) {
307
+ this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
308
+ if (refs.length === 0) return;
309
+ const stmt = this.db.prepare(
310
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
311
+ VALUES (?, ?, ?, ?, ?)`
312
+ );
313
+ for (const ref of refs) {
314
+ stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
315
+ }
316
+ }
317
+ /**
318
+ * Delete all refs whose source symbols are in a given file.
319
+ * Used when re-indexing a file to clear stale refs.
320
+ */
321
+ deleteRefsForFile(file) {
322
+ const ids = this.db.prepare(
323
+ "SELECT id FROM symbols WHERE file = ?"
324
+ ).all(file);
325
+ if (!ids.length) return;
326
+ const placeholders = ids.map(() => "?").join(",");
327
+ this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
328
+ }
329
+ /**
330
+ * Resolve `to_name` → `to_id` for all refs that have a name but no id.
331
+ * Call this after all symbols have been inserted to fill in cross-references.
332
+ */
333
+ resolveRefs() {
334
+ const unresolved = this.db.prepare(
335
+ "SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
336
+ ).all();
337
+ let resolved = 0;
338
+ for (const row of unresolved) {
339
+ const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
340
+ if (target.length) {
341
+ this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(target[0].id, row.id);
342
+ resolved++;
343
+ }
344
+ }
345
+ return resolved;
346
+ }
347
+ /**
348
+ * Find all references TO a given symbol (who calls / uses this symbol?).
349
+ */
350
+ findRefsTo(symbolId) {
351
+ return this.db.prepare(
352
+ "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
353
+ ).all(symbolId, symbolId).map((r) => ({
354
+ id: r.id,
355
+ fromId: r.from_id,
356
+ toName: r.to_name,
357
+ toId: r.to_id ?? void 0,
358
+ callType: r.call_type,
359
+ line: r.line
360
+ }));
361
+ }
362
+ /**
363
+ * Find all references FROM a given symbol (what does this symbol call/use?).
364
+ */
365
+ findRefsFrom(symbolId) {
366
+ return this.db.prepare(
367
+ "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
368
+ ).all(symbolId).map((r) => ({
369
+ id: r.id,
370
+ fromId: r.from_id,
371
+ toName: r.to_name,
372
+ toId: r.to_id ?? void 0,
373
+ callType: r.call_type,
374
+ line: r.line
375
+ }));
376
+ }
377
+ sizeBytes() {
378
+ const dbPath = path3.join(this.projectRoot, INDEX_DIR, DB_FILE);
379
+ try {
380
+ return fs.statSync(dbPath).size;
381
+ } catch {
382
+ return 0;
383
+ }
384
+ }
385
+ close() {
386
+ try {
387
+ this.db.close();
388
+ } catch {
389
+ }
390
+ }
391
+ };
392
+ var KIND_MAP = {
393
+ [ts.SyntaxKind.ClassDeclaration]: "class",
394
+ [ts.SyntaxKind.InterfaceDeclaration]: "interface",
395
+ [ts.SyntaxKind.EnumDeclaration]: "enum",
396
+ [ts.SyntaxKind.TypeAliasDeclaration]: "type",
397
+ [ts.SyntaxKind.FunctionDeclaration]: "function",
398
+ [ts.SyntaxKind.MethodDeclaration]: "method",
399
+ [ts.SyntaxKind.GetAccessor]: "property",
400
+ [ts.SyntaxKind.SetAccessor]: "property",
401
+ [ts.SyntaxKind.PropertyDeclaration]: "property",
402
+ [ts.SyntaxKind.Parameter]: "parameter",
403
+ [ts.SyntaxKind.NamespaceExportDeclaration]: "namespace"
404
+ };
405
+ function kindOf(node) {
406
+ if (ts.isVariableDeclaration(node)) {
407
+ const parent = node.parent;
408
+ if (ts.isVariableDeclarationList(parent)) {
409
+ const flags = parent.flags;
410
+ if (flags & ts.NodeFlags.Let) return "let";
411
+ if (flags & ts.NodeFlags.Const) return "const";
412
+ return "var";
413
+ }
414
+ }
415
+ if (ts.isModuleDeclaration(node)) return "namespace";
416
+ return KIND_MAP[node.kind] ?? null;
417
+ }
418
+ function extToLang(ext) {
419
+ switch (ext) {
420
+ case ".ts":
421
+ return "ts";
422
+ case ".tsx":
423
+ return "tsx";
424
+ case ".js":
425
+ return "js";
426
+ case ".jsx":
427
+ return "jsx";
428
+ case ".go":
429
+ return "go";
430
+ case ".py":
431
+ return "py";
432
+ case ".rs":
433
+ return "rs";
434
+ case ".json":
435
+ return "json";
436
+ case ".yaml":
437
+ return "yaml";
438
+ case ".yml":
439
+ return "yaml";
440
+ default:
441
+ return null;
442
+ }
443
+ }
444
+ function getSignature(node, sourceFile) {
445
+ const printer = ts.createPrinter({});
446
+ const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
447
+ return raw.replace(/\s+/g, " ").slice(0, 500);
448
+ }
449
+ function getJsDoc(node, sourceFile) {
450
+ const fullText = sourceFile.getFullText();
451
+ const nodePos = node.getFullWidth();
452
+ const comments = ts.getLeadingCommentRanges(fullText, nodePos);
453
+ if (!comments) return "";
454
+ for (const range of comments) {
455
+ const commentText = fullText.slice(range.pos, range.end);
456
+ const trimmed = commentText.trim();
457
+ if (trimmed.startsWith("/**") && trimmed.endsWith("*/")) {
458
+ const inner = trimmed.slice(3, -2).replace(/^[ \t]*\*[ ]?/gm, "").trim();
459
+ return inner.split("\n")[0]?.trim().slice(0, 200) ?? "";
460
+ }
461
+ }
462
+ return "";
463
+ }
464
+ function buildScope(node) {
465
+ const parts = [];
466
+ let current = node.parent;
467
+ while (current) {
468
+ if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
469
+ parts.unshift(current.name?.text ?? "Anon");
470
+ } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
471
+ if (current.name && ts.isIdentifier(current.name)) {
472
+ parts.unshift(current.name.text);
473
+ }
474
+ }
475
+ current = current.parent;
476
+ }
477
+ return parts.join(".");
478
+ }
479
+ function parseSymbols(opts) {
480
+ const { file, content, lang } = opts;
481
+ let sourceFile;
482
+ try {
483
+ sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
484
+ } catch {
485
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
486
+ }
487
+ const symbols = [];
488
+ function visit(node) {
489
+ const kind = kindOf(node);
490
+ if (kind) {
491
+ const nameNode = node.name;
492
+ if (!nameNode || !ts.isIdentifier(nameNode)) return;
493
+ const name = nameNode.text;
494
+ const pos = nameNode.getStart(sourceFile);
495
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
496
+ const scope = buildScope(node);
497
+ const signature = getSignature(node, sourceFile);
498
+ const docComment = getJsDoc(node, sourceFile);
499
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
500
+ symbols.push({
501
+ id: 0,
502
+ lang,
503
+ kind,
504
+ name,
505
+ file,
506
+ line: line + 1,
507
+ col: character,
508
+ signature,
509
+ docComment,
510
+ scope,
511
+ text
512
+ });
513
+ }
514
+ ts.forEachChild(node, visit);
515
+ }
516
+ visit(sourceFile);
517
+ const refs = extractRefs(sourceFile);
518
+ return { file, lang, symbols, refs, mtimeMs: Date.now() };
519
+ }
520
+ function extractRefs(sourceFile) {
521
+ const refs = [];
522
+ function visit(node) {
523
+ const pos = node.getStart(sourceFile);
524
+ const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
525
+ const lineNum = line + 1;
526
+ if (ts.isCallExpression(node)) {
527
+ const expr = node.expression;
528
+ if (ts.isIdentifier(expr)) {
529
+ refs.push({ fromId: 0, toName: expr.text, callType: "call", line: lineNum });
530
+ }
531
+ } else if (ts.isPropertyAccessExpression(node)) {
532
+ if (ts.isIdentifier(node.expression)) {
533
+ refs.push({ fromId: 0, toName: node.expression.text, callType: "call", line: lineNum });
534
+ }
535
+ } else if (ts.isTypeReferenceNode(node)) {
536
+ const name = getTypeName(node.typeName);
537
+ if (name) refs.push({ fromId: 0, toName: name, callType: "type_ref", line: lineNum });
538
+ } else if (ts.isHeritageClause(node)) {
539
+ for (const t of node.types) {
540
+ const name = getTypeName(t.expression);
541
+ if (name) refs.push({ fromId: 0, toName: name, callType: node.token === ts.SyntaxKind.ExtendsKeyword ? "inherit" : "implement", line: lineNum });
542
+ }
543
+ } else if (ts.isImportDeclaration(node)) {
544
+ const moduleName = getModuleName(node);
545
+ if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
546
+ }
547
+ ts.forEachChild(node, visit);
548
+ }
549
+ visit(sourceFile);
550
+ return deduplicateRefs(refs);
551
+ }
552
+ function getTypeName(name) {
553
+ if (ts.isIdentifier(name)) return name.text;
554
+ if (ts.isQualifiedName(name)) return `${getTypeName(name.left)}.${name.right.text}`;
555
+ return "";
556
+ }
557
+ function getModuleName(node) {
558
+ const moduleSpecifier = node.moduleSpecifier;
559
+ if (ts.isStringLiteral(moduleSpecifier)) return moduleSpecifier.text;
560
+ return "";
561
+ }
562
+ function deduplicateRefs(refs) {
563
+ const seen = /* @__PURE__ */ new Set();
564
+ return refs.filter((r) => {
565
+ const key = `${r.toName}:${r.callType}:${r.line}`;
566
+ if (seen.has(key)) return false;
567
+ seen.add(key);
568
+ return true;
569
+ });
570
+ }
571
+ function detectLang(file) {
572
+ const idx = file.lastIndexOf(".");
573
+ if (idx < 0) return null;
574
+ return extToLang(file.slice(idx));
575
+ }
576
+ function parseSymbols2(opts) {
577
+ const { file, content, lang } = opts;
578
+ try {
579
+ return syncGoParse(file, content, lang);
580
+ } catch {
581
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
582
+ }
583
+ }
584
+ var GO_PARSE_SCRIPT = `
585
+ package main
586
+
587
+ import (
588
+ "encoding/json"
589
+ "fmt"
590
+ "go/ast"
591
+ "go/parser"
592
+ "go/token"
593
+ "os"
594
+ "strings"
595
+ )
596
+
597
+ type Sym struct {
598
+ Name string \`json:"name"\`
599
+ Kind string \`json:"kind"\`
600
+ Line int \`json:"line"\`
601
+ Col int \`json:"col"\`
602
+ Signature string \`json:"signature"\`
603
+ Scope string \`json:"scope"\`
604
+ }
605
+
606
+ func main() {
607
+ if len(os.Args) < 2 {
608
+ fmt.Print("[]")
609
+ return
610
+ }
611
+ src, err := os.ReadFile(os.Args[1])
612
+ if err != nil {
613
+ fmt.Print("[]")
614
+ return
615
+ }
616
+ fset := token.NewFileSet()
617
+ node, err := parser.ParseFile(fset, os.Args[1], src, 0)
618
+ if err != nil {
619
+ fmt.Print("[]")
620
+ return
621
+ }
622
+
623
+ var syms []Sym
624
+
625
+ // Package-level scope
626
+ pkgScope := node.Name.Name
627
+
628
+ // Collect all top-level declarations
629
+ for _, decl := range node.Decls {
630
+ switch d := decl.(type) {
631
+ case *ast.FuncDecl:
632
+ name := d.Name.Name
633
+ kind := "function"
634
+ scope := pkgScope
635
+ if d.Recv != nil && len(d.Recv.List) > 0 {
636
+ scope = pkgScope + "." + recvTypeName(d.Recv.List[0].Type) + "." + name
637
+ kind = "method"
638
+ } else {
639
+ scope = pkgScope + "." + name
640
+ }
641
+ pos := fset.Position(d.Pos())
642
+ sig := formatFuncSig(d)
643
+ syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: scope})
644
+
645
+ case *ast.GenDecl:
646
+ for _, spec := range d.Specs {
647
+ switch s := spec.(type) {
648
+ case *ast.TypeSpec:
649
+ name := s.Name.Name
650
+ pos := fset.Position(s.Pos())
651
+ sig := "type " + name
652
+ if s.TypeParams != nil {
653
+ sig += formatTypeParams(s.TypeParams)
654
+ }
655
+ if st, ok := s.Type.(*ast.StructType); ok {
656
+ sig += " = struct { " + formatFields(st.Fields.List) + " }"
657
+ } else if it, ok := s.Type.(*ast.InterfaceType); ok {
658
+ sig += " = interface { " + formatMethods(it.Methods.List) + " }"
659
+ } else {
660
+ sig += " = " + formatType(s.Type)
661
+ }
662
+ syms = append(syms, Sym{Name: name, Kind: "type", Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
663
+
664
+ case *ast.ValueSpec:
665
+ for _, n := range s.Names {
666
+ name := n.Name
667
+ pos := fset.Position(n.Pos())
668
+ kind := "var"
669
+ if d.Tok == token.CONST {
670
+ kind = "const"
671
+ }
672
+ sig := kind + " " + name
673
+ if s.Type != nil {
674
+ sig += " " + formatType(s.Type)
675
+ }
676
+ syms = append(syms, Sym{Name: name, Kind: kind, Line: pos.Line, Col: pos.Column, Signature: sig, Scope: pkgScope})
677
+ }
678
+ }
679
+ }
680
+ }
681
+ }
682
+
683
+ data, err := json.Marshal(syms)
684
+ if err != nil {
685
+ fmt.Print("[]")
686
+ return
687
+ }
688
+ fmt.Print(string(data))
689
+ }
690
+
691
+ func recvTypeName(t ast.Expr) string {
692
+ switch v := t.(type) {
693
+ case *ast.Ident:
694
+ return v.Name
695
+ case *ast.StarExpr:
696
+ return recvTypeName(v.X)
697
+ default:
698
+ return "?"
699
+ }
700
+ }
701
+
702
+ func formatFuncSig(d *ast.FuncDecl) string {
703
+ scope := ""
704
+ if d.Recv != nil && len(d.Recv.List) > 0 {
705
+ scope = "(" + formatFieldList(d.Recv.List) + ") "
706
+ }
707
+ scope += formatFuncType(d.Type)
708
+ return "func " + scope
709
+ }
710
+
711
+ func formatFuncType(f *ast.FuncType) string {
712
+ params := formatFieldList(f.Params.List)
713
+ results := ""
714
+ if f.Results != nil {
715
+ results = " -> " + formatFieldList(f.Results.List)
716
+ }
717
+ return params + results
718
+ }
719
+
720
+ func formatFieldList(fields []*ast.Field) string {
721
+ if len(fields) == 0 {
722
+ return "()"
723
+ }
724
+ names := make([]string, 0, len(fields))
725
+ for _, f := range fields {
726
+ name := ""
727
+ if len(f.Names) > 0 {
728
+ name = f.Names[0].Name
729
+ }
730
+ t := formatType(f.Type)
731
+ if name != "" {
732
+ names = append(names, name+" "+t)
733
+ } else {
734
+ names = append(names, t)
735
+ }
736
+ }
737
+ return "(" + strings.Join(names, ", ") + ")"
738
+ }
739
+
740
+ func formatFields(fields []*ast.Field) string {
741
+ lines := make([]string, 0)
742
+ for _, f := range fields {
743
+ name := ""
744
+ if len(f.Names) > 0 {
745
+ name = f.Names[0].Name
746
+ }
747
+ t := formatType(f.Type)
748
+ if name != "" {
749
+ lines = append(lines, name+" "+t)
750
+ } else {
751
+ lines = append(lines, t)
752
+ }
753
+ }
754
+ return strings.Join(lines, "; ")
755
+ }
756
+
757
+ func formatMethods(fields []*ast.Field) string {
758
+ return formatFields(fields)
759
+ }
760
+
761
+ func formatTypeParams(tp *ast.TypeParams) string {
762
+ if tp == nil || len(tp.List) == 0 {
763
+ return ""
764
+ }
765
+ params := make([]string, len(tp.List))
766
+ for i, p := range tp.List {
767
+ if len(p.Names) > 0 {
768
+ params[i] = p.Names[0].Name
769
+ } else {
770
+ params[i] = "T"
771
+ }
772
+ }
773
+ return "[" + strings.Join(params, ", ") + "]"
774
+ }
775
+
776
+ func formatType(t ast.Expr) string {
777
+ if t == nil {
778
+ return "?"
779
+ }
780
+ switch v := t.(type) {
781
+ case *ast.Ident:
782
+ return v.Name
783
+ case *ast.SelectorExpr:
784
+ return formatType(v.X) + "." + v.Sel.Name
785
+ case *ast.StarExpr:
786
+ return "*" + formatType(v.X)
787
+ case *ast.ArrayType:
788
+ if v.Len == nil {
789
+ return "[]" + formatType(v.Elt)
790
+ }
791
+ return "[...]" + formatType(v.Elt)
792
+ case *ast.MapType:
793
+ return "map[" + formatType(v.Key) + "]" + formatType(v.Value)
794
+ case *ast.InterfaceType:
795
+ return "interface{}"
796
+ case *ast.StructType:
797
+ return "struct{}"
798
+ case *ast.FuncType:
799
+ return formatFuncType(v)
800
+ case *ast.ChanType:
801
+ return "chan " + formatType(v.Value)
802
+ case *ast.BasicLit:
803
+ return v.Value
804
+ default:
805
+ return "?"
806
+ }
807
+ }
808
+ `;
809
+ function syncGoParse(filePath, _content, lang) {
810
+ const tmpDir = path3.join(process.env.TEMP ?? "/tmp", "ws-go-parse");
811
+ try {
812
+ mkdirSync(tmpDir, { recursive: true });
813
+ const scriptPath = path3.join(tmpDir, "parse.go");
814
+ writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
815
+ let stdout;
816
+ try {
817
+ stdout = execSync(`go run "${scriptPath}" "${filePath}"`, {
818
+ timeout: 15e3,
819
+ encoding: "utf8",
820
+ windowsHide: true
821
+ });
822
+ } finally {
823
+ try {
824
+ unlinkSync(scriptPath);
825
+ } catch {
826
+ }
827
+ }
828
+ if (!stdout.trim()) {
829
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
830
+ }
831
+ const raw = JSON.parse(stdout.trim());
832
+ const symbols = raw.map((s) => ({
833
+ id: 0,
834
+ lang,
835
+ kind: s.kind,
836
+ name: s.name,
837
+ file: filePath,
838
+ line: s.line,
839
+ col: s.col,
840
+ signature: s.signature ?? "",
841
+ docComment: "",
842
+ scope: s.scope ?? "",
843
+ text: `${s.name} ${s.signature ?? ""}`.trim()
844
+ }));
845
+ return { file: filePath, lang, symbols, mtimeMs: Date.now() };
846
+ } catch {
847
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
848
+ }
849
+ }
850
+ function parseSymbols3(opts) {
851
+ const { file, lang } = opts;
852
+ try {
853
+ return syncPyParse(file, lang);
854
+ } catch {
855
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
856
+ }
857
+ }
858
+ var PY_PARSE_SCRIPT = `import ast, json, sys, os
859
+
860
+ def get_name(node):
861
+ if isinstance(node, ast.Name):
862
+ return node.id
863
+ elif isinstance(node, ast.Attribute):
864
+ return get_name(node.value) + "." + node.attr
865
+ elif isinstance(node, ast.Subscript):
866
+ return get_name(node.value)
867
+ elif isinstance(node, ast.Call):
868
+ return get_name(node.func)
869
+ elif isinstance(node, ast.Constant):
870
+ return str(node.value)
871
+ return ""
872
+
873
+ def get_decorators(node):
874
+ decs = []
875
+ for dec in node.decorator_list:
876
+ decs.append(get_name(dec))
877
+ return decs
878
+
879
+ def get_bases(node):
880
+ bases = []
881
+ for base in node.bases:
882
+ bases.append(get_name(base))
883
+ return bases
884
+
885
+ def get_args(args):
886
+ parts = []
887
+ for arg in args.args:
888
+ parts.append(arg.arg)
889
+ return ", ".join(parts)
890
+
891
+ def get_returns(node):
892
+ if node.returns is None:
893
+ return ""
894
+ return get_name(node.returns)
895
+
896
+ class Sym:
897
+ def __init__(self, name, kind, line, col, signature, scope):
898
+ self.name = name
899
+ self.kind = kind
900
+ self.line = line
901
+ self.col = col
902
+ self.signature = signature
903
+ self.scope = scope
904
+ def to_dict(self):
905
+ return {
906
+ "name": self.name,
907
+ "kind": self.kind,
908
+ "line": self.line,
909
+ "col": self.col,
910
+ "signature": self.signature,
911
+ "scope": self.scope,
912
+ }
913
+
914
+ def is_private(name):
915
+ return name.startswith("__") and not name.endswith("__")
916
+
917
+ syms = []
918
+ errors = []
919
+
920
+ try:
921
+ with open(sys.argv[1], "r", encoding="utf-8") as f:
922
+ source = f.read()
923
+ tree = ast.parse(source, filename=sys.argv[1])
924
+ except Exception as e:
925
+ errors.append(str(e))
926
+ print("[]")
927
+ sys.exit(0)
928
+
929
+ # Module-level scope
930
+ module_scope = os.path.basename(sys.argv[1])[:-3] # strip .py
931
+
932
+ class ModuleVisitor(ast.NodeVisitor):
933
+ def __init__(self):
934
+ self.scope_stack = [module_scope]
935
+
936
+ def visit_ClassDef(self, node):
937
+ bases = get_bases(node)
938
+ decs = get_decorators(node)
939
+ sig = "class " + node.name
940
+ if bases:
941
+ sig += "(" + ", ".join(bases) + ")"
942
+ sig += ": ..."
943
+ syms.append(Sym(
944
+ name=node.name,
945
+ kind="class",
946
+ line=node.lineno,
947
+ col=node.col_offset,
948
+ signature=sig,
949
+ scope=".".join(self.scope_stack) + "." + node.name,
950
+ ))
951
+ self.scope_stack.append(node.name)
952
+ self.generic_visit(node)
953
+ self.scope_stack.pop()
954
+
955
+ def visit_FunctionDef(self, node):
956
+ decs = get_decorators(node)
957
+ args = get_args(node.args)
958
+ returns = get_returns(node)
959
+ is_async = isinstance(node, ast.AsyncFunctionDef)
960
+
961
+ kind = "function"
962
+ prefix = "def "
963
+ if decs:
964
+ for d in decs:
965
+ if d.endswith(".staticmethod"):
966
+ kind = "staticmethod"
967
+ elif d.endswith(".classmethod"):
968
+ kind = "classmethod"
969
+ elif d == "property":
970
+ kind = "property"
971
+
972
+ if is_async:
973
+ kind = "async_" + kind
974
+
975
+ sig = f"{prefix}{node.name}({args})"
976
+ if returns:
977
+ sig += f" -> {returns}"
978
+ scope = ".".join(self.scope_stack) + "." + node.name
979
+
980
+ syms.append(Sym(
981
+ name=node.name,
982
+ kind=kind,
983
+ line=node.lineno,
984
+ col=node.col_offset,
985
+ signature=sig,
986
+ scope=scope,
987
+ ))
988
+ # Don't descend into function bodies to avoid local symbols
989
+ # self.generic_visit(node)
990
+
991
+ def visit_AsyncFunctionDef(self, node):
992
+ # Treat as function
993
+ self.visit_FunctionDef(node)
994
+
995
+ def visit_Assign(self, node):
996
+ for target in node.targets:
997
+ if isinstance(target, ast.Name):
998
+ name = target.id
999
+ if is_private(name):
1000
+ continue
1001
+ # Infer constness from UPPER_CASE naming
1002
+ kind = "const" if name.isupper() else "var"
1003
+ col = target.col_offset if hasattr(target, 'col_offset') else 0
1004
+ syms.append(Sym(
1005
+ name=name,
1006
+ kind=kind,
1007
+ line=node.lineno,
1008
+ col=col,
1009
+ signature=f"{name} = ...",
1010
+ scope=".".join(self.scope_stack),
1011
+ ))
1012
+
1013
+ def visit_AnnAssign(self, node):
1014
+ if isinstance(node.target, ast.Name):
1015
+ name = node.target.id
1016
+ if is_private(name):
1017
+ return
1018
+ kind = "const" if name.isupper() else "var"
1019
+ col = node.target.col_offset if hasattr(node.target, 'col_offset') else 0
1020
+ sig = f"{name}: {get_name(node.annotation)}"
1021
+ if node.value:
1022
+ sig += " = ..."
1023
+ syms.append(Sym(
1024
+ name=name,
1025
+ kind=kind,
1026
+ line=node.lineno,
1027
+ col=col,
1028
+ signature=sig,
1029
+ scope=".".join(self.scope_stack),
1030
+ ))
1031
+
1032
+ def visit_Import(self, node):
1033
+ for alias in node.names:
1034
+ name = alias.asname or alias.name
1035
+ syms.append(Sym(
1036
+ name=name,
1037
+ kind="import",
1038
+ line=node.lineno,
1039
+ col=node.col_offset,
1040
+ signature=f"import {alias.name}",
1041
+ scope=".".join(self.scope_stack),
1042
+ ))
1043
+
1044
+ def visit_ImportFrom(self, node):
1045
+ module = node.module or ""
1046
+ for alias in node.names:
1047
+ name = alias.asname or alias.name
1048
+ syms.append(Sym(
1049
+ name=name,
1050
+ kind="import",
1051
+ line=node.lineno,
1052
+ col=node.col_offset,
1053
+ signature=f"from {module} import {alias.name}",
1054
+ scope=".".join(self.scope_stack),
1055
+ ))
1056
+
1057
+ visitor = ModuleVisitor()
1058
+ visitor.visit(tree)
1059
+
1060
+ print(json.dumps([s.to_dict() for s in syms]))
1061
+ `;
1062
+ function syncPyParse(filePath, lang) {
1063
+ try {
1064
+ const stdout = execSync(`python -c "${PY_PARSE_SCRIPT.replace(/"/g, '\\"')}" "${filePath}"`, {
1065
+ timeout: 15e3,
1066
+ encoding: "utf8",
1067
+ windowsHide: true
1068
+ });
1069
+ if (!stdout.trim()) {
1070
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1071
+ }
1072
+ const raw = JSON.parse(stdout.trim());
1073
+ const symbols = raw.map((s) => ({
1074
+ id: 0,
1075
+ lang,
1076
+ kind: s.kind,
1077
+ name: s.name,
1078
+ file: filePath,
1079
+ line: s.line,
1080
+ col: s.col,
1081
+ signature: s.signature ?? "",
1082
+ docComment: "",
1083
+ scope: s.scope ?? "",
1084
+ text: `${s.name} ${s.signature ?? ""}`.trim()
1085
+ }));
1086
+ return { file: filePath, lang, symbols, mtimeMs: Date.now() };
1087
+ } catch {
1088
+ return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1089
+ }
1090
+ }
1091
+ function parseSymbols4(opts) {
1092
+ const { file, content, lang } = opts;
1093
+ const nativeAvailable = checkNativeParser();
1094
+ if (nativeAvailable) {
1095
+ const result = tryNativeParse(file, content);
1096
+ if (result) return result;
1097
+ }
1098
+ return regexParse({ file, content, lang });
1099
+ }
1100
+ function checkNativeParser() {
1101
+ try {
1102
+ execSync("rustc --version", { stdio: "pipe" });
1103
+ const toolsDir = path3.join(process.cwd(), "tools");
1104
+ try {
1105
+ execSync("cargo metadata --no-deps --format-version 1 --manifest-path " + path3.join(toolsDir, "Cargo.toml"), { stdio: "pipe" });
1106
+ return true;
1107
+ } catch {
1108
+ return false;
1109
+ }
1110
+ } catch {
1111
+ return false;
1112
+ }
1113
+ }
1114
+ function tryNativeParse(file, content) {
1115
+ try {
1116
+ const toolsDir = path3.join(process.cwd(), "tools");
1117
+ const crateDir = path3.join(toolsDir, "syn-parser");
1118
+ const tmpFile = path3.join(crateDir, "src", "input.rs");
1119
+ const { writeFileSync: writeFileSync2 } = __require("node:fs");
1120
+ writeFileSync2(tmpFile, content, "utf8");
1121
+ const result = spawnSync("cargo", ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")], {
1122
+ cwd: process.cwd(),
1123
+ encoding: "utf8",
1124
+ timeout: 15e3,
1125
+ stdio: ["pipe", "pipe", "pipe"]
1126
+ });
1127
+ if (result.status === 0 && result.stdout) {
1128
+ const symbols = JSON.parse(result.stdout);
1129
+ return {
1130
+ file,
1131
+ lang: "rs",
1132
+ symbols: symbols.map((s) => ({ ...s, id: 0, lang: "rs" })),
1133
+ mtimeMs: Date.now()
1134
+ };
1135
+ }
1136
+ } catch {
1137
+ }
1138
+ return null;
1139
+ }
1140
+ var RS_PATTERNS = [
1141
+ { regex: /fn\s+(\w+)\s*\([^)]*\)/g, kind: "function" },
1142
+ { regex: /struct\s+(\w+)/g, kind: "struct" },
1143
+ { regex: /enum\s+(\w+)/g, kind: "enum" },
1144
+ { regex: /trait\s+(\w+)/g, kind: "trait" },
1145
+ { regex: /impl\s+(?:<[^>]+>)?(\w+)/g, kind: "impl" },
1146
+ { regex: /type\s+(\w+)\s*=/g, kind: "type" },
1147
+ { regex: /const\s+(\w+)/g, kind: "const" },
1148
+ { regex: /static\s+(\w+)/g, kind: "static" },
1149
+ { regex: /mod\s+(\w+)/g, kind: "mod" }
1150
+ ];
1151
+ function regexParse(opts) {
1152
+ const { file, content, lang } = opts;
1153
+ const symbols = [];
1154
+ const lines = content.split("\n");
1155
+ const lineOffsets = [0];
1156
+ for (let i = 0; i < lines.length; i++) {
1157
+ lineOffsets.push(lineOffsets[i] + lines[i].length + 1);
1158
+ }
1159
+ function lineFromOffset(offset) {
1160
+ let lo = 0, hi = lineOffsets.length - 1;
1161
+ while (lo < hi) {
1162
+ const mid = lo + hi + 1 >>> 1;
1163
+ if (lineOffsets[mid] <= offset) lo = mid;
1164
+ else hi = mid - 1;
1165
+ }
1166
+ return lo + 1;
1167
+ }
1168
+ function extractDeclaration(lineIdx, match) {
1169
+ const line = lines[lineIdx] ?? "";
1170
+ return line.trim().slice(0, 500);
1171
+ }
1172
+ for (const pattern of RS_PATTERNS) {
1173
+ pattern.regex.lastIndex = 0;
1174
+ let match;
1175
+ while ((match = pattern.regex.exec(content)) !== null) {
1176
+ const name = match[1];
1177
+ const offset = match.index;
1178
+ const line = lineFromOffset(offset);
1179
+ const col = offset - (lineOffsets[line - 1] ?? 0);
1180
+ const lineIdx = line - 1;
1181
+ const signature = extractDeclaration(lineIdx);
1182
+ symbols.push({
1183
+ id: 0,
1184
+ lang,
1185
+ kind: pattern.kind,
1186
+ name,
1187
+ file,
1188
+ line,
1189
+ col,
1190
+ signature,
1191
+ docComment: "",
1192
+ scope: "",
1193
+ text: `${name} ${signature}`.trim()
1194
+ });
1195
+ }
1196
+ }
1197
+ const seen = /* @__PURE__ */ new Set();
1198
+ const deduped = symbols.filter((s) => {
1199
+ const key = `${s.name}:${s.line}`;
1200
+ if (seen.has(key)) return false;
1201
+ seen.add(key);
1202
+ return true;
1203
+ });
1204
+ return { file, lang, symbols: deduped, mtimeMs: Date.now() };
1205
+ }
1206
+ function parseSymbols5(opts) {
1207
+ const { file, content, lang } = opts;
1208
+ try {
1209
+ return regexParse2({ file, content, lang });
1210
+ } catch {
1211
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
1212
+ }
1213
+ }
1214
+ function regexParse2(opts) {
1215
+ const { file, content, lang } = opts;
1216
+ const symbols = [];
1217
+ const basename2 = path3.basename(file).toLowerCase();
1218
+ const isPackageJson = basename2 === "package.json";
1219
+ const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
1220
+ const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
1221
+ const isOpenApi = content.includes("openapi") || content.includes("swagger");
1222
+ const lines = content.split("\n");
1223
+ const lineOffsets = [0];
1224
+ for (let i = 0; i < lines.length; i++) {
1225
+ lineOffsets.push(lineOffsets[i] + lines[i].length + 1);
1226
+ }
1227
+ function lineFromOffset(offset) {
1228
+ let lo = 0, hi = lineOffsets.length - 1;
1229
+ while (lo < hi) {
1230
+ const mid = lo + hi + 1 >>> 1;
1231
+ if (lineOffsets[mid] <= offset) lo = mid;
1232
+ else hi = mid - 1;
1233
+ }
1234
+ return lo + 1;
1235
+ }
1236
+ const rootMatch = content.match(/^\s*\{/m);
1237
+ if (rootMatch) {
1238
+ const offset = rootMatch.index;
1239
+ const line = lineFromOffset(offset);
1240
+ symbols.push(makeSymbol({
1241
+ name: path3.basename(file),
1242
+ kind: "object",
1243
+ line,
1244
+ col: 0,
1245
+ signature: `"${path3.basename(file)}" = { ... }`,
1246
+ file,
1247
+ lang
1248
+ }));
1249
+ }
1250
+ const topLevelKeyRegex = /^\s*"([^"]+)"\s*:/gm;
1251
+ let match;
1252
+ while ((match = topLevelKeyRegex.exec(content)) !== null) {
1253
+ const key = match[1];
1254
+ const offset = match.index;
1255
+ const line = lineFromOffset(offset);
1256
+ const col = offset - (lineOffsets[line - 1] ?? 0);
1257
+ let kind = "property";
1258
+ let signature = `"${key}": ..."`;
1259
+ if (isPackageJson) {
1260
+ if (key === "scripts" || key === "dependencies" || key === "devDependencies" || key === "peerDependencies" || key === "optionalDependencies") {
1261
+ kind = "const";
1262
+ signature = `"${key}": { ... }`;
1263
+ }
1264
+ } else if (isTsconfig) {
1265
+ if (key === "compilerOptions") {
1266
+ kind = "property";
1267
+ signature = `"compilerOptions": { ... }`;
1268
+ }
1269
+ }
1270
+ if (isJsonSchema || isOpenApi) {
1271
+ if (key === "$schema" || key === "$id") {
1272
+ kind = "schema";
1273
+ signature = `"${key}": "..."`;
1274
+ } else if (key === "$ref") {
1275
+ kind = "schema";
1276
+ signature = `"$ref": "..."`;
1277
+ }
1278
+ }
1279
+ symbols.push(makeSymbol({
1280
+ name: key,
1281
+ kind,
1282
+ line,
1283
+ col,
1284
+ signature,
1285
+ file,
1286
+ lang
1287
+ }));
1288
+ if (isPackageJson && key === "scripts") {
1289
+ extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFromOffset);
1290
+ }
1291
+ if (isTsconfig && key === "compilerOptions") {
1292
+ extractCompilerOptions(content, symbols, file, lang, lineOffsets, line, lineFromOffset);
1293
+ }
1294
+ }
1295
+ const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
1296
+ let defsMatch;
1297
+ while ((defsMatch = defsRegex.exec(content)) !== null) {
1298
+ const offset = defsMatch.index;
1299
+ const line = lineFromOffset(offset);
1300
+ symbols.push(makeSymbol({
1301
+ name: "$defs",
1302
+ kind: "property",
1303
+ line,
1304
+ col: offset - (lineOffsets[line - 1] ?? 0),
1305
+ signature: '"$defs": { ... }',
1306
+ file,
1307
+ lang
1308
+ }));
1309
+ break;
1310
+ }
1311
+ const defsPatterns = [
1312
+ /"\$defs"\s*:/g,
1313
+ /"definitions"\s*:/g,
1314
+ /"components"\s*:/g,
1315
+ /"schemas"\s*:/g
1316
+ ];
1317
+ for (const pat of defsPatterns) {
1318
+ pat.lastIndex = 0;
1319
+ while ((match = pat.exec(content)) !== null) {
1320
+ const offset = match.index;
1321
+ const line = lineFromOffset(offset);
1322
+ const key = match[0].match(/"([^"]+)"/)?.[1] ?? match[0];
1323
+ symbols.push(makeSymbol({
1324
+ name: key,
1325
+ kind: "property",
1326
+ line,
1327
+ col: offset - (lineOffsets[line - 1] ?? 0),
1328
+ signature: `"${key}": { ... }`,
1329
+ file,
1330
+ lang
1331
+ }));
1332
+ }
1333
+ }
1334
+ return { file, lang, symbols, mtimeMs: Date.now() };
1335
+ }
1336
+ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFromOffset) {
1337
+ const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
1338
+ let match;
1339
+ while ((match = scriptsBlockRegex.exec(content)) !== null) {
1340
+ const blockContent = match[0];
1341
+ const blockOffset = match.index;
1342
+ const scriptKeyRegex = /"(\w[\w-]*)"\s*:/g;
1343
+ let scriptMatch;
1344
+ while ((scriptMatch = scriptKeyRegex.exec(blockContent)) !== null) {
1345
+ const key = scriptMatch[1];
1346
+ const keyOffset = blockOffset + scriptMatch.index;
1347
+ const line = lineFromOffset(keyOffset);
1348
+ symbols.push(makeSymbol({
1349
+ name: key,
1350
+ kind: "function",
1351
+ line,
1352
+ col: keyOffset - (lineOffsets[line - 1] ?? 0),
1353
+ signature: `"${key}": "..."`,
1354
+ file,
1355
+ lang
1356
+ }));
1357
+ }
1358
+ }
1359
+ }
1360
+ function extractCompilerOptions(content, symbols, file, lang, lineOffsets, parentLine, lineFromOffset) {
1361
+ const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
1362
+ let match;
1363
+ while ((match = optsBlockRegex.exec(content)) !== null) {
1364
+ const blockContent = match[0];
1365
+ const blockOffset = match.index;
1366
+ const optKeyRegex = /"(\w[\w]*)"\s*:/g;
1367
+ let optMatch;
1368
+ while ((optMatch = optKeyRegex.exec(blockContent)) !== null) {
1369
+ const key = optMatch[1];
1370
+ const keyOffset = blockOffset + optMatch.index;
1371
+ const line = lineFromOffset(keyOffset);
1372
+ if (line <= parentLine) continue;
1373
+ symbols.push(makeSymbol({
1374
+ name: key,
1375
+ kind: "property",
1376
+ line,
1377
+ col: keyOffset - (lineOffsets[line - 1] ?? 0),
1378
+ signature: `"${key}": ...`,
1379
+ file,
1380
+ lang
1381
+ }));
1382
+ }
1383
+ }
1384
+ }
1385
+ function makeSymbol(opts) {
1386
+ return {
1387
+ id: 0,
1388
+ lang: opts.lang,
1389
+ kind: opts.kind,
1390
+ name: opts.name,
1391
+ file: opts.file,
1392
+ line: opts.line,
1393
+ col: opts.col,
1394
+ signature: opts.signature,
1395
+ docComment: "",
1396
+ scope: "",
1397
+ text: `${opts.name} ${opts.signature}`.trim()
1398
+ };
1399
+ }
1400
+
1401
+ // src/codebase-index/yaml-parser.ts
1402
+ function parseSymbols6(opts) {
1403
+ const { file, content, lang } = opts;
1404
+ try {
1405
+ return regexParse3({ file, content, lang });
1406
+ } catch {
1407
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
1408
+ }
1409
+ }
1410
+ function regexParse3(opts) {
1411
+ const { file, content, lang } = opts;
1412
+ const symbols = [];
1413
+ const lines = content.split("\n");
1414
+ const lineOffsets = [0];
1415
+ for (let i = 0; i < lines.length; i++) {
1416
+ lineOffsets.push(lineOffsets[i] + lines[i].length + 1);
1417
+ }
1418
+ function lineFromOffset(offset) {
1419
+ let lo = 0, hi = lineOffsets.length - 1;
1420
+ while (lo < hi) {
1421
+ const mid = lo + hi + 1 >>> 1;
1422
+ if (lineOffsets[mid] <= offset) lo = mid;
1423
+ else hi = mid - 1;
1424
+ }
1425
+ return lo + 1;
1426
+ }
1427
+ const anchorRegex = /&(\w[\w-]*)/g;
1428
+ let match;
1429
+ while ((match = anchorRegex.exec(content)) !== null) {
1430
+ const name = match[1];
1431
+ const offset = match.index;
1432
+ const line = lineFromOffset(offset);
1433
+ const col = offset - (lineOffsets[line - 1] ?? 0);
1434
+ symbols.push(makeSymbol2({
1435
+ name,
1436
+ kind: "const",
1437
+ line,
1438
+ col,
1439
+ signature: `&${name}`,
1440
+ file,
1441
+ lang
1442
+ }));
1443
+ }
1444
+ const aliasRegex = /\*(\w[\w-]*)/g;
1445
+ while ((match = aliasRegex.exec(content)) !== null) {
1446
+ const name = match[1];
1447
+ const offset = match.index;
1448
+ const line = lineFromOffset(offset);
1449
+ const col = offset - (lineOffsets[line - 1] ?? 0);
1450
+ symbols.push(makeSymbol2({
1451
+ name,
1452
+ kind: "const",
1453
+ line,
1454
+ col,
1455
+ signature: `*${name}`,
1456
+ file,
1457
+ lang
1458
+ }));
1459
+ }
1460
+ const kvRegex = /^(\s*)([^:#\s][^:#\s]*)\s*:/gm;
1461
+ while ((match = kvRegex.exec(content)) !== null) {
1462
+ const indent = match[1].length;
1463
+ const key = match[2];
1464
+ const offset = match.index;
1465
+ const line = lineFromOffset(offset);
1466
+ const col = offset - (lineOffsets[line - 1] ?? 0);
1467
+ const lineContent = lines[line - 1] ?? "";
1468
+ if (/^[|&>]/.test(lineContent.trim())) continue;
1469
+ if (key === "---" || key === "...") continue;
1470
+ if (indent > 12) continue;
1471
+ const value = extractValue(content, match.index);
1472
+ const kind = isScalar(value) ? "literal" : "property";
1473
+ const signature = `${key}: ${truncate(value, 60)}`;
1474
+ symbols.push(makeSymbol2({ name: key, kind, line, col, signature, file, lang }));
1475
+ }
1476
+ const listItemRegex = /^-(\s+)([^:#\s][^:#\s]*)\s*:/gm;
1477
+ while ((match = listItemRegex.exec(content)) !== null) {
1478
+ const key = match[2];
1479
+ const offset = match.index;
1480
+ const line = lineFromOffset(offset);
1481
+ const col = offset - (lineOffsets[line - 1] ?? 0);
1482
+ const value = extractValue(content, offset + match[0].length);
1483
+ const kind = isScalar(value) ? "literal" : "property";
1484
+ symbols.push(makeSymbol2({
1485
+ name: key,
1486
+ kind,
1487
+ line,
1488
+ col,
1489
+ signature: `- ${key}: ${truncate(value, 60)}`,
1490
+ file,
1491
+ lang
1492
+ }));
1493
+ }
1494
+ const blockScalarRegex = /^(\s*)([^:#\s][^:#\s]*)\s*:\s*[|>](\s|$)/gm;
1495
+ while ((match = blockScalarRegex.exec(content)) !== null) {
1496
+ const key = match[2];
1497
+ const offset = match.index;
1498
+ const line = lineFromOffset(offset);
1499
+ const col = offset - (lineOffsets[line - 1] ?? 0);
1500
+ symbols.push(makeSymbol2({
1501
+ name: key,
1502
+ kind: "property",
1503
+ line,
1504
+ col,
1505
+ signature: `${key}: | ...`,
1506
+ file,
1507
+ lang
1508
+ }));
1509
+ }
1510
+ return { file, lang, symbols, mtimeMs: Date.now() };
1511
+ }
1512
+ function extractValue(content, afterColonOffset) {
1513
+ const lineEnd = content.indexOf("\n", afterColonOffset);
1514
+ const rest = content.slice(afterColonOffset, lineEnd < 0 ? void 0 : lineEnd);
1515
+ return rest.trim();
1516
+ }
1517
+ function isScalar(value) {
1518
+ if (!value) return false;
1519
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value)) return true;
1520
+ if (/^(true|false|null|undefined)$/i.test(value)) return true;
1521
+ if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
1522
+ return false;
1523
+ }
1524
+ function truncate(s, max) {
1525
+ if (s.length <= max) return s;
1526
+ return s.slice(0, max) + "...";
1527
+ }
1528
+ function makeSymbol2(opts) {
1529
+ return {
1530
+ id: 0,
1531
+ lang: opts.lang,
1532
+ kind: opts.kind,
1533
+ name: opts.name,
1534
+ file: opts.file,
1535
+ line: opts.line,
1536
+ col: opts.col,
1537
+ signature: opts.signature,
1538
+ docComment: "",
1539
+ scope: "",
1540
+ text: `${opts.name} ${opts.signature}`.trim()
1541
+ };
1542
+ }
1543
+
1544
+ // src/codebase-index/indexer.ts
1545
+ var DEFAULT_IGNORE = [
1546
+ "node_modules",
1547
+ ".git",
1548
+ "dist",
1549
+ "build",
1550
+ ".next",
1551
+ "coverage",
1552
+ ".turbo",
1553
+ "__snapshots__",
1554
+ ".nyc_output"
1555
+ ];
1556
+ async function findSourceFiles(projectRoot, ignore) {
1557
+ const results = [];
1558
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
1559
+ const globs = [
1560
+ { ext: ".ts", pat: compileGlob("**/*.ts") },
1561
+ { ext: ".tsx", pat: compileGlob("**/*.tsx") },
1562
+ { ext: ".js", pat: compileGlob("**/*.js") },
1563
+ { ext: ".jsx", pat: compileGlob("**/*.jsx") },
1564
+ { ext: ".go", pat: compileGlob("**/*.go") },
1565
+ { ext: ".py", pat: compileGlob("**/*.py") },
1566
+ { ext: ".rs", pat: compileGlob("**/*.rs") },
1567
+ { ext: ".json", pat: compileGlob("**/*.json") },
1568
+ { ext: ".yaml", pat: compileGlob("**/*.yaml") },
1569
+ { ext: ".yml", pat: compileGlob("**/*.yml") }
1570
+ ];
1571
+ const walk = async (dir) => {
1572
+ let entries;
1573
+ try {
1574
+ entries = await fs2.readdir(dir, { withFileTypes: true });
1575
+ } catch {
1576
+ return;
1577
+ }
1578
+ for (const e of entries) {
1579
+ if (ignoreSet.has(e.name)) continue;
1580
+ const full = path3.join(dir, e.name);
1581
+ if (e.isDirectory()) {
1582
+ await walk(full);
1583
+ } else if (e.isFile()) {
1584
+ const rel = path3.relative(projectRoot, full).replace(/\\/g, "/");
1585
+ const ext = path3.extname(e.name);
1586
+ for (const { ext: extName, pat } of globs) {
1587
+ if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
1588
+ results.push(full);
1589
+ break;
1590
+ }
1591
+ }
1592
+ }
1593
+ }
1594
+ };
1595
+ await walk(projectRoot);
1596
+ return results;
1597
+ }
1598
+ async function parseFile(file, content, lang) {
1599
+ switch (lang) {
1600
+ case "ts":
1601
+ case "tsx":
1602
+ case "js":
1603
+ case "jsx":
1604
+ return parseSymbols({ file, content, lang });
1605
+ case "go":
1606
+ return parseSymbols2({ file, content, lang: "go" });
1607
+ case "py":
1608
+ return parseSymbols3({ file, lang: "py" });
1609
+ case "rs":
1610
+ return parseSymbols4({ file, content, lang: "rs" });
1611
+ case "json":
1612
+ return parseSymbols5({ file, content, lang: "json" });
1613
+ case "yaml":
1614
+ return parseSymbols6({ file, content, lang: "yaml" });
1615
+ default:
1616
+ return { file, lang, symbols: [], mtimeMs: Date.now() };
1617
+ }
1618
+ }
1619
+ async function runIndexer(ctx, opts) {
1620
+ const { projectRoot, force = false, langs, ignore = [] } = opts;
1621
+ const store = new IndexStore(projectRoot);
1622
+ const startMs = Date.now();
1623
+ const errors = [];
1624
+ const langStats = {};
1625
+ let filesIndexed = 0;
1626
+ let symbolsIndexed = 0;
1627
+ let files;
1628
+ if (opts.files && opts.files.length > 0) {
1629
+ files = opts.files.map((f) => path3.resolve(projectRoot, f));
1630
+ } else {
1631
+ files = await findSourceFiles(projectRoot, ignore);
1632
+ }
1633
+ if (langs && langs.length > 0) {
1634
+ const langSet = new Set(langs);
1635
+ files = files.filter((f) => {
1636
+ const lang = detectLang(f);
1637
+ return lang ? langSet.has(lang) : false;
1638
+ });
1639
+ }
1640
+ if (force) store.clearAll();
1641
+ const existingMeta = /* @__PURE__ */ new Map();
1642
+ if (!force) {
1643
+ for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
1644
+ }
1645
+ for (const file of files) {
1646
+ let stat2;
1647
+ try {
1648
+ stat2 = await fs2.stat(file);
1649
+ } catch {
1650
+ store.deleteFile(file);
1651
+ continue;
1652
+ }
1653
+ if (!stat2.isFile()) continue;
1654
+ const lang = detectLang(file);
1655
+ if (!lang) continue;
1656
+ const meta = existingMeta.get(file);
1657
+ if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
1658
+ langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
1659
+ symbolsIndexed += meta.symbolCount;
1660
+ filesIndexed++;
1661
+ continue;
1662
+ }
1663
+ store.deleteSymbolsForFile(file);
1664
+ store.deleteRefsForFile(file);
1665
+ let content;
1666
+ try {
1667
+ content = await fs2.readFile(file, "utf8");
1668
+ } catch (e) {
1669
+ errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
1670
+ continue;
1671
+ }
1672
+ let parsed;
1673
+ try {
1674
+ parsed = await parseFile(file, content, lang);
1675
+ } catch (e) {
1676
+ errors.push(`parse error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
1677
+ continue;
1678
+ }
1679
+ if (parsed.symbols.length === 0) {
1680
+ store.upsertFile({
1681
+ file,
1682
+ lang,
1683
+ mtimeMs: Math.floor(stat2.mtimeMs),
1684
+ symbolCount: 0,
1685
+ lastIndexed: Date.now()
1686
+ });
1687
+ filesIndexed++;
1688
+ continue;
1689
+ }
1690
+ const nextId = store.getStats().totalSymbols + 1;
1691
+ const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
1692
+ store.insertSymbols(symbolsWithIds, nextId);
1693
+ const count = symbolsWithIds.length;
1694
+ symbolsIndexed += count;
1695
+ langStats[lang] = (langStats[lang] ?? 0) + count;
1696
+ if (parsed.refs && parsed.refs.length > 0) {
1697
+ for (let i = 0; i < symbolsWithIds.length; i++) {
1698
+ const sym = symbolsWithIds[i];
1699
+ const symRefs = parsed.refs.filter((r) => r.line === sym.line);
1700
+ if (symRefs.length > 0) {
1701
+ const refsWithFromId = symRefs.map((r) => ({ ...r, fromId: sym.id }));
1702
+ store.insertRefs(sym.id, refsWithFromId);
1703
+ }
1704
+ }
1705
+ }
1706
+ store.upsertFile({
1707
+ file,
1708
+ lang,
1709
+ mtimeMs: Math.floor(stat2.mtimeMs),
1710
+ symbolCount: count,
1711
+ lastIndexed: Date.now()
1712
+ });
1713
+ filesIndexed++;
1714
+ }
1715
+ for (const [file_] of existingMeta) {
1716
+ try {
1717
+ await fs2.stat(file_);
1718
+ } catch {
1719
+ store.deleteFile(file_);
1720
+ }
1721
+ }
1722
+ const durationMs = Date.now() - startMs;
1723
+ store.setLastIndexed(Date.now());
1724
+ store.close();
1725
+ return {
1726
+ filesIndexed,
1727
+ symbolsIndexed,
1728
+ langStats,
1729
+ durationMs,
1730
+ errors
1731
+ };
1732
+ }
1733
+
1734
+ // src/codebase-index/codebase-index-tool.ts
1735
+ var codebaseIndexTool = {
1736
+ name: "codebase-index",
1737
+ category: "Project",
1738
+ description: "Build or update the symbol index for the project. Runs incrementally by default \u2014 only re-indexes files that changed since the last run.",
1739
+ usageHint: "Call with `force: true` to wipe and rebuild the index from scratch. Use `langs` to limit to specific languages. First call without arguments to do an incremental index.",
1740
+ permission: "auto",
1741
+ mutating: true,
1742
+ timeoutMs: 12e4,
1743
+ inputSchema: {
1744
+ type: "object",
1745
+ properties: {
1746
+ force: {
1747
+ type: "boolean",
1748
+ description: "Force a full reindex \u2014 clears the index first and reindexes all files."
1749
+ },
1750
+ langs: {
1751
+ type: "array",
1752
+ items: { type: "string" },
1753
+ description: "Limit reindex to specific languages: ts, tsx, js, jsx, go, py, rs"
1754
+ }
1755
+ }
1756
+ },
1757
+ async execute(input, ctx) {
1758
+ const result = await runIndexer(ctx, {
1759
+ projectRoot: ctx.projectRoot,
1760
+ force: input.force ?? false,
1761
+ langs: input.langs
1762
+ });
1763
+ return result;
1764
+ }
1765
+ };
1766
+
1767
+ // src/codebase-index/bm25.ts
1768
+ var K1 = 1.5;
1769
+ var B = 0.75;
1770
+ function tokenise(text) {
1771
+ const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
1772
+ return sanitised.toLowerCase().split(" ").filter(Boolean);
1773
+ }
1774
+ function splitName(name) {
1775
+ return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_\-]+/g, " ").trim();
1776
+ }
1777
+ function buildIndexableText(name, signature, docComment) {
1778
+ return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
1779
+ }
1780
+ function buildBm25Index(docs) {
1781
+ const documents = docs.map((d) => {
1782
+ const tokens = tokenise(d.text);
1783
+ return { id: d.id, tokens, raw: d.text, len: tokens.length };
1784
+ });
1785
+ const df = {};
1786
+ for (const doc of documents) {
1787
+ const seen = /* @__PURE__ */ new Set();
1788
+ for (const t of doc.tokens) {
1789
+ if (!seen.has(t)) {
1790
+ df[t] = (df[t] ?? 0) + 1;
1791
+ seen.add(t);
1792
+ }
1793
+ }
1794
+ }
1795
+ const N = documents.length;
1796
+ const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
1797
+ const avgLen = N === 0 ? 0 : totalLen / N;
1798
+ return new Bm25Index(documents, df, N, avgLen);
1799
+ }
1800
+ var Bm25Index = class {
1801
+ constructor(documents, df, N, avgLen) {
1802
+ this.documents = documents;
1803
+ this.df = df;
1804
+ this.N = N;
1805
+ this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
1806
+ }
1807
+ documents;
1808
+ df;
1809
+ N;
1810
+ safeAvgLen;
1811
+ score(query, filter) {
1812
+ const qTokens = tokenise(query);
1813
+ if (qTokens.length === 0) return [];
1814
+ const results = [];
1815
+ for (const doc of this.documents) {
1816
+ if (filter && !filter(doc.id)) continue;
1817
+ let docScore = 0;
1818
+ for (const qTerm of qTokens) {
1819
+ let tf = 0;
1820
+ for (const t of doc.tokens) {
1821
+ if (t === qTerm) tf++;
1822
+ }
1823
+ if (tf === 0) continue;
1824
+ const dfVal = this.df[qTerm] ?? 0;
1825
+ if (dfVal === 0) continue;
1826
+ const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
1827
+ const lenRatio = B * (doc.len / this.safeAvgLen);
1828
+ const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
1829
+ docScore += idf * tfComponent;
1830
+ }
1831
+ if (docScore > 0) results.push({ id: doc.id, score: docScore });
1832
+ }
1833
+ return results;
1834
+ }
1835
+ getDoc(id) {
1836
+ return this.documents.find((d) => d.id === id);
1837
+ }
1838
+ extractSnippet(docId, queryTokens, radius = 40) {
1839
+ const doc = this.getDoc(docId);
1840
+ if (!doc) return "";
1841
+ for (const tok of queryTokens) {
1842
+ const idx = doc.raw.toLowerCase().indexOf(tok);
1843
+ if (idx !== -1) {
1844
+ const start = Math.max(0, idx - radius);
1845
+ const end = Math.min(doc.raw.length, idx + tok.length + radius);
1846
+ const excerpt = doc.raw.slice(start, end);
1847
+ const ellipsis = "\u2026";
1848
+ return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
1849
+ }
1850
+ }
1851
+ return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
1852
+ }
1853
+ };
1854
+
1855
+ // src/codebase-index/codebase-search-tool.ts
1856
+ var codebaseSearchTool = {
1857
+ name: "codebase-search",
1858
+ category: "Project",
1859
+ description: "Search indexed code symbols by name, signature, or doc comment. Uses BM25 ranking for relevance.",
1860
+ usageHint: "Pass `query` for keyword search. Filter with `kind` (class/function/interface/etc), `lang` (ts/js/etc), `file` (substring). `limit` caps results (default 20).",
1861
+ permission: "auto",
1862
+ mutating: false,
1863
+ timeoutMs: 1e4,
1864
+ inputSchema: {
1865
+ type: "object",
1866
+ properties: {
1867
+ query: {
1868
+ type: "string",
1869
+ description: "Search query \u2014 searches symbol names, signatures, and doc comments"
1870
+ },
1871
+ kind: {
1872
+ type: "string",
1873
+ description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
1874
+ },
1875
+ lang: {
1876
+ type: "string",
1877
+ description: "Filter by language: ts, tsx, js, jsx"
1878
+ },
1879
+ lspKind: {
1880
+ type: "integer",
1881
+ description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
1882
+ },
1883
+ file: {
1884
+ type: "string",
1885
+ description: "Filter to files matching this path substring"
1886
+ },
1887
+ limit: {
1888
+ type: "integer",
1889
+ description: "Maximum results to return (default 20, max 100)",
1890
+ minimum: 1,
1891
+ maximum: 100
1892
+ }
1893
+ },
1894
+ required: ["query"]
1895
+ },
1896
+ async execute(input, ctx) {
1897
+ const store = new IndexStore(ctx.projectRoot);
1898
+ try {
1899
+ const limit = Math.min(input.limit ?? 20, 100);
1900
+ const candidates = store.search(input.query, {
1901
+ kind: input.kind,
1902
+ lang: input.lang,
1903
+ file: input.file,
1904
+ lspKind: input.lspKind
1905
+ });
1906
+ if (candidates.length === 0) {
1907
+ return { results: [], total: 0, query: input.query };
1908
+ }
1909
+ const indexable = candidates.map((c) => ({
1910
+ id: c.id,
1911
+ text: buildIndexableText(c.name, c.signature, c.docComment)
1912
+ }));
1913
+ const bm25 = buildBm25Index(indexable);
1914
+ const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
1915
+ scored.sort((a, b) => b.score - a.score);
1916
+ const top = scored.slice(0, limit);
1917
+ const qTokens = tokenise(input.query);
1918
+ const results = top.map(({ id, score }) => {
1919
+ const c = candidates.find((c2) => c2.id === id);
1920
+ const snippet = bm25.extractSnippet(id, qTokens);
1921
+ return {
1922
+ ...c,
1923
+ score,
1924
+ snippet
1925
+ };
1926
+ });
1927
+ return {
1928
+ results,
1929
+ total: candidates.length,
1930
+ query: input.query
1931
+ };
1932
+ } finally {
1933
+ store.close();
1934
+ }
1935
+ }
1936
+ };
1937
+
1938
+ // src/codebase-index/codebase-stats-tool.ts
1939
+ var codebaseStatsTool = {
1940
+ name: "codebase-stats",
1941
+ category: "Project",
1942
+ description: "Return statistics about the symbol index: total symbols, files, breakdown by language and kind, index size, and last update time.",
1943
+ usageHint: "No arguments needed. Use to check if the index is stale or healthy before running a search.",
1944
+ permission: "auto",
1945
+ mutating: false,
1946
+ timeoutMs: 5e3,
1947
+ inputSchema: {
1948
+ type: "object",
1949
+ properties: {},
1950
+ additionalProperties: false
1951
+ },
1952
+ async execute(_input, ctx) {
1953
+ const store = new IndexStore(ctx.projectRoot);
1954
+ try {
1955
+ const stats = store.getStats();
1956
+ return {
1957
+ totalSymbols: stats.totalSymbols,
1958
+ totalFiles: stats.totalFiles,
1959
+ byLang: stats.byLang,
1960
+ byKind: stats.byKind,
1961
+ lastIndexed: stats.lastIndexed,
1962
+ sizeBytes: stats.sizeBytes,
1963
+ indexPath: stats.indexPath,
1964
+ version: stats.version
1965
+ };
1966
+ } finally {
1967
+ store.close();
1968
+ }
1969
+ }
1970
+ };
1971
+
1972
+ export { IndexStore, SCHEMA_VERSION, buildBm25Index, buildIndexableText, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, internalKindToLspKind, lspKindToInternalKind, tokenise };
1973
+ //# sourceMappingURL=index.js.map
1974
+ //# sourceMappingURL=index.js.map