claude-ex 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -79,8 +79,63 @@ export interface FileSymbolResult {
79
79
  exported: boolean;
80
80
  parameters: string | null;
81
81
  }
82
+ export interface FileContextSymbol extends FileSymbolResult {
83
+ pagerank: number;
84
+ code?: string | null;
85
+ }
86
+ export interface FileContextImport {
87
+ file: string;
88
+ importName: string | null;
89
+ }
90
+ export interface FileContextPackage {
91
+ package: string;
92
+ importedNames: string | null;
93
+ }
94
+ export interface FileContextFile {
95
+ path: string;
96
+ language: string | null;
97
+ lineCount: number;
98
+ symbols: FileContextSymbol[];
99
+ exports: FileContextSymbol[];
100
+ imports: FileContextImport[];
101
+ importedBy: FileContextImport[];
102
+ packages: FileContextPackage[];
103
+ }
104
+ export interface RelatedFileContext {
105
+ file: string;
106
+ score: number;
107
+ reasons: string[];
108
+ symbolCount: number;
109
+ exports: string[];
110
+ }
111
+ export interface FileContextResult {
112
+ files: FileContextFile[];
113
+ relatedFiles: RelatedFileContext[];
114
+ }
115
+ export interface TaskContextResult {
116
+ query: string;
117
+ topSymbols: SearchResult[];
118
+ matchedFiles: FileResult[];
119
+ selectedFiles: string[];
120
+ fileContext: FileContextResult;
121
+ notes: string[];
122
+ }
82
123
  /** Get all symbols in a specific file */
83
124
  export declare function getFileSymbols(db: Database.Database, filePath: string): FileSymbolResult[];
125
+ /** Build a compact, ranked context bundle around one or more files. */
126
+ export declare function getFileContext(db: Database.Database, filePaths: string[], options?: {
127
+ maxSymbols?: number;
128
+ maxRelated?: number;
129
+ includeCode?: boolean;
130
+ }): FileContextResult;
131
+ /** One-shot context builder for AI agents: query -> relevant symbols/files/context. */
132
+ export declare function getTaskContext(db: Database.Database, query: string, options?: {
133
+ files?: string[];
134
+ maxSymbols?: number;
135
+ maxFiles?: number;
136
+ maxRelated?: number;
137
+ includeCode?: boolean;
138
+ }): TaskContextResult;
84
139
  /** Find symbols by kind (class, function, interface, type, enum, method, variable) */
85
140
  export declare function findByKind(db: Database.Database, kind: string, limit?: number): SearchResult[];
86
141
  export interface TypeHierarchyResult {
@@ -167,4 +222,16 @@ export declare function getModulesFromRoot(rootDir: string): ModuleResult[];
167
222
  export declare function getStatsFromRoot(rootDir: string): Stats;
168
223
  export declare function briefFromRoot(rootDir: string): string;
169
224
  export declare function preEditContextFromRoot(rootDir: string, filePath: string): string;
225
+ export declare function getFileContextFromRoot(rootDir: string, files: string[], options?: {
226
+ maxSymbols?: number;
227
+ maxRelated?: number;
228
+ includeCode?: boolean;
229
+ }): FileContextResult;
230
+ export declare function getTaskContextFromRoot(rootDir: string, query: string, options?: {
231
+ files?: string[];
232
+ maxSymbols?: number;
233
+ maxFiles?: number;
234
+ maxRelated?: number;
235
+ includeCode?: boolean;
236
+ }): TaskContextResult;
170
237
  export declare function reviewDiffFromRoot(rootDir: string, target?: string): ReviewDiffResult;
@@ -14,6 +14,8 @@ exports.getStats = getStats;
14
14
  exports.brief = brief;
15
15
  exports.preEditContext = preEditContext;
16
16
  exports.getFileSymbols = getFileSymbols;
17
+ exports.getFileContext = getFileContext;
18
+ exports.getTaskContext = getTaskContext;
17
19
  exports.findByKind = findByKind;
18
20
  exports.getTypeHierarchy = getTypeHierarchy;
19
21
  exports.findDeadExports = findDeadExports;
@@ -31,6 +33,8 @@ exports.getModulesFromRoot = getModulesFromRoot;
31
33
  exports.getStatsFromRoot = getStatsFromRoot;
32
34
  exports.briefFromRoot = briefFromRoot;
33
35
  exports.preEditContextFromRoot = preEditContextFromRoot;
36
+ exports.getFileContextFromRoot = getFileContextFromRoot;
37
+ exports.getTaskContextFromRoot = getTaskContextFromRoot;
34
38
  exports.reviewDiffFromRoot = reviewDiffFromRoot;
35
39
  const child_process_1 = require("child_process");
36
40
  const schema_1 = require("../db/schema");
@@ -62,15 +66,73 @@ function sanitizeFts(query) {
62
66
  const orFallback = tokens.map(t => `"${t}"`).join(' OR ');
63
67
  return `(${phrase}) OR (${orFallback})`;
64
68
  }
69
+ function isSimpleSymbolQuery(query) {
70
+ return /^[\w.$-]+$/.test(query.trim());
71
+ }
72
+ function escapeLike(query) {
73
+ return query.replace(/[\\%_]/g, ch => `\\${ch}`);
74
+ }
75
+ function pushUnique(results, seen, rows, limit) {
76
+ for (const row of rows) {
77
+ const key = `${row.name}:${row.qualifiedName ?? ''}:${row.file}:${row.lineStart}`;
78
+ if (seen.has(key))
79
+ continue;
80
+ results.push(row);
81
+ seen.add(key);
82
+ if (results.length >= limit)
83
+ return;
84
+ }
85
+ }
65
86
  // --- DB-direct functions (for MCP server hot path) ---
66
87
  function search(db, query, limit = 15) {
67
- const ftsQuery = sanitizeFts(query);
88
+ const cleanedQuery = query.trim();
89
+ if (!cleanedQuery)
90
+ return [];
91
+ const boundedLimit = Math.max(1, Math.min(limit, 200));
92
+ const results = [];
93
+ const seen = new Set();
94
+ if (isSimpleSymbolQuery(cleanedQuery)) {
95
+ const exactRows = cached(db, `
96
+ SELECT s.name, s.qualified_name as qualifiedName, s.kind, f.path as file,
97
+ s.line_start as lineStart, s.line_end as lineEnd, s.signature,
98
+ COALESCE(r.pagerank, 0) as pagerank
99
+ FROM symbols s
100
+ JOIN files f ON f.id = s.file_id
101
+ LEFT JOIN rankings r ON r.symbol_id = s.id
102
+ WHERE s.name = ? COLLATE NOCASE
103
+ OR s.qualified_name = ? COLLATE NOCASE
104
+ ORDER BY s.exported DESC, r.pagerank DESC, s.line_start
105
+ LIMIT ?
106
+ `).all(cleanedQuery, cleanedQuery, boundedLimit);
107
+ pushUnique(results, seen, exactRows, boundedLimit);
108
+ if (results.length >= boundedLimit)
109
+ return results;
110
+ if (cleanedQuery.length >= 2) {
111
+ const prefix = `${escapeLike(cleanedQuery)}%`;
112
+ const prefixRows = cached(db, `
113
+ SELECT s.name, s.qualified_name as qualifiedName, s.kind, f.path as file,
114
+ s.line_start as lineStart, s.line_end as lineEnd, s.signature,
115
+ COALESCE(r.pagerank, 0) as pagerank
116
+ FROM symbols s
117
+ JOIN files f ON f.id = s.file_id
118
+ LEFT JOIN rankings r ON r.symbol_id = s.id
119
+ WHERE s.name COLLATE NOCASE LIKE ? ESCAPE '\\'
120
+ OR s.qualified_name COLLATE NOCASE LIKE ? ESCAPE '\\'
121
+ ORDER BY r.pagerank DESC, s.name
122
+ LIMIT ?
123
+ `).all(prefix, prefix, boundedLimit);
124
+ pushUnique(results, seen, prefixRows, boundedLimit);
125
+ if (results.length >= boundedLimit)
126
+ return results;
127
+ }
128
+ }
129
+ const ftsQuery = sanitizeFts(cleanedQuery);
68
130
  if (!ftsQuery)
69
131
  return [];
70
132
  // Primary: FTS5 word-level search (fast, ranked)
71
- const results = cached(db, `
72
- SELECT s.name, s.qualified_name, s.kind, f.path as file,
73
- s.line_start, s.line_end, s.signature,
133
+ const ftsRows = cached(db, `
134
+ SELECT s.name, s.qualified_name as qualifiedName, s.kind, f.path as file,
135
+ s.line_start as lineStart, s.line_end as lineEnd, s.signature,
74
136
  COALESCE(r.pagerank, 0) as pagerank
75
137
  FROM symbols_fts fts
76
138
  JOIN symbols s ON s.id = fts.rowid
@@ -79,19 +141,19 @@ function search(db, query, limit = 15) {
79
141
  WHERE symbols_fts MATCH ?
80
142
  ORDER BY r.pagerank DESC, fts.rank
81
143
  LIMIT ?
82
- `).all(ftsQuery, limit);
144
+ `).all(ftsQuery, boundedLimit);
145
+ pushUnique(results, seen, ftsRows, boundedLimit);
83
146
  // If FTS has enough results, return immediately
84
- if (results.length >= limit)
147
+ if (results.length >= boundedLimit)
85
148
  return results;
86
149
  // Fallback: trigram substring search for partial/camelCase matches
87
150
  try {
88
- const seenIds = new Set(results.map(r => `${r.name}:${r.file}`));
89
- const cleaned = query.replace(/[^\w\s]/g, '').trim();
151
+ const cleaned = cleanedQuery.replace(/[^\w\s]/g, '').trim();
90
152
  if (cleaned.length < 3)
91
153
  return results;
92
154
  const trigramResults = cached(db, `
93
- SELECT s.name, s.qualified_name, s.kind, f.path as file,
94
- s.line_start, s.line_end, s.signature,
155
+ SELECT s.name, s.qualified_name as qualifiedName, s.kind, f.path as file,
156
+ s.line_start as lineStart, s.line_end as lineEnd, s.signature,
95
157
  COALESCE(r.pagerank, 0) as pagerank
96
158
  FROM symbols_trigram tri
97
159
  JOIN symbols s ON s.id = tri.rowid
@@ -100,16 +162,8 @@ function search(db, query, limit = 15) {
100
162
  WHERE symbols_trigram MATCH ?
101
163
  ORDER BY r.pagerank DESC
102
164
  LIMIT ?
103
- `).all(cleaned, limit);
104
- for (const r of trigramResults) {
105
- const key = `${r.name}:${r.file}`;
106
- if (!seenIds.has(key)) {
107
- results.push(r);
108
- seenIds.add(key);
109
- if (results.length >= limit)
110
- break;
111
- }
112
- }
165
+ `).all(cleaned, boundedLimit);
166
+ pushUnique(results, seen, trigramResults, boundedLimit);
113
167
  }
114
168
  catch {
115
169
  // Trigram table may not exist on older DBs
@@ -452,6 +506,229 @@ function getFileSymbols(db, filePath) {
452
506
  ORDER BY s.line_start
453
507
  `).all(filePath);
454
508
  }
509
+ function addRelated(related, file, score, reason) {
510
+ const current = related.get(file) || { score: 0, reasons: new Set() };
511
+ current.score += score;
512
+ current.reasons.add(reason);
513
+ related.set(file, current);
514
+ }
515
+ /** Build a compact, ranked context bundle around one or more files. */
516
+ function getFileContext(db, filePaths, options) {
517
+ const maxSymbols = Math.max(1, Math.min(options?.maxSymbols ?? 30, 100));
518
+ const maxRelated = Math.max(0, Math.min(options?.maxRelated ?? 20, 100));
519
+ const includeCode = options?.includeCode ?? false;
520
+ const inputFiles = [...new Set(filePaths.filter(Boolean))];
521
+ const inputSet = new Set(inputFiles);
522
+ const related = new Map();
523
+ const fileRows = cached(db, `
524
+ SELECT id, path, language, line_count as lineCount
525
+ FROM files
526
+ WHERE path = ?
527
+ `);
528
+ const symbolSql = includeCode ? `
529
+ SELECT s.name, s.qualified_name as qualifiedName, s.kind,
530
+ s.line_start as lineStart, s.line_end as lineEnd,
531
+ s.signature, s.exported, s.parameters, s.content as code,
532
+ COALESCE(r.pagerank, 0) as pagerank
533
+ FROM symbols s
534
+ LEFT JOIN rankings r ON r.symbol_id = s.id
535
+ WHERE s.file_id = ?
536
+ ORDER BY s.exported DESC, COALESCE(r.pagerank, 0) DESC, s.line_start
537
+ LIMIT ?
538
+ ` : `
539
+ SELECT s.name, s.qualified_name as qualifiedName, s.kind,
540
+ s.line_start as lineStart, s.line_end as lineEnd,
541
+ s.signature, s.exported, s.parameters,
542
+ COALESCE(r.pagerank, 0) as pagerank
543
+ FROM symbols s
544
+ LEFT JOIN rankings r ON r.symbol_id = s.id
545
+ WHERE s.file_id = ?
546
+ ORDER BY s.exported DESC, COALESCE(r.pagerank, 0) DESC, s.line_start
547
+ LIMIT ?
548
+ `;
549
+ const symbolsStmt = cached(db, symbolSql);
550
+ const importsStmt = cached(db, `
551
+ SELECT f.path as file, fd.import_name as importName
552
+ FROM file_deps fd
553
+ JOIN files f ON f.id = fd.to_file
554
+ WHERE fd.from_file = ?
555
+ ORDER BY f.path
556
+ `);
557
+ const importedByStmt = cached(db, `
558
+ SELECT f.path as file, fd.import_name as importName
559
+ FROM file_deps fd
560
+ JOIN files f ON f.id = fd.from_file
561
+ WHERE fd.to_file = ?
562
+ ORDER BY f.path
563
+ `);
564
+ const packagesStmt = cached(db, `
565
+ SELECT package, imported_names as importedNames
566
+ FROM pkg_deps
567
+ WHERE file_id = ?
568
+ ORDER BY package
569
+ `);
570
+ const callerFilesStmt = cached(db, `
571
+ SELECT f.path as file, GROUP_CONCAT(DISTINCT target.name) as symbols
572
+ FROM edges e
573
+ JOIN symbols target ON target.id = e.to_id
574
+ JOIN symbols caller ON caller.id = e.from_id
575
+ JOIN files f ON f.id = caller.file_id
576
+ WHERE target.file_id = ?
577
+ AND target.exported = 1
578
+ AND caller.file_id != target.file_id
579
+ AND e.kind IN ('calls', 'references')
580
+ GROUP BY f.path
581
+ `);
582
+ const files = [];
583
+ for (const filePath of inputFiles) {
584
+ const file = fileRows.get(filePath);
585
+ if (!file)
586
+ continue;
587
+ const symbols = symbolsStmt.all(file.id, maxSymbols);
588
+ const imports = importsStmt.all(file.id);
589
+ const importedBy = importedByStmt.all(file.id);
590
+ const packages = packagesStmt.all(file.id);
591
+ for (const imp of imports) {
592
+ if (!inputSet.has(imp.file)) {
593
+ addRelated(related, imp.file, 30, `imported by ${file.path}`);
594
+ }
595
+ }
596
+ for (const dep of importedBy) {
597
+ if (!inputSet.has(dep.file)) {
598
+ addRelated(related, dep.file, 45, `imports ${file.path}`);
599
+ }
600
+ }
601
+ const callerFiles = callerFilesStmt.all(file.id);
602
+ for (const caller of callerFiles) {
603
+ if (!inputSet.has(caller.file)) {
604
+ const suffix = caller.symbols ? ` (${caller.symbols})` : '';
605
+ addRelated(related, caller.file, 55, `calls exported symbols from ${file.path}${suffix}`);
606
+ }
607
+ }
608
+ for (const impact of getImpact(db, file.path, 3)) {
609
+ if (!inputSet.has(impact.file)) {
610
+ addRelated(related, impact.file, Math.max(5, 25 - impact.depth * 5), `${impact.depth}-hop dependent of ${file.path}`);
611
+ }
612
+ }
613
+ files.push({
614
+ path: file.path,
615
+ language: file.language,
616
+ lineCount: file.lineCount,
617
+ symbols,
618
+ exports: symbols.filter(s => !!s.exported),
619
+ imports,
620
+ importedBy,
621
+ packages,
622
+ });
623
+ }
624
+ const relatedRows = [...related.entries()]
625
+ .sort((a, b) => b[1].score - a[1].score || a[0].localeCompare(b[0]))
626
+ .slice(0, maxRelated);
627
+ const relatedFiles = relatedRows.map(([file, data]) => {
628
+ const meta = cached(db, `
629
+ SELECT f.id, COUNT(s.id) as symbolCount,
630
+ GROUP_CONCAT(CASE WHEN s.exported = 1 THEN s.name || ' [' || s.kind || ']' END, '|||') as exportsStr
631
+ FROM files f
632
+ LEFT JOIN symbols s ON s.file_id = f.id
633
+ WHERE f.path = ?
634
+ GROUP BY f.id
635
+ `).get(file);
636
+ return {
637
+ file,
638
+ score: data.score,
639
+ reasons: [...data.reasons],
640
+ symbolCount: meta?.symbolCount ?? 0,
641
+ exports: meta?.exportsStr ? meta.exportsStr.split('|||').filter(Boolean).slice(0, 12) : [],
642
+ };
643
+ });
644
+ return { files, relatedFiles };
645
+ }
646
+ function findFilesByText(db, query, limit) {
647
+ const cleaned = query.trim();
648
+ if (!cleaned)
649
+ return [];
650
+ const rows = [];
651
+ const seen = new Set();
652
+ if (cleaned.includes('*') || cleaned.includes('/') || cleaned.includes('.')) {
653
+ for (const row of findFiles(db, cleaned, limit)) {
654
+ rows.push(row);
655
+ seen.add(row.path);
656
+ if (rows.length >= limit)
657
+ return rows;
658
+ }
659
+ }
660
+ const tokens = cleaned
661
+ .replace(/[^\w.\-/]/g, ' ')
662
+ .split(/\s+/)
663
+ .filter(token => token.length >= 2)
664
+ .slice(0, 4);
665
+ for (const token of tokens) {
666
+ const like = `%${escapeLike(token)}%`;
667
+ const matches = cached(db, `
668
+ SELECT path, language, line_count as lineCount
669
+ FROM files
670
+ WHERE path LIKE ? ESCAPE '\\'
671
+ ORDER BY
672
+ CASE WHEN path LIKE ? ESCAPE '\\' THEN 0 ELSE 1 END,
673
+ LENGTH(path),
674
+ path
675
+ LIMIT ?
676
+ `).all(like, `${escapeLike(token)}%`, limit);
677
+ for (const match of matches) {
678
+ if (seen.has(match.path))
679
+ continue;
680
+ rows.push(match);
681
+ seen.add(match.path);
682
+ if (rows.length >= limit)
683
+ return rows;
684
+ }
685
+ }
686
+ return rows;
687
+ }
688
+ /** One-shot context builder for AI agents: query -> relevant symbols/files/context. */
689
+ function getTaskContext(db, query, options) {
690
+ const maxSymbols = Math.max(1, Math.min(options?.maxSymbols ?? 12, 50));
691
+ const maxFiles = Math.max(1, Math.min(options?.maxFiles ?? 8, 30));
692
+ const maxRelated = Math.max(0, Math.min(options?.maxRelated ?? 12, 50));
693
+ const explicitFiles = [...new Set((options?.files ?? []).filter(Boolean))];
694
+ const topSymbols = search(db, query, maxSymbols);
695
+ const matchedFiles = findFilesByText(db, query, maxFiles);
696
+ const selected = new Map();
697
+ for (const file of explicitFiles)
698
+ selected.set(file, 1000);
699
+ for (let i = 0; i < topSymbols.length; i++) {
700
+ selected.set(topSymbols[i].file, Math.max(selected.get(topSymbols[i].file) ?? 0, 500 - i));
701
+ }
702
+ for (let i = 0; i < matchedFiles.length; i++) {
703
+ selected.set(matchedFiles[i].path, Math.max(selected.get(matchedFiles[i].path) ?? 0, 300 - i));
704
+ }
705
+ const selectedFiles = [...selected.entries()]
706
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
707
+ .slice(0, maxFiles)
708
+ .map(([file]) => file);
709
+ const fileContext = getFileContext(db, selectedFiles, {
710
+ maxSymbols,
711
+ maxRelated,
712
+ includeCode: options?.includeCode,
713
+ });
714
+ const notes = [];
715
+ if (explicitFiles.length > 0)
716
+ notes.push(`${explicitFiles.length} explicit file(s) were pinned first.`);
717
+ if (topSymbols.length > 0)
718
+ notes.push(`${topSymbols.length} ranked symbol match(es) were used to choose files.`);
719
+ if (matchedFiles.length > 0)
720
+ notes.push(`${matchedFiles.length} path match(es) were considered.`);
721
+ if (selectedFiles.length === 0)
722
+ notes.push('No indexed files matched the query.');
723
+ return {
724
+ query,
725
+ topSymbols,
726
+ matchedFiles,
727
+ selectedFiles,
728
+ fileContext,
729
+ notes,
730
+ };
731
+ }
455
732
  /** Find symbols by kind (class, function, interface, type, enum, method, variable) */
456
733
  function findByKind(db, kind, limit = 50) {
457
734
  return cached(db, `
@@ -1384,6 +1661,12 @@ function briefFromRoot(rootDir) {
1384
1661
  function preEditContextFromRoot(rootDir, filePath) {
1385
1662
  return withDb(rootDir, db => preEditContext(db, filePath));
1386
1663
  }
1664
+ function getFileContextFromRoot(rootDir, files, options) {
1665
+ return withDb(rootDir, db => getFileContext(db, files, options));
1666
+ }
1667
+ function getTaskContextFromRoot(rootDir, query, options) {
1668
+ return withDb(rootDir, db => getTaskContext(db, query, options));
1669
+ }
1387
1670
  function reviewDiffFromRoot(rootDir, target) {
1388
1671
  return withDb(rootDir, db => reviewDiff(db, rootDir, target));
1389
1672
  }