@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,33 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { RagDB } from "../db";
3
+ import { loadConfig, type RagConfig } from "../config";
4
+ import { registerSearchTools } from "./search";
5
+ import { registerIndexTools } from "./index-tools";
6
+ import { registerGraphTools } from "./graph-tools";
7
+ import { registerConversationTools } from "./conversation-tools";
8
+ import { registerCheckpointTools } from "./checkpoint-tools";
9
+ import { registerAnnotationTools } from "./annotation-tools";
10
+ import { registerAnalyticsTools } from "./analytics-tools";
11
+ import { registerGitTools } from "./git-tools";
12
+
13
+ export type GetDB = (dir: string) => RagDB;
14
+
15
+ /** Resolve the project directory, database, and config from an optional directory param. */
16
+ export async function resolveProject(
17
+ directory: string | undefined,
18
+ getDB: GetDB
19
+ ): Promise<{ projectDir: string; db: RagDB; config: RagConfig }> {
20
+ const projectDir = directory || process.env.RAG_PROJECT_DIR || process.cwd();
21
+ return { projectDir, db: getDB(projectDir), config: await loadConfig(projectDir) };
22
+ }
23
+
24
+ export function registerAllTools(server: McpServer, getDB: (dir: string) => RagDB) {
25
+ registerSearchTools(server, getDB);
26
+ registerIndexTools(server, getDB);
27
+ registerGraphTools(server, getDB);
28
+ registerConversationTools(server, getDB);
29
+ registerCheckpointTools(server, getDB);
30
+ registerAnnotationTools(server, getDB);
31
+ registerAnalyticsTools(server, getDB);
32
+ registerGitTools(server, getDB);
33
+ }
@@ -0,0 +1,238 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { relative } from "path";
4
+ import { type AnnotationRow } from "../db";
5
+ import { search, searchChunks } from "../search/hybrid";
6
+ import { type GetDB, resolveProject } from "./index";
7
+
8
+ export function registerSearchTools(server: McpServer, getDB: GetDB) {
9
+ server.tool(
10
+ "search",
11
+ "Semantic search over indexed files. Returns ranked file paths with relevance scores and snippets.",
12
+ {
13
+ query: z.string().describe("The search query (natural language)"),
14
+ directory: z
15
+ .string()
16
+ .optional()
17
+ .describe(
18
+ "Project directory to search. Defaults to RAG_PROJECT_DIR env or cwd"
19
+ ),
20
+ top: z
21
+ .number()
22
+ .optional()
23
+ .describe("Number of results to return (default: from config or 5)"),
24
+ },
25
+ async ({ query, directory, top }) => {
26
+ const { db: ragDb, config } = await resolveProject(directory, getDB);
27
+
28
+ const results = await search(query, ragDb, top ?? config.searchTopK, 0, config.hybridWeight, config.enableReranking);
29
+
30
+ if (results.length === 0) {
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text" as const,
35
+ text: "No results found. Has the directory been indexed? Try calling index_files first.",
36
+ },
37
+ ],
38
+ };
39
+ }
40
+
41
+ const text = results
42
+ .map(
43
+ (r) =>
44
+ `${r.score.toFixed(4)} ${r.path}\n ${r.snippets[0]?.slice(0, 400)}...`
45
+ )
46
+ .join("\n\n");
47
+
48
+ return {
49
+ content: [{ type: "text" as const, text }],
50
+ };
51
+ }
52
+ );
53
+
54
+ server.tool(
55
+ "read_relevant",
56
+ "Retrieve the most relevant semantic chunks for a query. Returns full chunk content — individual functions, classes, or sections — ranked by relevance. No file deduplication: multiple chunks from the same file can appear. Use this instead of search + Read when you need the actual content.",
57
+ {
58
+ query: z.string().describe("The search query (natural language)"),
59
+ directory: z
60
+ .string()
61
+ .optional()
62
+ .describe(
63
+ "Project directory to search. Defaults to RAG_PROJECT_DIR env or cwd"
64
+ ),
65
+ top: z
66
+ .number()
67
+ .optional()
68
+ .describe("Max chunks to return (default: 8)"),
69
+ threshold: z
70
+ .number()
71
+ .optional()
72
+ .describe("Min relevance score to include (default: 0.3)"),
73
+ },
74
+ async ({ query, directory, top, threshold }) => {
75
+ const { projectDir, db: ragDb, config } = await resolveProject(directory, getDB);
76
+
77
+ const results = await searchChunks(
78
+ query,
79
+ ragDb,
80
+ top ?? 8,
81
+ threshold ?? 0.3,
82
+ config.hybridWeight,
83
+ config.enableReranking
84
+ );
85
+
86
+ if (results.length === 0) {
87
+ return {
88
+ content: [
89
+ {
90
+ type: "text" as const,
91
+ text: "No relevant chunks found. Has the directory been indexed? Try calling index_files first.",
92
+ },
93
+ ],
94
+ };
95
+ }
96
+
97
+ // Batch-fetch annotations for all unique paths (avoids N+1 queries)
98
+ const uniqueRelPaths = [...new Set(results.map((r) => relative(projectDir, r.path)))];
99
+ const annotationsByPath = new Map<string, AnnotationRow[]>();
100
+ for (const relPath of uniqueRelPaths) {
101
+ const anns = ragDb.getAnnotations(relPath);
102
+ if (anns.length > 0) annotationsByPath.set(relPath, anns);
103
+ }
104
+
105
+ const text = results
106
+ .map((r) => {
107
+ const lineRange = r.startLine != null && r.endLine != null ? `:${r.startLine}-${r.endLine}` : "";
108
+ const entity = r.entityName ? ` • ${r.entityName}` : "";
109
+ const header = `[${r.score.toFixed(2)}] ${r.path}${lineRange}${entity}`;
110
+
111
+ // Surface annotations for this file (and matching entity if applicable)
112
+ const relPath = relative(projectDir, r.path);
113
+ const fileAnnotations = annotationsByPath.get(relPath) ?? [];
114
+ const relevant = fileAnnotations.filter(
115
+ (a) => a.symbolName == null || a.symbolName === r.entityName
116
+ );
117
+ const noteBlock = relevant.length > 0
118
+ ? relevant.map((a) => {
119
+ const target = a.symbolName ? ` (${a.symbolName})` : "";
120
+ return `[NOTE${target}] ${a.note}`;
121
+ }).join("\n") + "\n"
122
+ : "";
123
+
124
+ return `${header}\n${noteBlock}${r.content}`;
125
+ })
126
+ .join("\n\n---\n\n");
127
+
128
+ return {
129
+ content: [{ type: "text" as const, text }],
130
+ };
131
+ }
132
+ );
133
+
134
+ server.tool(
135
+ "search_symbols",
136
+ "Search for exported symbols by name — functions, classes, types, interfaces, enums. Returns the files that export matching symbols with the defining code snippet. Faster than semantic search when you know the symbol name.",
137
+ {
138
+ symbol: z.string().describe("Symbol name to search for"),
139
+ exact: z
140
+ .boolean()
141
+ .optional()
142
+ .describe("Require exact match (case-insensitive). Default: false (substring match)"),
143
+ type: z
144
+ .enum(["function", "class", "interface", "type", "enum", "export"])
145
+ .optional()
146
+ .describe("Filter by symbol type"),
147
+ directory: z
148
+ .string()
149
+ .optional()
150
+ .describe("Project directory. Defaults to RAG_PROJECT_DIR env or cwd"),
151
+ top: z.number().optional().describe("Max results (default: 20)"),
152
+ },
153
+ async ({ symbol, exact, type, directory, top }) => {
154
+ const { db: ragDb } = await resolveProject(directory, getDB);
155
+
156
+ const results = ragDb.searchSymbols(symbol, exact ?? false, type, top ?? 20);
157
+
158
+ if (results.length === 0) {
159
+ return {
160
+ content: [{ type: "text" as const, text: `No exported symbols matching "${symbol}" found.` }],
161
+ };
162
+ }
163
+
164
+ const text = results
165
+ .map((r) => {
166
+ const snippet = r.snippet ? `\n${r.snippet.slice(0, 300)}` : "";
167
+ return `${r.path} • ${r.symbolName} (${r.symbolType})${snippet}`;
168
+ })
169
+ .join("\n\n---\n\n");
170
+
171
+ return { content: [{ type: "text" as const, text }] };
172
+ }
173
+ );
174
+
175
+ server.tool(
176
+ "write_relevant",
177
+ "Find the best location to insert new content. Given code or documentation you want to add, returns the most semantically appropriate files and insertion points — the chunk after which your content belongs, with an anchor for precise placement.",
178
+ {
179
+ content: z.string().describe("The content you want to add — a function, class, doc section, etc."),
180
+ directory: z
181
+ .string()
182
+ .optional()
183
+ .describe("Project directory. Defaults to RAG_PROJECT_DIR env or cwd"),
184
+ top: z
185
+ .number()
186
+ .optional()
187
+ .describe("Number of candidate locations to return (default: 3)"),
188
+ threshold: z
189
+ .number()
190
+ .optional()
191
+ .describe("Min relevance score (default: 0.3)"),
192
+ },
193
+ async ({ content, directory, top, threshold }) => {
194
+ const { db: ragDb, config } = await resolveProject(directory, getDB);
195
+
196
+ const topN = top ?? 3;
197
+ const chunks = await searchChunks(
198
+ content,
199
+ ragDb,
200
+ topN * 3,
201
+ threshold ?? 0.3,
202
+ config.hybridWeight,
203
+ config.enableReranking
204
+ );
205
+
206
+ if (chunks.length === 0) {
207
+ return {
208
+ content: [{ type: "text" as const, text: "No relevant location found. The index may be empty — try index_files first." }],
209
+ };
210
+ }
211
+
212
+ // Best-scoring chunk per file, up to topN
213
+ const byFile = new Map<string, (typeof chunks)[0]>();
214
+ for (const r of chunks) {
215
+ const existing = byFile.get(r.path);
216
+ if (!existing || r.score > existing.score) {
217
+ byFile.set(r.path, r);
218
+ }
219
+ }
220
+
221
+ const candidates = [...byFile.values()]
222
+ .sort((a, b) => b.score - a.score)
223
+ .slice(0, topN);
224
+
225
+ const text = candidates
226
+ .map((r) => {
227
+ const insertAfter = r.entityName
228
+ ? `after \`${r.entityName}\` (chunk ${r.chunkIndex})`
229
+ : `after chunk ${r.chunkIndex}`;
230
+ const anchor = r.content.slice(-150).trim();
231
+ return `[${r.score.toFixed(2)}] ${r.path}\n Insert ${insertAfter}\n Anchor: ...${anchor}`;
232
+ })
233
+ .join("\n\n---\n\n");
234
+
235
+ return { content: [{ type: "text" as const, text }] };
236
+ }
237
+ );
238
+ }
package/src/types.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** A chunk with its computed embedding, ready for DB insertion. */
2
+ export interface EmbeddedChunk {
3
+ snippet: string;
4
+ embedding: Float32Array;
5
+ entityName?: string | null;
6
+ chunkType?: string | null;
7
+ startLine?: number | null;
8
+ endLine?: number | null;
9
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Lightweight logger that writes to stderr (the MCP diagnostic channel).
3
+ * Configurable via LOG_LEVEL env var: "debug" | "warn" | "error" | "silent"
4
+ * Default: "warn"
5
+ */
6
+
7
+ type Level = "debug" | "warn" | "error" | "silent";
8
+
9
+ const LEVELS: Record<Level, number> = {
10
+ debug: 0,
11
+ warn: 1,
12
+ error: 2,
13
+ silent: 3,
14
+ };
15
+
16
+ function currentLevel(): number {
17
+ const env = (process.env.LOG_LEVEL || "warn").toLowerCase() as Level;
18
+ return LEVELS[env] ?? LEVELS.warn;
19
+ }
20
+
21
+ function write(level: Level, prefix: string, msg: string, context?: string) {
22
+ if (LEVELS[level] < currentLevel()) return;
23
+ const line = context
24
+ ? `[local-rag] ${prefix} ${msg} (${context})`
25
+ : `[local-rag] ${prefix} ${msg}`;
26
+ process.stderr.write(line + "\n");
27
+ }
28
+
29
+ export const log = {
30
+ debug(msg: string, context?: string) {
31
+ write("debug", "DEBUG", msg, context);
32
+ },
33
+ warn(msg: string, context?: string) {
34
+ write("warn", "WARN", msg, context);
35
+ },
36
+ error(msg: string, context?: string) {
37
+ write("error", "ERROR", msg, context);
38
+ },
39
+ };