@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.
Files changed (61) hide show
  1. package/.claude-plugin/plugin.json +24 -0
  2. package/.mcp.json +11 -0
  3. package/LICENSE +21 -0
  4. package/README.md +567 -0
  5. package/hooks/hooks.json +25 -0
  6. package/hooks/scripts/reindex-file.sh +19 -0
  7. package/hooks/scripts/session-start.sh +11 -0
  8. package/package.json +52 -0
  9. package/skills/local-rag/SKILL.md +42 -0
  10. package/src/cli/commands/analytics.ts +58 -0
  11. package/src/cli/commands/benchmark.ts +30 -0
  12. package/src/cli/commands/checkpoint.ts +85 -0
  13. package/src/cli/commands/conversation.ts +102 -0
  14. package/src/cli/commands/demo.ts +119 -0
  15. package/src/cli/commands/eval.ts +31 -0
  16. package/src/cli/commands/index-cmd.ts +26 -0
  17. package/src/cli/commands/init.ts +35 -0
  18. package/src/cli/commands/map.ts +21 -0
  19. package/src/cli/commands/remove.ts +15 -0
  20. package/src/cli/commands/search-cmd.ts +59 -0
  21. package/src/cli/commands/serve.ts +5 -0
  22. package/src/cli/commands/status.ts +13 -0
  23. package/src/cli/index.ts +117 -0
  24. package/src/cli/progress.ts +21 -0
  25. package/src/cli/setup.ts +192 -0
  26. package/src/config/index.ts +101 -0
  27. package/src/conversation/indexer.ts +147 -0
  28. package/src/conversation/parser.ts +323 -0
  29. package/src/db/analytics.ts +116 -0
  30. package/src/db/annotations.ts +161 -0
  31. package/src/db/checkpoints.ts +166 -0
  32. package/src/db/conversation.ts +241 -0
  33. package/src/db/files.ts +146 -0
  34. package/src/db/graph.ts +250 -0
  35. package/src/db/index.ts +468 -0
  36. package/src/db/search.ts +244 -0
  37. package/src/db/types.ts +85 -0
  38. package/src/embeddings/embed.ts +73 -0
  39. package/src/graph/resolver.ts +305 -0
  40. package/src/indexing/chunker.ts +523 -0
  41. package/src/indexing/indexer.ts +263 -0
  42. package/src/indexing/parse.ts +99 -0
  43. package/src/indexing/watcher.ts +84 -0
  44. package/src/main.ts +8 -0
  45. package/src/search/benchmark.ts +139 -0
  46. package/src/search/eval.ts +171 -0
  47. package/src/search/hybrid.ts +194 -0
  48. package/src/search/reranker.ts +99 -0
  49. package/src/search/usages.ts +27 -0
  50. package/src/server/index.ts +126 -0
  51. package/src/tools/analytics-tools.ts +58 -0
  52. package/src/tools/annotation-tools.ts +89 -0
  53. package/src/tools/checkpoint-tools.ts +147 -0
  54. package/src/tools/conversation-tools.ts +86 -0
  55. package/src/tools/git-tools.ts +103 -0
  56. package/src/tools/graph-tools.ts +163 -0
  57. package/src/tools/index-tools.ts +91 -0
  58. package/src/tools/index.ts +33 -0
  59. package/src/tools/search.ts +238 -0
  60. package/src/types.ts +9 -0
  61. package/src/utils/log.ts +39 -0
@@ -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
+ }