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.
- package/README.md +12 -1
- package/dist/claude/installer.d.ts +8 -0
- package/dist/claude/installer.js +93 -1
- package/dist/claude/installer.js.map +1 -1
- package/dist/claude/mcp.d.ts +3 -1
- package/dist/claude/mcp.js +298 -14
- package/dist/claude/mcp.js.map +1 -1
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +63 -24
- package/dist/db/schema.js.map +1 -1
- package/dist/index.js +15 -5
- package/dist/index.js.map +1 -1
- package/dist/indexer/collector.js +1 -1
- package/dist/indexer/collector.js.map +1 -1
- package/dist/indexer/index.d.ts +4 -1
- package/dist/indexer/index.js +42 -10
- package/dist/indexer/index.js.map +1 -1
- package/dist/query/engine.d.ts +67 -0
- package/dist/query/engine.js +303 -20
- package/dist/query/engine.js.map +1 -1
- package/dist/watcher/daemon.js +69 -18
- package/dist/watcher/daemon.js.map +1 -1
- package/package.json +1 -1
package/dist/query/engine.d.ts
CHANGED
|
@@ -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;
|
package/dist/query/engine.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
144
|
+
`).all(ftsQuery, boundedLimit);
|
|
145
|
+
pushUnique(results, seen, ftsRows, boundedLimit);
|
|
83
146
|
// If FTS has enough results, return immediately
|
|
84
|
-
if (results.length >=
|
|
147
|
+
if (results.length >= boundedLimit)
|
|
85
148
|
return results;
|
|
86
149
|
// Fallback: trigram substring search for partial/camelCase matches
|
|
87
150
|
try {
|
|
88
|
-
const
|
|
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,
|
|
104
|
-
|
|
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
|
}
|