@winci/local-rag 0.2.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/.claude-plugin/plugin.json +24 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +567 -0
- package/hooks/hooks.json +25 -0
- package/hooks/scripts/reindex-file.sh +19 -0
- package/hooks/scripts/session-start.sh +11 -0
- package/package.json +52 -0
- package/skills/local-rag/SKILL.md +42 -0
- package/src/cli/commands/analytics.ts +58 -0
- package/src/cli/commands/benchmark.ts +30 -0
- package/src/cli/commands/checkpoint.ts +85 -0
- package/src/cli/commands/conversation.ts +102 -0
- package/src/cli/commands/demo.ts +119 -0
- package/src/cli/commands/eval.ts +31 -0
- package/src/cli/commands/index-cmd.ts +26 -0
- package/src/cli/commands/init.ts +35 -0
- package/src/cli/commands/map.ts +21 -0
- package/src/cli/commands/remove.ts +15 -0
- package/src/cli/commands/search-cmd.ts +59 -0
- package/src/cli/commands/serve.ts +5 -0
- package/src/cli/commands/status.ts +13 -0
- package/src/cli/index.ts +117 -0
- package/src/cli/progress.ts +21 -0
- package/src/cli/setup.ts +192 -0
- package/src/config/index.ts +101 -0
- package/src/conversation/indexer.ts +147 -0
- package/src/conversation/parser.ts +323 -0
- package/src/db/analytics.ts +116 -0
- package/src/db/annotations.ts +161 -0
- package/src/db/checkpoints.ts +166 -0
- package/src/db/conversation.ts +241 -0
- package/src/db/files.ts +146 -0
- package/src/db/graph.ts +250 -0
- package/src/db/index.ts +468 -0
- package/src/db/search.ts +244 -0
- package/src/db/types.ts +85 -0
- package/src/embeddings/embed.ts +73 -0
- package/src/graph/resolver.ts +305 -0
- package/src/indexing/chunker.ts +523 -0
- package/src/indexing/indexer.ts +263 -0
- package/src/indexing/parse.ts +99 -0
- package/src/indexing/watcher.ts +84 -0
- package/src/main.ts +8 -0
- package/src/search/benchmark.ts +139 -0
- package/src/search/eval.ts +171 -0
- package/src/search/hybrid.ts +194 -0
- package/src/search/reranker.ts +99 -0
- package/src/search/usages.ts +27 -0
- package/src/server/index.ts +126 -0
- package/src/tools/analytics-tools.ts +58 -0
- package/src/tools/annotation-tools.ts +89 -0
- package/src/tools/checkpoint-tools.ts +147 -0
- package/src/tools/conversation-tools.ts +86 -0
- package/src/tools/git-tools.ts +103 -0
- package/src/tools/graph-tools.ts +163 -0
- package/src/tools/index-tools.ts +91 -0
- package/src/tools/index.ts +33 -0
- package/src/tools/search.ts +238 -0
- package/src/types.ts +9 -0
- package/src/utils/log.ts +39 -0
package/src/db/graph.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
export function upsertFileGraph(
|
|
4
|
+
db: Database,
|
|
5
|
+
fileId: number,
|
|
6
|
+
imports: { name: string; source: string }[],
|
|
7
|
+
exports: { name: string; type: string }[]
|
|
8
|
+
) {
|
|
9
|
+
const tx = db.transaction(() => {
|
|
10
|
+
db.run("DELETE FROM file_imports WHERE file_id = ?", [fileId]);
|
|
11
|
+
db.run("DELETE FROM file_exports WHERE file_id = ?", [fileId]);
|
|
12
|
+
|
|
13
|
+
for (const imp of imports) {
|
|
14
|
+
db.run(
|
|
15
|
+
"INSERT INTO file_imports (file_id, source, names) VALUES (?, ?, ?)",
|
|
16
|
+
[fileId, imp.source, imp.name]
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const exp of exports) {
|
|
21
|
+
db.run(
|
|
22
|
+
"INSERT INTO file_exports (file_id, name, type) VALUES (?, ?, ?)",
|
|
23
|
+
[fileId, exp.name, exp.type]
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
tx();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveImport(db: Database, importId: number, resolvedFileId: number) {
|
|
31
|
+
db.run(
|
|
32
|
+
"UPDATE file_imports SET resolved_file_id = ? WHERE id = ?",
|
|
33
|
+
[resolvedFileId, importId]
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getUnresolvedImports(db: Database): { id: number; fileId: number; filePath: string; source: string }[] {
|
|
38
|
+
return db
|
|
39
|
+
.query<{ id: number; file_id: number; path: string; source: string }, []>(
|
|
40
|
+
`SELECT fi.id, fi.file_id, f.path, fi.source
|
|
41
|
+
FROM file_imports fi
|
|
42
|
+
JOIN files f ON f.id = fi.file_id
|
|
43
|
+
WHERE fi.resolved_file_id IS NULL`
|
|
44
|
+
)
|
|
45
|
+
.all()
|
|
46
|
+
.map((r) => ({ id: r.id, fileId: r.file_id, filePath: r.path, source: r.source }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getGraph(db: Database): {
|
|
50
|
+
nodes: { id: number; path: string; exports: { name: string; type: string }[] }[];
|
|
51
|
+
edges: { fromId: number; fromPath: string; toId: number; toPath: string; source: string }[];
|
|
52
|
+
} {
|
|
53
|
+
const files = db
|
|
54
|
+
.query<{ id: number; path: string }, []>("SELECT id, path FROM files")
|
|
55
|
+
.all();
|
|
56
|
+
|
|
57
|
+
// Batch-load all exports in one query instead of per-file
|
|
58
|
+
const allExports = db
|
|
59
|
+
.query<{ file_id: number; name: string; type: string }, []>(
|
|
60
|
+
"SELECT file_id, name, type FROM file_exports"
|
|
61
|
+
)
|
|
62
|
+
.all();
|
|
63
|
+
|
|
64
|
+
const exportsByFile = new Map<number, { name: string; type: string }[]>();
|
|
65
|
+
for (const exp of allExports) {
|
|
66
|
+
let arr = exportsByFile.get(exp.file_id);
|
|
67
|
+
if (!arr) {
|
|
68
|
+
arr = [];
|
|
69
|
+
exportsByFile.set(exp.file_id, arr);
|
|
70
|
+
}
|
|
71
|
+
arr.push({ name: exp.name, type: exp.type });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nodes = files.map((f) => ({
|
|
75
|
+
id: f.id,
|
|
76
|
+
path: f.path,
|
|
77
|
+
exports: exportsByFile.get(f.id) || [],
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const edges = db
|
|
81
|
+
.query<
|
|
82
|
+
{ file_id: number; from_path: string; resolved_file_id: number; to_path: string; source: string },
|
|
83
|
+
[]
|
|
84
|
+
>(
|
|
85
|
+
`SELECT fi.file_id, f1.path as from_path, fi.resolved_file_id, f2.path as to_path, fi.source
|
|
86
|
+
FROM file_imports fi
|
|
87
|
+
JOIN files f1 ON f1.id = fi.file_id
|
|
88
|
+
JOIN files f2 ON f2.id = fi.resolved_file_id
|
|
89
|
+
WHERE fi.resolved_file_id IS NOT NULL`
|
|
90
|
+
)
|
|
91
|
+
.all()
|
|
92
|
+
.map((r) => ({
|
|
93
|
+
fromId: r.file_id,
|
|
94
|
+
fromPath: r.from_path,
|
|
95
|
+
toId: r.resolved_file_id,
|
|
96
|
+
toPath: r.to_path,
|
|
97
|
+
source: r.source,
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
return { nodes, edges };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getSubgraph(db: Database, fileIds: number[], maxHops: number = 2): {
|
|
104
|
+
nodes: { id: number; path: string; exports: { name: string; type: string }[] }[];
|
|
105
|
+
edges: { fromId: number; fromPath: string; toId: number; toPath: string; source: string }[];
|
|
106
|
+
} {
|
|
107
|
+
// BFS via SQL queries per hop instead of loading the full graph.
|
|
108
|
+
// Batch frontier to stay within SQLite's 999-parameter limit (each query uses 2× frontier).
|
|
109
|
+
const BATCH_LIMIT = 499;
|
|
110
|
+
const visited = new Set<number>(fileIds);
|
|
111
|
+
let frontier = [...fileIds];
|
|
112
|
+
|
|
113
|
+
for (let hop = 0; hop < maxHops && frontier.length > 0; hop++) {
|
|
114
|
+
const allNeighbors: { file_id: number; resolved_file_id: number }[] = [];
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < frontier.length; i += BATCH_LIMIT) {
|
|
117
|
+
const batch = frontier.slice(i, i + BATCH_LIMIT);
|
|
118
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
119
|
+
const rows = db
|
|
120
|
+
.query<{ file_id: number; resolved_file_id: number }, number[]>(
|
|
121
|
+
`SELECT file_id, resolved_file_id FROM file_imports
|
|
122
|
+
WHERE resolved_file_id IS NOT NULL
|
|
123
|
+
AND (file_id IN (${placeholders}) OR resolved_file_id IN (${placeholders}))`
|
|
124
|
+
)
|
|
125
|
+
.all(...batch, ...batch);
|
|
126
|
+
allNeighbors.push(...rows);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const nextFrontier: number[] = [];
|
|
130
|
+
for (const row of allNeighbors) {
|
|
131
|
+
if (!visited.has(row.file_id)) {
|
|
132
|
+
visited.add(row.file_id);
|
|
133
|
+
nextFrontier.push(row.file_id);
|
|
134
|
+
}
|
|
135
|
+
if (!visited.has(row.resolved_file_id)) {
|
|
136
|
+
visited.add(row.resolved_file_id);
|
|
137
|
+
nextFrontier.push(row.resolved_file_id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
frontier = nextFrontier;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Load only the nodes and edges for visited file IDs, batched for large sets
|
|
144
|
+
const idList = [...visited];
|
|
145
|
+
|
|
146
|
+
function batchQuery<T>(ids: number[], buildSql: (ph: string) => string): T[] {
|
|
147
|
+
const results: T[] = [];
|
|
148
|
+
for (let i = 0; i < ids.length; i += BATCH_LIMIT) {
|
|
149
|
+
const batch = ids.slice(i, i + BATCH_LIMIT);
|
|
150
|
+
const ph = batch.map(() => "?").join(",");
|
|
151
|
+
results.push(...db.query<T, number[]>(buildSql(ph)).all(...batch));
|
|
152
|
+
}
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const files = batchQuery<{ id: number; path: string }>(
|
|
157
|
+
idList, (ph) => `SELECT id, path FROM files WHERE id IN (${ph})`
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const allExports = batchQuery<{ file_id: number; name: string; type: string }>(
|
|
161
|
+
idList, (ph) => `SELECT file_id, name, type FROM file_exports WHERE file_id IN (${ph})`
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const exportsByFile = new Map<number, { name: string; type: string }[]>();
|
|
165
|
+
for (const exp of allExports) {
|
|
166
|
+
let arr = exportsByFile.get(exp.file_id);
|
|
167
|
+
if (!arr) { arr = []; exportsByFile.set(exp.file_id, arr); }
|
|
168
|
+
arr.push({ name: exp.name, type: exp.type });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const nodes = files.map((f) => ({
|
|
172
|
+
id: f.id,
|
|
173
|
+
path: f.path,
|
|
174
|
+
exports: exportsByFile.get(f.id) || [],
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
// Edge query needs ids twice (file_id IN + resolved_file_id IN), so use half the batch limit
|
|
178
|
+
const EDGE_BATCH = Math.floor(BATCH_LIMIT / 2);
|
|
179
|
+
const edges: { fromId: number; fromPath: string; toId: number; toPath: string; source: string }[] = [];
|
|
180
|
+
for (let i = 0; i < idList.length; i += EDGE_BATCH) {
|
|
181
|
+
const batch = idList.slice(i, i + EDGE_BATCH);
|
|
182
|
+
const ph = batch.map(() => "?").join(",");
|
|
183
|
+
const rows = db
|
|
184
|
+
.query<
|
|
185
|
+
{ file_id: number; from_path: string; resolved_file_id: number; to_path: string; source: string },
|
|
186
|
+
number[]
|
|
187
|
+
>(
|
|
188
|
+
`SELECT fi.file_id, f1.path as from_path, fi.resolved_file_id, f2.path as to_path, fi.source
|
|
189
|
+
FROM file_imports fi
|
|
190
|
+
JOIN files f1 ON f1.id = fi.file_id
|
|
191
|
+
JOIN files f2 ON f2.id = fi.resolved_file_id
|
|
192
|
+
WHERE fi.resolved_file_id IS NOT NULL
|
|
193
|
+
AND fi.file_id IN (${ph}) AND fi.resolved_file_id IN (${ph})`
|
|
194
|
+
)
|
|
195
|
+
.all(...batch, ...batch);
|
|
196
|
+
for (const r of rows) {
|
|
197
|
+
edges.push({
|
|
198
|
+
fromId: r.file_id,
|
|
199
|
+
fromPath: r.from_path,
|
|
200
|
+
toId: r.resolved_file_id,
|
|
201
|
+
toPath: r.to_path,
|
|
202
|
+
source: r.source,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { nodes, edges };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function getImportsForFile(db: Database, fileId: number): { id: number; source: string; resolvedFileId: number | null }[] {
|
|
211
|
+
return db
|
|
212
|
+
.query<{ id: number; source: string; resolved_file_id: number | null }, [number]>(
|
|
213
|
+
"SELECT id, source, resolved_file_id FROM file_imports WHERE file_id = ?"
|
|
214
|
+
)
|
|
215
|
+
.all(fileId)
|
|
216
|
+
.map((r) => ({ id: r.id, source: r.source, resolvedFileId: r.resolved_file_id }));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getImportersOf(db: Database, fileId: number): number[] {
|
|
220
|
+
return db
|
|
221
|
+
.query<{ file_id: number }, [number]>(
|
|
222
|
+
"SELECT file_id FROM file_imports WHERE resolved_file_id = ?"
|
|
223
|
+
)
|
|
224
|
+
.all(fileId)
|
|
225
|
+
.map((r) => r.file_id);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Get resolved dependency paths for a file (what it imports). */
|
|
229
|
+
export function getDependsOn(db: Database, fileId: number): { path: string; source: string }[] {
|
|
230
|
+
return db
|
|
231
|
+
.query<{ path: string; source: string }, [number]>(
|
|
232
|
+
`SELECT f.path, fi.source
|
|
233
|
+
FROM file_imports fi
|
|
234
|
+
JOIN files f ON f.id = fi.resolved_file_id
|
|
235
|
+
WHERE fi.file_id = ? AND fi.resolved_file_id IS NOT NULL`
|
|
236
|
+
)
|
|
237
|
+
.all(fileId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Get files that import a given file (reverse dependencies). */
|
|
241
|
+
export function getDependedOnBy(db: Database, fileId: number): { path: string; source: string }[] {
|
|
242
|
+
return db
|
|
243
|
+
.query<{ path: string; source: string }, [number]>(
|
|
244
|
+
`SELECT f.path, fi.source
|
|
245
|
+
FROM file_imports fi
|
|
246
|
+
JOIN files f ON f.id = fi.file_id
|
|
247
|
+
WHERE fi.resolved_file_id = ?`
|
|
248
|
+
)
|
|
249
|
+
.all(fileId);
|
|
250
|
+
}
|