@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,11 @@
1
+ #!/bin/bash
2
+ # SessionStart hook: ensure the index is warm on session start.
3
+ # The MCP server handles auto-indexing, so this is a lightweight check
4
+ # that logs index status for visibility.
5
+
6
+ STATUS=$(bunx @winci/local-rag status 2>/dev/null)
7
+ if [ $? -eq 0 ]; then
8
+ echo "$STATUS" >&2
9
+ fi
10
+
11
+ exit 0
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@winci/local-rag",
3
+ "version": "0.2.1",
4
+ "description": "Semantic search for your codebase — local-first RAG MCP server with hybrid search, AST-aware chunking, and usage analytics",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TheWinci/local-rag-mcp.git"
10
+ },
11
+ "keywords": [
12
+ "mcp",
13
+ "rag",
14
+ "semantic-search",
15
+ "vector-search",
16
+ "sqlite-vec",
17
+ "embeddings",
18
+ "claude-code",
19
+ "model-context-protocol",
20
+ "local-first",
21
+ "bun"
22
+ ],
23
+ "bin": {
24
+ "local-rag": "src/main.ts"
25
+ },
26
+ "files": [
27
+ "src/",
28
+ ".claude-plugin/",
29
+ ".mcp.json",
30
+ "skills/",
31
+ "hooks/",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "server": "bun run src/main.ts serve",
37
+ "cli": "bun run src/main.ts",
38
+ "test": "bun test tests/ 2>&1; EXIT=$?; if [ $EXIT -eq 133 ]; then printf '\\n[note] Exit code 133 suppressed — known Bun crash on macOS Silicon during process cleanup (github.com/oven-sh/bun/issues/19917). All tests passed.\\n'; exit 0; else exit $EXIT; fi",
39
+ "bench": "bun test benchmarks/"
40
+ },
41
+ "dependencies": {
42
+ "@huggingface/transformers": "^3.4.0",
43
+ "@modelcontextprotocol/sdk": "^1.12.0",
44
+ "code-chunk": "^0.1.13",
45
+ "gray-matter": "^4.0.3",
46
+ "sqlite-vec": "^0.1.6",
47
+ "zod": "^4.3.6"
48
+ },
49
+ "devDependencies": {
50
+ "@types/bun": "^1.3.11"
51
+ }
52
+ }
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: local-rag
3
+ description: Semantic search, code navigation, and conversation memory for the current project. Use automatically when searching code, finding usages, navigating dependencies, or recalling past discussions.
4
+ ---
5
+
6
+ This project has a local RAG index (local-rag). Use these MCP tools proactively:
7
+
8
+ - **`search`**: Discover which files are relevant to a topic. Returns file paths
9
+ with snippet previews — use this when you need to know *where* something is.
10
+ - **`read_relevant`**: Get the actual content of relevant semantic chunks —
11
+ individual functions, classes, or markdown sections — ranked by relevance.
12
+ Results include exact line ranges (`src/db.ts:42-67`) so you can navigate
13
+ directly to the edit location. Use this instead of `search` + `Read` when
14
+ you need the content itself.
15
+ - **`project_map`**: When you need to understand how files relate to each other,
16
+ generate a dependency graph. Use `focus` to zoom into a specific file's
17
+ neighborhood.
18
+ - **`search_conversation`**: Search past conversation history to recall previous
19
+ decisions, discussions, and tool outputs. Use this before re-investigating
20
+ something that may have been discussed in an earlier session.
21
+ - **`create_checkpoint`**: Mark important moments — decisions, milestones,
22
+ blockers, direction changes. Do this after completing features, making key
23
+ decisions, or changing direction.
24
+ - **`list_checkpoints`** / **`search_checkpoints`**: Review or search past
25
+ checkpoints to understand project history and prior decisions.
26
+ - **`index_files`**: If you've created or modified files and want them searchable,
27
+ re-index the project directory.
28
+ - **`search_analytics`**: Check what queries return no results or low-relevance
29
+ results — this reveals documentation gaps.
30
+ - **`search_symbols`**: When you know a symbol name (function, class, type, etc.),
31
+ find it directly by name instead of using semantic search.
32
+ - **`find_usages`**: Before changing a function or type, find all its call sites.
33
+ Use this to understand the blast radius of a rename or API change.
34
+ - **`git_context`**: At the start of a session, call this to see what files have
35
+ already been modified, recent commits, and which changed files are in the index.
36
+ - **`annotate`**: Attach a persistent note to a file or symbol — "known race
37
+ condition", "don't refactor until auth rewrite lands", etc. Notes surface
38
+ inline in `read_relevant` results automatically.
39
+ - **`get_annotations`**: Retrieve all notes for a file, or search semantically
40
+ across all annotations.
41
+ - **`write_relevant`**: Before adding new code or docs, find the best insertion
42
+ point — returns the most semantically appropriate file and anchor.
@@ -0,0 +1,58 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+
4
+ export async function analyticsCommand(args: string[], getFlag: (flag: string) => string | undefined) {
5
+ const dir = resolve(args[1] && !args[1].startsWith("--") ? args[1] : ".");
6
+ const days = parseInt(getFlag("--days") || "30", 10);
7
+ const db = new RagDB(dir);
8
+ const analytics = db.getAnalytics(days);
9
+
10
+ const zeroCount = analytics.zeroResultQueries.reduce((s, q) => s + q.count, 0);
11
+ const zeroRate = analytics.totalQueries > 0
12
+ ? ((zeroCount / analytics.totalQueries) * 100).toFixed(0)
13
+ : "0";
14
+
15
+ console.log(`Search analytics (last ${days} days):`);
16
+ console.log(` Total queries: ${analytics.totalQueries}`);
17
+ console.log(` Avg results: ${analytics.avgResultCount.toFixed(1)}`);
18
+ console.log(` Avg top score: ${analytics.avgTopScore?.toFixed(2) ?? "n/a"}`);
19
+ console.log(` Zero-result rate: ${zeroRate}% (${zeroCount} queries)`);
20
+
21
+ if (analytics.topSearchedTerms.length > 0) {
22
+ console.log("\nTop searches:");
23
+ for (const t of analytics.topSearchedTerms) {
24
+ console.log(` ${t.count}× "${t.query}"`);
25
+ }
26
+ }
27
+
28
+ if (analytics.zeroResultQueries.length > 0) {
29
+ console.log("\nZero-result queries (consider indexing these topics):");
30
+ for (const q of analytics.zeroResultQueries) {
31
+ console.log(` ${q.count}× "${q.query}"`);
32
+ }
33
+ }
34
+
35
+ if (analytics.lowScoreQueries.length > 0) {
36
+ console.log("\nLow-relevance queries (top score < 0.3):");
37
+ for (const q of analytics.lowScoreQueries) {
38
+ console.log(` "${q.query}" (score: ${q.topScore.toFixed(2)})`);
39
+ }
40
+ }
41
+
42
+ // Trend comparison vs prior period
43
+ const trend = db.getAnalyticsTrend(days);
44
+ if (trend.previous.totalQueries > 0 || trend.current.totalQueries > 0) {
45
+ const arrow = (delta: number) => delta > 0 ? `+${delta}` : `${delta}`;
46
+ const pctArrow = (delta: number) =>
47
+ delta > 0 ? `+${(delta * 100).toFixed(1)}%` : `${(delta * 100).toFixed(1)}%`;
48
+
49
+ console.log(`\nTrend (current ${days}d vs prior ${days}d):`);
50
+ console.log(` Queries: ${trend.current.totalQueries} (${arrow(trend.delta.queries)})`);
51
+ if (trend.delta.avgTopScore !== null) {
52
+ console.log(` Avg top score: ${trend.current.avgTopScore?.toFixed(2)} (${trend.delta.avgTopScore >= 0 ? "+" : ""}${trend.delta.avgTopScore.toFixed(2)})`);
53
+ }
54
+ console.log(` Zero-result rate: ${(trend.current.zeroResultRate * 100).toFixed(0)}% (${pctArrow(trend.delta.zeroResultRate)})`);
55
+ }
56
+
57
+ db.close();
58
+ }
@@ -0,0 +1,30 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+ import { loadConfig } from "../../config";
4
+ import { loadBenchmarkQueries, runBenchmark, formatBenchmarkReport } from "../../search/benchmark";
5
+
6
+ export async function benchmarkCommand(args: string[], getFlag: (flag: string) => string | undefined) {
7
+ const file = args[1];
8
+ if (!file) {
9
+ console.error("Usage: local-rag benchmark <file> [--dir D] [--top N]");
10
+ process.exit(1);
11
+ }
12
+
13
+ const dir = resolve(getFlag("--dir") || ".");
14
+ const db = new RagDB(dir);
15
+ const config = await loadConfig(dir);
16
+ const top = parseInt(getFlag("--top") || String(config.benchmarkTopK), 10);
17
+
18
+ const queries = await loadBenchmarkQueries(resolve(file));
19
+ console.log(`Running ${queries.length} benchmark queries against ${dir}...\n`);
20
+
21
+ const summary = await runBenchmark(queries, db, dir, top);
22
+ console.log(formatBenchmarkReport(summary, top));
23
+
24
+ db.close();
25
+
26
+ // Exit with non-zero if below thresholds
27
+ if (summary.recallAtK < config.benchmarkMinRecall || summary.mrr < config.benchmarkMinMrr) {
28
+ process.exit(1);
29
+ }
30
+ }
@@ -0,0 +1,85 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+ import { embed } from "../../embeddings/embed";
4
+ import { discoverSessions } from "../../conversation/parser";
5
+
6
+ export async function checkpointCommand(args: string[], getFlag: (flag: string) => string | undefined) {
7
+ const subCommand = args[1];
8
+ const dir = resolve(getFlag("--dir") || ".");
9
+ const db = new RagDB(dir);
10
+
11
+ if (subCommand === "create") {
12
+ const type = args[2];
13
+ const title = args[3];
14
+ const summary = args[4];
15
+ if (!type || !title || !summary) {
16
+ console.error("Usage: local-rag checkpoint create <type> <title> <summary> [--dir D] [--files f1,f2] [--tags t1,t2]");
17
+ process.exit(1);
18
+ }
19
+
20
+ const filesStr = getFlag("--files");
21
+ const tagsStr = getFlag("--tags");
22
+ const filesInvolved = filesStr ? filesStr.split(",").map((f) => f.trim()) : [];
23
+ const tags = tagsStr ? tagsStr.split(",").map((t) => t.trim()) : [];
24
+
25
+ const sessions = discoverSessions(dir);
26
+ const sessionId = sessions.length > 0 ? sessions[0].sessionId : "unknown";
27
+ const turnCount = db.getTurnCount(sessionId);
28
+ const turnIndex = Math.max(0, turnCount - 1);
29
+
30
+ const embedding = await embed(`${title}. ${summary}`);
31
+ const id = db.createCheckpoint(
32
+ sessionId, turnIndex, new Date().toISOString(),
33
+ type, title, summary, filesInvolved, tags, embedding
34
+ );
35
+ console.log(`Checkpoint #${id} created: [${type}] ${title}`);
36
+ } else if (subCommand === "list") {
37
+ const type = getFlag("--type");
38
+ const top = parseInt(getFlag("--top") || "20", 10);
39
+ const checkpoints = db.listCheckpoints(undefined, type, top);
40
+
41
+ if (checkpoints.length === 0) {
42
+ console.log("No checkpoints found.");
43
+ } else {
44
+ for (const cp of checkpoints) {
45
+ const tagStr = cp.tags.length > 0 ? ` [${cp.tags.join(", ")}]` : "";
46
+ console.log(`#${cp.id} [${cp.type}] ${cp.title}${tagStr}`);
47
+ console.log(` ${cp.timestamp} (turn ${cp.turnIndex})`);
48
+ console.log(` ${cp.summary}`);
49
+ if (cp.filesInvolved.length > 0) {
50
+ console.log(` Files: ${cp.filesInvolved.join(", ")}`);
51
+ }
52
+ console.log();
53
+ }
54
+ }
55
+ } else if (subCommand === "search") {
56
+ const query = args[2];
57
+ if (!query) {
58
+ console.error("Usage: local-rag checkpoint search <query> [--dir D] [--type T] [--top N]");
59
+ process.exit(1);
60
+ }
61
+
62
+ const type = getFlag("--type");
63
+ const top = parseInt(getFlag("--top") || "5", 10);
64
+ const queryEmb = await embed(query);
65
+ const results = db.searchCheckpoints(queryEmb, top, type);
66
+
67
+ if (results.length === 0) {
68
+ console.log("No matching checkpoints found.");
69
+ } else {
70
+ for (const cp of results) {
71
+ console.log(`${cp.score.toFixed(4)} #${cp.id} [${cp.type}] ${cp.title}`);
72
+ console.log(` ${cp.summary}`);
73
+ if (cp.filesInvolved.length > 0) {
74
+ console.log(` Files: ${cp.filesInvolved.join(", ")}`);
75
+ }
76
+ console.log();
77
+ }
78
+ }
79
+ } else {
80
+ console.error("Usage: local-rag checkpoint <create|list|search>");
81
+ process.exit(1);
82
+ }
83
+
84
+ db.close();
85
+ }
@@ -0,0 +1,102 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+ import { loadConfig } from "../../config";
4
+ import { embed } from "../../embeddings/embed";
5
+ import { discoverSessions } from "../../conversation/parser";
6
+ import { indexConversation } from "../../conversation/indexer";
7
+
8
+ export async function conversationCommand(args: string[], getFlag: (flag: string) => string | undefined) {
9
+ const subCommand = args[1];
10
+ const dir = resolve(getFlag("--dir") || ".");
11
+ const db = new RagDB(dir);
12
+
13
+ if (subCommand === "search") {
14
+ const query = args[2];
15
+ if (!query) {
16
+ console.error("Usage: local-rag conversation search <query> [--dir D] [--top N]");
17
+ process.exit(1);
18
+ }
19
+
20
+ const config = await loadConfig(dir);
21
+ const top = parseInt(getFlag("--top") || String(config.searchTopK), 10);
22
+
23
+ // Ensure conversations are indexed
24
+ const sessions = discoverSessions(dir);
25
+ for (const session of sessions) {
26
+ const existing = db.getSession(session.sessionId);
27
+ if (!existing || existing.mtime < session.mtime) {
28
+ await indexConversation(session.jsonlPath, session.sessionId, db);
29
+ }
30
+ }
31
+
32
+ // Hybrid search
33
+ const queryEmb = await embed(query);
34
+ const vecResults = db.searchConversation(queryEmb, top);
35
+ let bm25Results: typeof vecResults = [];
36
+ try {
37
+ bm25Results = db.textSearchConversation(query, top);
38
+ } catch { /* FTS can fail on special chars */ }
39
+
40
+ const merged = new Map<number, (typeof vecResults)[0]>();
41
+ for (const r of vecResults) {
42
+ merged.set(r.turnId, { ...r, score: r.score * config.hybridWeight });
43
+ }
44
+ for (const r of bm25Results) {
45
+ const existing = merged.get(r.turnId);
46
+ if (existing) {
47
+ existing.score += r.score * (1 - config.hybridWeight);
48
+ } else {
49
+ merged.set(r.turnId, { ...r, score: r.score * (1 - config.hybridWeight) });
50
+ }
51
+ }
52
+
53
+ const results = [...merged.values()].sort((a, b) => b.score - a.score).slice(0, top);
54
+
55
+ if (results.length === 0) {
56
+ console.log("No conversation results found.");
57
+ } else {
58
+ for (const r of results) {
59
+ const tools = r.toolsUsed.length > 0 ? ` [${r.toolsUsed.join(", ")}]` : "";
60
+ console.log(`Turn ${r.turnIndex} (${r.timestamp})${tools}`);
61
+ console.log(` ${r.snippet.slice(0, 200)}`);
62
+ if (r.filesReferenced.length > 0) {
63
+ console.log(` Files: ${r.filesReferenced.slice(0, 5).join(", ")}`);
64
+ }
65
+ console.log();
66
+ }
67
+ }
68
+ } else if (subCommand === "sessions") {
69
+ const sessions = discoverSessions(dir);
70
+ if (sessions.length === 0) {
71
+ console.log("No conversation sessions found for this project.");
72
+ } else {
73
+ for (const s of sessions) {
74
+ const indexed = db.getSession(s.sessionId);
75
+ const status = indexed ? `${indexed.turnCount} turns indexed` : "not indexed";
76
+ const date = new Date(s.mtime).toISOString().slice(0, 19);
77
+ console.log(` ${s.sessionId.slice(0, 8)}... ${date} ${status} (${(s.size / 1024).toFixed(0)}KB)`);
78
+ }
79
+ }
80
+ } else if (subCommand === "index") {
81
+ const sessions = discoverSessions(dir);
82
+ if (sessions.length === 0) {
83
+ console.log("No conversation sessions found for this project.");
84
+ } else {
85
+ console.log(`Found ${sessions.length} sessions, indexing...`);
86
+ let totalTurns = 0;
87
+ for (const session of sessions) {
88
+ const result = await indexConversation(session.jsonlPath, session.sessionId, db);
89
+ totalTurns += result.turnsIndexed;
90
+ if (result.turnsIndexed > 0) {
91
+ console.log(` ${session.sessionId.slice(0, 8)}...: ${result.turnsIndexed} turns`);
92
+ }
93
+ }
94
+ console.log(`Done: ${totalTurns} turns indexed across ${sessions.length} sessions`);
95
+ }
96
+ } else {
97
+ console.error("Usage: local-rag conversation <search|sessions|index>");
98
+ process.exit(1);
99
+ }
100
+
101
+ db.close();
102
+ }
@@ -0,0 +1,119 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+ import { loadConfig } from "../../config";
4
+ import { indexDirectory } from "../../indexing/indexer";
5
+ import { search, searchChunks } from "../../search/hybrid";
6
+ import { cliProgress } from "../progress";
7
+
8
+ const CYAN = "\x1b[36m";
9
+ const GREEN = "\x1b[32m";
10
+ const YELLOW = "\x1b[33m";
11
+ const DIM = "\x1b[2m";
12
+ const BOLD = "\x1b[1m";
13
+ const RESET = "\x1b[0m";
14
+
15
+ function header(text: string) {
16
+ console.log(`\n${BOLD}${CYAN}--- ${text} ---${RESET}\n`);
17
+ }
18
+
19
+ function pause(ms: number): Promise<void> {
20
+ return new Promise((r) => setTimeout(r, ms));
21
+ }
22
+
23
+ export async function demoCommand(args: string[]) {
24
+ const dir = resolve(args[1] && !args[1].startsWith("--") ? args[1] : ".");
25
+
26
+ console.log(`${BOLD}local-rag demo${RESET}`);
27
+ console.log(`${DIM}Running against: ${dir}${RESET}`);
28
+
29
+ // Step 1: Index
30
+ header("1. Index your project");
31
+ console.log("Indexing files with AST-aware chunking...\n");
32
+
33
+ const db = new RagDB(dir);
34
+ const config = await loadConfig(dir);
35
+ const result = await indexDirectory(dir, db, config, cliProgress);
36
+ console.log(
37
+ `\n${GREEN}Done:${RESET} ${result.indexed} indexed, ${result.skipped} skipped, ${result.pruned} pruned`
38
+ );
39
+ await pause(500);
40
+
41
+ // Step 2: Semantic search
42
+ header("2. Semantic search");
43
+ const demoQuery = "how does search work";
44
+ console.log(`${DIM}> search "${demoQuery}"${RESET}\n`);
45
+
46
+ const searchResults = await search(demoQuery, db, 3, 0, config.hybridWeight, config.enableReranking);
47
+ if (searchResults.length > 0) {
48
+ for (const r of searchResults) {
49
+ console.log(` ${YELLOW}${r.score.toFixed(4)}${RESET} ${r.path}`);
50
+ const preview = r.snippets[0]?.slice(0, 100).replace(/\n/g, " ");
51
+ console.log(` ${DIM}${preview}...${RESET}\n`);
52
+ }
53
+ } else {
54
+ console.log(" No results — try a query related to your project.");
55
+ }
56
+ await pause(500);
57
+
58
+ // Step 3: Chunk-level retrieval
59
+ header("3. Chunk-level retrieval (read_relevant)");
60
+ console.log(`${DIM}> read_relevant "${demoQuery}"${RESET}\n`);
61
+
62
+ const chunks = await searchChunks(demoQuery, db, 2, 0.3, config.hybridWeight, config.enableReranking);
63
+ if (chunks.length > 0) {
64
+ for (const r of chunks) {
65
+ const lineRange = r.startLine != null && r.endLine != null ? `:${r.startLine}-${r.endLine}` : "";
66
+ const entity = r.entityName ? ` ${CYAN}${r.entityName}${RESET}` : "";
67
+ console.log(` ${YELLOW}[${r.score.toFixed(2)}]${RESET} ${r.path}${lineRange}${entity}`);
68
+ // Show first 3 lines of content
69
+ const lines = r.content.split("\n").slice(0, 3);
70
+ for (const line of lines) {
71
+ console.log(` ${DIM}${line}${RESET}`);
72
+ }
73
+ console.log();
74
+ }
75
+ } else {
76
+ console.log(" No chunks above threshold.");
77
+ }
78
+ await pause(500);
79
+
80
+ // Step 4: Symbol search
81
+ header("4. Symbol search (find_usages)");
82
+ const symbols = db.searchSymbols("search", false, undefined, 3);
83
+ if (symbols.length > 0) {
84
+ console.log(`${DIM}> search_symbols "search"${RESET}\n`);
85
+ for (const s of symbols) {
86
+ console.log(` ${s.path} ${CYAN}${s.symbolName}${RESET} (${s.symbolType})`);
87
+ }
88
+ } else {
89
+ console.log(" No exported symbols found matching 'search'.");
90
+ }
91
+ await pause(500);
92
+
93
+ // Step 5: Project map
94
+ header("5. Project map");
95
+ console.log(
96
+ `${DIM}The project_map tool generates a Mermaid dependency graph\n` +
97
+ `showing how files import from each other. Run:${RESET}\n\n` +
98
+ ` local-rag map ${dir}\n`
99
+ );
100
+
101
+ // Step 6: Unique features summary
102
+ header("6. What no other tool does");
103
+ console.log(` ${GREEN}search_conversation${RESET} Search past AI session history`);
104
+ console.log(` ${GREEN}create_checkpoint${RESET} Mark decisions, milestones, blockers`);
105
+ console.log(` ${GREEN}annotate${RESET} Attach notes to files/symbols (surface in search)`);
106
+ console.log(` ${GREEN}find_usages${RESET} Find all call sites before refactoring`);
107
+ console.log(` ${GREEN}project_map${RESET} Mermaid dependency graph`);
108
+ console.log(` ${GREEN}git_context${RESET} Uncommitted changes + index status`);
109
+ console.log(` ${GREEN}search_analytics${RESET} Find documentation gaps`);
110
+ console.log(` ${GREEN}write_relevant${RESET} Find best insertion point for new code`);
111
+
112
+ console.log(`\n${BOLD}Done.${RESET} Add to your editor with:\n`);
113
+ console.log(` ${DIM}# Claude Code (plugin)${RESET}`);
114
+ console.log(` /plugin install local-rag@claude-plugins-official\n`);
115
+ console.log(` ${DIM}# Any MCP client${RESET}`);
116
+ console.log(` bunx @winci/local-rag serve\n`);
117
+
118
+ db.close();
119
+ }
@@ -0,0 +1,31 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+ import { loadConfig } from "../../config";
4
+ import { loadEvalTasks, runEval, formatEvalReport, saveEvalTraces } from "../../search/eval";
5
+
6
+ export async function evalCommand(args: string[], getFlag: (flag: string) => string | undefined) {
7
+ const file = args[1];
8
+ if (!file) {
9
+ console.error("Usage: local-rag eval <file> [--dir D] [--top N] [--out F]");
10
+ process.exit(1);
11
+ }
12
+
13
+ const dir = resolve(getFlag("--dir") || ".");
14
+ const outPath = getFlag("--out");
15
+ const db = new RagDB(dir);
16
+ const config = await loadConfig(dir);
17
+ const top = parseInt(getFlag("--top") || String(config.benchmarkTopK), 10);
18
+
19
+ const tasks = await loadEvalTasks(resolve(file));
20
+ console.log(`Running A/B eval with ${tasks.length} tasks against ${dir}...\n`);
21
+
22
+ const summary = await runEval(tasks, db, dir, top);
23
+ console.log(formatEvalReport(summary));
24
+
25
+ if (outPath) {
26
+ await saveEvalTraces(summary.traces, resolve(outPath));
27
+ console.log(`\nTraces saved to ${outPath}`);
28
+ }
29
+
30
+ db.close();
31
+ }
@@ -0,0 +1,26 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+ import { loadConfig } from "../../config";
4
+ import { indexDirectory } from "../../indexing/indexer";
5
+ import { cliProgress } from "../progress";
6
+
7
+ export async function indexCommand(args: string[], getFlag: (flag: string) => string | undefined) {
8
+ const dir = resolve(args[1] && !args[1].startsWith("--") ? args[1] : ".");
9
+ const db = new RagDB(dir);
10
+ const config = await loadConfig(dir);
11
+
12
+ const patternsStr = getFlag("--patterns");
13
+ if (patternsStr) {
14
+ config.include = patternsStr.split(",").map((p) => p.trim());
15
+ }
16
+
17
+ console.log(`Indexing ${dir}...`);
18
+ const result = await indexDirectory(dir, db, config, cliProgress);
19
+ console.log(
20
+ `\nDone: ${result.indexed} indexed, ${result.skipped} skipped, ${result.pruned} pruned`
21
+ );
22
+ if (result.errors.length > 0) {
23
+ console.error(`Errors: ${result.errors.join("\n ")}`);
24
+ }
25
+ db.close();
26
+ }
@@ -0,0 +1,35 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+ import { loadConfig } from "../../config";
4
+ import { indexDirectory } from "../../indexing/indexer";
5
+ import { runSetup, mcpConfigSnippet, detectAgentHints, confirm } from "../setup";
6
+ import { cliProgress } from "../progress";
7
+
8
+ export async function initCommand(args: string[], getFlag: (flag: string) => string | undefined) {
9
+ const dir = resolve(args[1] && !args[1].startsWith("--") ? args[1] : ".");
10
+ const { actions } = await runSetup(dir);
11
+ if (actions.length === 0) {
12
+ console.log("Already set up — nothing to do.");
13
+ } else {
14
+ for (const action of actions) console.log(action);
15
+ }
16
+
17
+ console.log("\nAdd this to your agent's MCP config (mcpServers):\n");
18
+ console.log(mcpConfigSnippet(dir));
19
+ const hints = detectAgentHints(dir);
20
+ console.log();
21
+ for (const hint of hints) console.log(` ${hint}`);
22
+
23
+ console.log();
24
+ const shouldIndex = await confirm("Index project now? [Y/n] ");
25
+ if (shouldIndex) {
26
+ const db = new RagDB(dir);
27
+ const config = await loadConfig(dir);
28
+ console.log(`Indexing ${dir}...`);
29
+ const result = await indexDirectory(dir, db, config, cliProgress);
30
+ console.log(
31
+ `\nDone: ${result.indexed} indexed, ${result.skipped} skipped, ${result.pruned} pruned`
32
+ );
33
+ db.close();
34
+ }
35
+ }
@@ -0,0 +1,21 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+ import { generateProjectMap } from "../../graph/resolver";
4
+
5
+ export async function mapCommand(args: string[], getFlag: (flag: string) => string | undefined) {
6
+ const dir = resolve(args[1] && !args[1].startsWith("--") ? args[1] : ".");
7
+ const db = new RagDB(dir);
8
+ const focus = getFlag("--focus");
9
+ const zoom = (getFlag("--zoom") || "file") as "file" | "directory";
10
+ const max = parseInt(getFlag("--max") || "50", 10);
11
+
12
+ const map = generateProjectMap(db, {
13
+ projectDir: dir,
14
+ focus: focus ?? undefined,
15
+ zoom,
16
+ maxNodes: max,
17
+ });
18
+
19
+ console.log(map);
20
+ db.close();
21
+ }
@@ -0,0 +1,15 @@
1
+ import { resolve } from "path";
2
+ import { RagDB } from "../../db";
3
+
4
+ export async function removeCommand(args: string[]) {
5
+ const file = args[1];
6
+ if (!file) {
7
+ console.error("Usage: local-rag remove <file> [dir]");
8
+ process.exit(1);
9
+ }
10
+ const dir = resolve(args[2] && !args[2].startsWith("--") ? args[2] : ".");
11
+ const db = new RagDB(dir);
12
+ const removed = db.removeFile(resolve(file));
13
+ console.log(removed ? `Removed ${file}` : `${file} was not in the index`);
14
+ db.close();
15
+ }