55 * Local-first AI code review for your git diffs.
66 * Powered by Qwen2.5-Coder-3B running entirely on-device via WebAssembly.
77 *
8- * Usage:
9- * diffmind --branch main
10- * diffmind --branch develop --format json
118 * git diff main...HEAD | diffmind --stdin
129 */
1310
11+ import { Indexer } from "./indexer" ;
1412import { Command } from "commander" ;
1513import chalk from "chalk" ;
1614import ora from "ora" ;
@@ -46,10 +44,17 @@ const TOKENIZER_URL =
4644
4745const program = new Command ( ) ;
4846
47+ program
48+ . command ( "index" )
49+ . description ( "Build a symbol index of the local repository for context-aware reviews" )
50+ . action ( async ( ) => {
51+ await runIndexer ( ) ;
52+ } ) ;
53+
4954program
5055 . name ( "diffmind" )
5156 . description ( "Local-first AI code review for your git diffs" )
52- . version ( "0.2 .0" )
57+ . version ( "0.3 .0" )
5358 . option ( "-b, --branch <name>" , "Target branch to diff against" , "main" )
5459 . option (
5560 "-f, --format <type>" ,
@@ -67,8 +72,26 @@ program
6772 "low"
6873 )
6974 . option ( "--stdin" , "Read git diff from stdin instead of running git diff" )
70- . option ( "--no-color" , "Disable colored output" )
71- . parse ( process . argv ) ;
75+ . option ( "--no-color" , "Disable colored output" ) ;
76+
77+ // ─── Entry Point ──────────────────────────────────────────────────────────────
78+
79+ function run ( ) {
80+ program . parse ( process . argv ) ;
81+
82+ // If no subcommand is used, run the default analysis
83+ if ( ! program . args . length || program . args [ 0 ] !== "index" ) {
84+ main ( ) . catch ( ( err ) => {
85+ console . error ( chalk . red ( `Fatal Error: ${ err . message } ` ) ) ;
86+ process . exit ( 1 ) ;
87+ } ) ;
88+ }
89+ }
90+
91+ // Only run if executed directly
92+ if ( require . main === module ) {
93+ run ( ) ;
94+ }
7295
7396const opts = program . opts < {
7497 branch : string ;
@@ -80,12 +103,10 @@ const opts = program.opts<{
80103 context ? : string ;
81104} > ( ) ;
82105
83- // ─── Main ────── ───────────────────────────────────────────────────────────────
106+ // ─── Main Logic ───────────────────────────────────────────────────────────────
84107
85108import { Worker } from "worker_threads" ;
86109
87- // ─── Main ─────────────────────────────────────────────────────────────────────
88-
89110async function main ( ) : Promise < void > {
90111 printBanner ( ) ;
91112
@@ -109,7 +130,13 @@ async function main(): Promise<void> {
109130 }
110131 }
111132
112- // 4. Run analysis in a background worker
133+ // 4. Retrieve architectural context (Local RAG Phase 1)
134+ const ragContext = await getRagContext ( diff ) ;
135+ if ( ragContext ) {
136+ context = `${ context } \n\n### Architectural Reference (Local RAG):\n${ ragContext } ` ;
137+ }
138+
139+ // 5. Run analysis in a background worker
113140 // This keeps the process responsive and the spinner animated.
114141 const analyzeSpinner = ora ( "Initializing engine & analyzing diff..." ) . start ( ) ;
115142
@@ -132,7 +159,7 @@ async function main(): Promise<void> {
132159 process . exit ( 1 ) ;
133160 }
134161
135- // 5 . Format and output results
162+ // 6 . Format and output results
136163 const output =
137164 opts . format === "json"
138165 ? formatJson ( report )
@@ -190,6 +217,57 @@ function runAnalysisInWorker(workerData: {
190217 } ) ;
191218}
192219
220+ // ─── Local RAG ────────────────────────────────────────────────────────────────
221+
222+ async function runIndexer ( ) : Promise < void > {
223+ const spinner = ora ( "Scanning repository for symbols..." ) . start ( ) ;
224+ try {
225+ const indexer = new Indexer ( process . cwd ( ) ) ;
226+ const index = await indexer . buildIndex ( ) ;
227+ indexer . save ( index ) ;
228+ spinner . succeed ( `Index built: ${ Object . keys ( index . symbols ) . length } symbols found` ) ;
229+ } catch ( err ) {
230+ spinner . fail ( "Indexing failed" ) ;
231+ console . error ( chalk . red ( String ( err ) ) ) ;
232+ process . exit ( 1 ) ;
233+ }
234+ }
235+
236+ async function getRagContext ( diff : string ) : Promise < string | null > {
237+ const index = Indexer . load ( process . cwd ( ) ) ;
238+ if ( ! index ) return null ;
239+
240+ const foundSymbols = new Set < string > ( ) ;
241+ const lines = diff . split ( "\n" ) ;
242+
243+ // Simple heuristic: find all words that match a known symbol name in added/modified lines
244+ for ( const line of lines ) {
245+ if ( ! line . startsWith ( "+" ) || line . startsWith ( "+++" ) ) continue ;
246+
247+ const words = line . match ( / [ a - z A - Z 0 - 9 _ $ ] + / g) ;
248+ if ( ! words ) continue ;
249+
250+ for ( const word of words ) {
251+ if ( index . symbols [ word ] ) {
252+ foundSymbols . add ( word ) ;
253+ }
254+ }
255+ }
256+
257+ if ( foundSymbols . size === 0 ) return null ;
258+
259+ let contexts = "" ;
260+ // Pick top 10 symbols to avoid prompt overflow
261+ const symbolsToInclude = Array . from ( foundSymbols ) . slice ( 0 , 10 ) ;
262+
263+ for ( const symName of symbolsToInclude ) {
264+ const def = index . symbols [ symName ] ;
265+ contexts += `\n--- Symbol: ${ symName } (from ${ def . file } ) ---\n${ def . snippet } \n` ;
266+ }
267+
268+ return contexts ;
269+ }
270+
193271// ─── Diff Acquisition ─────────────────────────────────────────────────────────
194272
195273async function getDiff ( ) : Promise < string > {
@@ -395,7 +473,7 @@ export function categoryBadge(category: Category): string {
395473}
396474
397475function printBanner ( ) : void {
398- console . log ( chalk . cyan . bold ( "\n diffmind" ) + chalk . dim ( " v0.2 .0 — local-first AI code review" ) ) ;
476+ console . log ( chalk . cyan . bold ( "\n diffmind" ) + chalk . dim ( " v0.3 .0 — local-first AI code review" ) ) ;
399477 console . log ( chalk . dim ( " Model: Qwen2.5-Coder-3B-Instruct Q4_K_M | Inference: on-device Wasm\n" ) ) ;
400478}
401479
0 commit comments