nano-brain 2026.4.1 → 2026.4.2

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.
@@ -2,35 +2,46 @@
2
2
  description: Initialize nano-brain persistent memory for the current workspace.
3
3
  ---
4
4
 
5
- ## Prerequisites
5
+ ## Steps
6
6
 
7
- nano-brain MCP server must be configured in opencode.json. If `memory_status` tool is not available, tell user to add nano-brain to their MCP config first.
7
+ **Try MCP first, fall back to CLI.**
8
8
 
9
- ## Steps
9
+ ### Option A: MCP tools available
10
10
 
11
11
  1. Call `memory_status` tool to check current state
12
12
  - If codebase > 0 docs: already initialized, skip to step 4
13
- - If error "tool not found": MCP not configured, stop and instruct user
13
+ - If error "tool not found": use Option B
14
14
 
15
15
  2. Call `memory_index_codebase` with `root` = current workspace path
16
- - This indexes source files (respects .gitignore)
17
16
 
18
17
  3. Call `memory_update` to index sessions and curated notes
19
18
 
20
- 4. Call `memory_status` again and report:
21
- - Total documents indexed
22
- - Pending embeddings count
23
- - Embedding server status (connected/disconnected)
19
+ 4. Call `memory_status` again and report results
20
+
21
+ ### Option B: MCP tools NOT available (fallback)
22
+
23
+ Run via Bash:
24
+
25
+ ```bash
26
+ npx nano-brain init --root=$(pwd)
27
+ ```
28
+
29
+ This does everything: indexes codebase + harvests sessions + indexes collections + generates embeddings.
30
+
31
+ ### Detection
32
+
33
+ Try `memory_status` first. If it errors with "tool not found" or "MCP server not found", use Option B.
24
34
 
25
35
  ## Output Format
26
36
 
27
37
  ```
28
38
  nano-brain initialized:
29
39
  - Codebase: X files
30
- - Sessions: Y documents
40
+ - Symbol graph: A symbols, B edges
41
+ - Sessions: Y documents
31
42
  - Pending embeddings: Z (processing in background)
32
- - Embedding server: connected / disconnected
43
+ - Embedding server: connected / disconnected
33
44
  ```
34
45
 
35
46
  If pending embeddings > 0, explain they process automatically when MCP server runs.
36
- If embedding server disconnected, suggest: "Start Ollama: `ollama serve`"
47
+ If embedding server disconnected, suggest checking config.yml embedding settings.
@@ -7,25 +7,50 @@ description: Rescan codebase and refresh all nano-brain indexes after branch swi
7
7
  - After `git checkout`, `git pull`, or branch switch
8
8
  - After major code changes (new files, deleted files, refactors)
9
9
  - When search results seem stale or missing recent changes
10
+ - When `code_context` / `code_impact` return "symbol not found"
10
11
 
11
12
  ## Steps
12
13
 
13
- 1. Call `memory_index_codebase` with `root` = current workspace path
14
- - Detects new, changed, and deleted files via content hash
15
- - Only re-indexes what changed (incremental)
14
+ **Try MCP first, fall back to CLI.**
15
+
16
+ ### Option A: MCP tools available
16
17
 
18
+ 1. Call `memory_index_codebase` with `root` = current workspace path
17
19
  2. Call `memory_update` to refresh session and note indexes
20
+ 3. Call `memory_status` and report results
21
+
22
+ ### Option B: MCP tools NOT available (fallback)
23
+
24
+ Run via Bash:
25
+
26
+ ```bash
27
+ npx nano-brain reindex
28
+ ```
29
+
30
+ If `reindex` command is not yet available (older version), use:
31
+
32
+ ```bash
33
+ npx nano-brain init
34
+ ```
35
+
36
+ Note: `init` is safe without `--force` — it preserves all existing data and only re-scans.
37
+
38
+ ### Detection
39
+
40
+ Try `memory_status` first. If it errors with "tool not found" or "MCP server not found", use Option B.
18
41
 
19
- 3. Call `memory_status` and report:
42
+ ## Output Format
20
43
 
21
44
  ```
22
45
  Reindex complete:
23
- - Codebase: X files (Y new, Z updated)
46
+ - Codebase: X files (Y new, Z unchanged)
47
+ - Symbol graph: A symbols, B edges
24
48
  - Pending embeddings: N
25
49
  ```
26
50
 
27
51
  ## Notes
28
52
 
29
53
  - Reindexing is incremental — unchanged files are skipped
54
+ - Symbol graph (code_symbols, symbol_edges) is rebuilt for changed files
30
55
  - New/changed files need embedding — happens in background
31
56
  - If many pending embeddings, they process automatically over time
@@ -4,9 +4,26 @@ description: Show nano-brain memory health and statistics.
4
4
 
5
5
  ## Steps
6
6
 
7
+ **Try MCP first, fall back to CLI.**
8
+
9
+ ### Option A: MCP tools available
10
+
7
11
  1. Call `memory_status` tool
12
+ 2. Present results (see format below)
13
+
14
+ ### Option B: MCP tools NOT available (fallback)
15
+
16
+ Run via Bash:
17
+
18
+ ```bash
19
+ npx nano-brain status
20
+ ```
21
+
22
+ ### Detection
23
+
24
+ Try `memory_status` first. If it errors with "tool not found" or "MCP server not found", use Option B.
8
25
 
9
- 2. Present results in this format:
26
+ ## Output Format
10
27
 
11
28
  ```
12
29
  nano-brain Status
@@ -17,7 +34,7 @@ Documents: X total
17
34
  - memory: C notes
18
35
 
19
36
  Embeddings: Y embedded, Z pending
20
- Server: connected (model) / disconnected
37
+ Server: connected (model) / disconnected
21
38
  ```
22
39
 
23
40
  ## Suggested Actions
@@ -28,5 +45,5 @@ Based on status, suggest ONE relevant action:
28
45
  |-----------|------------|
29
46
  | codebase = 0 | "Run `/nano-brain-init` to index this workspace" |
30
47
  | pending > 100 | "Embeddings processing in background. Check again in a few minutes." |
31
- | server disconnected | "Start Ollama: `ollama serve`" |
32
- | all good | "Memory system healthy. Use `memory_query` to search." |
48
+ | server disconnected | "Check config.yml embedding settings" |
49
+ | all good | "Memory system healthy." |
package/AGENTS.md CHANGED
@@ -55,6 +55,7 @@ npx nano-brain update
55
55
 
56
56
 
57
57
 
58
+
58
59
  ## File Writing Rules (MANDATORY)
59
60
 
60
61
  **NEVER write an entire file at once.** Always use chunk-by-chunk editing:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-brain",
3
- "version": "2026.4.1",
3
+ "version": "2026.4.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "nano-brain": "./bin/cli.js"
package/src/codebase.ts CHANGED
@@ -440,6 +440,10 @@ export async function indexCodebase(
440
440
  if (flowResult.flowsDetected > 0) {
441
441
  log('codebase', `Flow detection: ${flowResult.flowsDetected} flows from ${flowResult.entryPointsFound} entry points`)
442
442
  }
443
+ } else if (!db && isTreeSitterAvailable()) {
444
+ log('codebase', 'WARNING: db parameter not provided — symbol graph indexing skipped. Pass db to indexCodebase() to enable code intelligence.')
445
+ } else if (db && !isTreeSitterAvailable()) {
446
+ log('codebase', 'WARNING: tree-sitter not available — symbol graph indexing skipped. Install tree-sitter-wasms to enable code intelligence.')
443
447
  }
444
448
 
445
449
  const finalStorageUsed = store.getCollectionStorageSize('codebase')
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@ import { indexCodebase, embedPendingCodebase, getCodebaseStats } from './codebas
9
9
  import { findCycles } from './graph.js';
10
10
  import { handleBench } from './bench.js';
11
11
  import { resolveHostUrl } from './host.js';
12
+ import { SymbolGraph } from './symbol-graph.js';
13
+ import { isTreeSitterAvailable } from './treesitter.js';
12
14
  import { QdrantVecStore } from './providers/qdrant.js';
13
15
  import { createVectorStore } from './vector-store.js';
14
16
  import type { SearchResult, CollectionConfig, Store } from './types.js';
@@ -144,6 +146,20 @@ nano-brain - Memory system with hybrid search
144
146
  --type=<type> Symbol type (required)
145
147
  --pattern=<pat> Pattern to analyze (required)
146
148
  --json Output as JSON
149
+ context <name> 360° view of a code symbol (callers, callees, flows)
150
+ --file=<path> Disambiguate when multiple symbols share the name
151
+ --json Output as JSON
152
+ code-impact <name> Analyze impact of changing a symbol
153
+ --direction=<d> upstream (callers) or downstream (callees), default: upstream
154
+ --max-depth=<n> Max traversal depth (default: 5)
155
+ --min-confidence=<n> Min edge confidence 0-1 (default: 0)
156
+ --file=<path> Disambiguate symbol
157
+ --json Output as JSON
158
+ detect-changes Map git changes to affected symbols and flows
159
+ --scope=<s> unstaged, staged, or all (default: all)
160
+ --json Output as JSON
161
+ reindex Re-index codebase files and symbol graph
162
+ --root=<path> Workspace root (default: current directory)
147
163
  reset Delete nano-brain data (selective or all)
148
164
  --databases Delete SQLite workspace databases (~/.nano-brain/data/*.sqlite)
149
165
  --sessions Delete harvested session markdown (~/.nano-brain/sessions/)
@@ -250,6 +266,7 @@ async function handleMcp(globalOpts: GlobalOptions, commandArgs: string[]): Prom
250
266
  let port = 8282;
251
267
  let host = '127.0.0.1';
252
268
  let daemon = false;
269
+ let root: string | undefined;
253
270
 
254
271
  for (const arg of commandArgs) {
255
272
  if (arg === '--http') {
@@ -258,6 +275,8 @@ async function handleMcp(globalOpts: GlobalOptions, commandArgs: string[]): Prom
258
275
  port = parseInt(arg.substring(7), 10);
259
276
  } else if (arg.startsWith('--host=')) {
260
277
  host = arg.substring(7);
278
+ } else if (arg.startsWith('--root=')) {
279
+ root = arg.substring(7);
261
280
  } else if (arg === '--daemon') {
262
281
  daemon = true;
263
282
  } else if (arg === 'stop') {
@@ -273,6 +292,7 @@ async function handleMcp(globalOpts: GlobalOptions, commandArgs: string[]): Prom
273
292
  httpPort: useHttp ? port : undefined,
274
293
  httpHost: useHttp ? host : undefined,
275
294
  daemon,
295
+ root,
276
296
  });
277
297
  }
278
298
 
@@ -283,10 +303,13 @@ async function handleServe(globalOpts: GlobalOptions, commandArgs: string[]): Pr
283
303
  let port = 3100;
284
304
  let foreground = false;
285
305
  let subcommand: string | undefined;
306
+ let root: string | undefined;
286
307
 
287
308
  for (const arg of commandArgs) {
288
309
  if (arg.startsWith('--port=')) {
289
310
  port = parseInt(arg.substring(7), 10);
311
+ } else if (arg.startsWith('--root=')) {
312
+ root = arg.substring(7);
290
313
  } else if (arg === '--foreground' || arg === '-f') {
291
314
  foreground = true;
292
315
  } else if (arg === 'stop' || arg === 'status') {
@@ -321,7 +344,7 @@ async function handleServe(globalOpts: GlobalOptions, commandArgs: string[]): Pr
321
344
 
322
345
  // serve (start)
323
346
  if (foreground) {
324
- return handleMcp(globalOpts, ['--http', `--port=${port}`, '--host=0.0.0.0', '--daemon']);
347
+ return handleMcp(globalOpts, ['--http', `--port=${port}`, '--host=0.0.0.0', '--daemon', ...(root ? [`--root=${root}`] : [])]);
325
348
  }
326
349
 
327
350
  // Check if already running
@@ -340,6 +363,9 @@ async function handleServe(globalOpts: GlobalOptions, commandArgs: string[]): Pr
340
363
 
341
364
  const cliPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../bin/cli.js');
342
365
  const args = [cliPath, 'mcp', '--http', `--port=${port}`, '--host=0.0.0.0', '--daemon'];
366
+ if (root) {
367
+ args.push(`--root=${root}`);
368
+ }
343
369
  if (globalOpts.configPath !== DEFAULT_CONFIG) {
344
370
  args.push(`--config=${globalOpts.configPath}`);
345
371
  }
@@ -653,6 +679,7 @@ async function handleStatus(globalOpts: GlobalOptions, commandArgs: string[]): P
653
679
  const workspaceRoot = process.cwd();
654
680
  const resolvedDbPath = resolveDbPath(globalOpts.dbPath, workspaceRoot);
655
681
  const workspaceName = extractWorkspaceName(resolvedDbPath);
682
+ const projectHash = crypto.createHash('sha256').update(workspaceRoot).digest('hex').substring(0, 12);
656
683
 
657
684
  let dbSize = 0;
658
685
  try {
@@ -696,6 +723,27 @@ async function handleStatus(globalOpts: GlobalOptions, commandArgs: string[]): P
696
723
  console.log('');
697
724
  }
698
725
 
726
+ // Code Intelligence (symbol graph)
727
+ try {
728
+ const symbolDb = new Database(resolvedDbPath);
729
+ const symbolCount = (symbolDb.prepare('SELECT COUNT(*) as cnt FROM code_symbols WHERE project_hash = ?').get(projectHash) as { cnt: number }).cnt;
730
+ const edgeCount = (symbolDb.prepare('SELECT COUNT(*) as cnt FROM symbol_edges WHERE project_hash = ?').get(projectHash) as { cnt: number }).cnt;
731
+ let flowCount = 0;
732
+ try {
733
+ flowCount = (symbolDb.prepare('SELECT COUNT(*) as cnt FROM execution_flows WHERE project_hash = ?').get(projectHash) as { cnt: number }).cnt;
734
+ } catch { /* table may not exist */ }
735
+ symbolDb.close();
736
+
737
+ console.log('Code Intelligence:');
738
+ console.log(` Symbols: ${symbolCount.toLocaleString()}`);
739
+ console.log(` Edges: ${edgeCount.toLocaleString()}`);
740
+ console.log(` Flows: ${flowCount.toLocaleString()}`);
741
+ if (symbolCount === 0) {
742
+ console.log(' ⚠️ Empty — run `npx nano-brain reindex` to populate');
743
+ }
744
+ console.log('');
745
+ } catch { /* code_symbols table may not exist in older DBs */ }
746
+
699
747
  await printEmbeddingServerStatus(config);
700
748
  console.log('');
701
749
 
@@ -1496,6 +1544,306 @@ async function handleImpact(globalOpts: GlobalOptions, commandArgs: string[]): P
1496
1544
  store.close()
1497
1545
  }
1498
1546
 
1547
+ function warnIfEmptySymbolGraph(db: Database.Database, projectHash: string): boolean {
1548
+ const count = db.prepare('SELECT COUNT(*) as cnt FROM code_symbols WHERE project_hash = ?').get(projectHash) as { cnt: number };
1549
+ if (count.cnt === 0) {
1550
+ console.error('⚠️ Symbol graph is empty. Run `npx nano-brain reindex` first.');
1551
+ return true;
1552
+ }
1553
+ return false;
1554
+ }
1555
+
1556
+ async function handleContext(globalOpts: GlobalOptions, commandArgs: string[]): Promise<void> {
1557
+ let name: string | undefined;
1558
+ let filePath: string | undefined;
1559
+ let format: 'text' | 'json' = 'text';
1560
+
1561
+ for (const arg of commandArgs) {
1562
+ if (arg.startsWith('--file=')) {
1563
+ filePath = arg.substring(7);
1564
+ } else if (arg === '--json') {
1565
+ format = 'json';
1566
+ } else if (!arg.startsWith('-')) {
1567
+ name = arg;
1568
+ }
1569
+ }
1570
+
1571
+ if (!name) {
1572
+ console.error('Usage: context <symbol-name> [--file=<path>] [--json]');
1573
+ process.exit(1);
1574
+ }
1575
+
1576
+ log('cli', 'context name=' + name + ' file=' + (filePath || ''));
1577
+ const workspaceRoot = process.cwd();
1578
+ const projectHash = crypto.createHash('sha256').update(workspaceRoot).digest('hex').substring(0, 12);
1579
+ const resolvedDbPath = resolveDbPath(globalOpts.dbPath, workspaceRoot);
1580
+ const db = new Database(resolvedDbPath);
1581
+
1582
+ if (warnIfEmptySymbolGraph(db, projectHash)) {
1583
+ db.close();
1584
+ return;
1585
+ }
1586
+
1587
+ const graph = new SymbolGraph(db);
1588
+ const result = graph.handleContext({ name, filePath, projectHash });
1589
+
1590
+ if (format === 'json') {
1591
+ console.log(JSON.stringify(result, null, 2));
1592
+ db.close();
1593
+ return;
1594
+ }
1595
+
1596
+ if (!result.found) {
1597
+ if (result.disambiguation) {
1598
+ console.log(`Multiple symbols named "${name}". Use --file= to disambiguate:`);
1599
+ for (const s of result.disambiguation) {
1600
+ console.log(` ${s.kind} ${s.name} — ${s.filePath}:${s.startLine}`);
1601
+ }
1602
+ } else {
1603
+ console.log(`Symbol "${name}" not found.`);
1604
+ }
1605
+ db.close();
1606
+ return;
1607
+ }
1608
+
1609
+ const sym = result.symbol!;
1610
+ console.log(`${sym.kind} ${sym.name}`);
1611
+ console.log(` File: ${sym.filePath}:${sym.startLine}-${sym.endLine}`);
1612
+ console.log(` Exported: ${sym.exported ? 'yes' : 'no'}`);
1613
+ if (result.clusterLabel) {
1614
+ console.log(` Cluster: ${result.clusterLabel}`);
1615
+ }
1616
+ console.log('');
1617
+
1618
+ if (result.incoming && result.incoming.length > 0) {
1619
+ console.log(`Callers (${result.incoming.length}):`);
1620
+ for (const e of result.incoming) {
1621
+ console.log(` ← ${e.kind} ${e.name} (${e.filePath}) [${e.edgeType}]`);
1622
+ }
1623
+ console.log('');
1624
+ }
1625
+
1626
+ if (result.outgoing && result.outgoing.length > 0) {
1627
+ console.log(`Callees (${result.outgoing.length}):`);
1628
+ for (const e of result.outgoing) {
1629
+ console.log(` → ${e.kind} ${e.name} (${e.filePath}) [${e.edgeType}]`);
1630
+ }
1631
+ console.log('');
1632
+ }
1633
+
1634
+ if (result.flows && result.flows.length > 0) {
1635
+ console.log(`Flows (${result.flows.length}):`);
1636
+ for (const f of result.flows) {
1637
+ console.log(` ${f.flowType}: ${f.label} (step ${f.stepIndex})`);
1638
+ }
1639
+ console.log('');
1640
+ }
1641
+
1642
+ if (result.infrastructureSymbols && result.infrastructureSymbols.length > 0) {
1643
+ console.log(`Infrastructure:`);
1644
+ for (const s of result.infrastructureSymbols) {
1645
+ console.log(` [${s.operation}] ${s.type}: ${s.pattern}`);
1646
+ }
1647
+ }
1648
+
1649
+ db.close();
1650
+ }
1651
+
1652
+ async function handleCodeImpact(globalOpts: GlobalOptions, commandArgs: string[]): Promise<void> {
1653
+ let target: string | undefined;
1654
+ let direction: 'upstream' | 'downstream' = 'upstream';
1655
+ let maxDepth = 5;
1656
+ let minConfidence = 0;
1657
+ let filePath: string | undefined;
1658
+ let format: 'text' | 'json' = 'text';
1659
+
1660
+ for (const arg of commandArgs) {
1661
+ if (arg.startsWith('--direction=')) {
1662
+ const val = arg.substring(12);
1663
+ if (val === 'upstream' || val === 'downstream') direction = val;
1664
+ } else if (arg.startsWith('--max-depth=')) {
1665
+ maxDepth = parseInt(arg.substring(12), 10);
1666
+ } else if (arg.startsWith('--min-confidence=')) {
1667
+ minConfidence = parseFloat(arg.substring(17));
1668
+ } else if (arg.startsWith('--file=')) {
1669
+ filePath = arg.substring(7);
1670
+ } else if (arg === '--json') {
1671
+ format = 'json';
1672
+ } else if (!arg.startsWith('-')) {
1673
+ target = arg;
1674
+ }
1675
+ }
1676
+
1677
+ if (!target) {
1678
+ console.error('Usage: code-impact <symbol-name> [--direction=upstream|downstream] [--max-depth=N] [--min-confidence=N] [--file=<path>] [--json]');
1679
+ process.exit(1);
1680
+ }
1681
+
1682
+ log('cli', 'code-impact target=' + target + ' direction=' + direction + ' depth=' + maxDepth);
1683
+ const workspaceRoot = process.cwd();
1684
+ const projectHash = crypto.createHash('sha256').update(workspaceRoot).digest('hex').substring(0, 12);
1685
+ const resolvedDbPath = resolveDbPath(globalOpts.dbPath, workspaceRoot);
1686
+ const db = new Database(resolvedDbPath);
1687
+
1688
+ if (warnIfEmptySymbolGraph(db, projectHash)) {
1689
+ db.close();
1690
+ return;
1691
+ }
1692
+
1693
+ const graph = new SymbolGraph(db);
1694
+ const result = graph.handleImpact({ target, direction, maxDepth, minConfidence, filePath, projectHash });
1695
+
1696
+ if (format === 'json') {
1697
+ console.log(JSON.stringify(result, null, 2));
1698
+ db.close();
1699
+ return;
1700
+ }
1701
+
1702
+ if (!result.found) {
1703
+ if (result.disambiguation) {
1704
+ console.log(`Multiple symbols named "${target}". Use --file= to disambiguate:`);
1705
+ for (const s of result.disambiguation) {
1706
+ console.log(` ${s.kind} ${s.name} — ${s.filePath}`);
1707
+ }
1708
+ } else {
1709
+ console.log(`Symbol "${target}" not found.`);
1710
+ }
1711
+ db.close();
1712
+ return;
1713
+ }
1714
+
1715
+ const t = result.target!;
1716
+ console.log(`Impact Analysis: ${t.kind} ${t.name} (${t.filePath})`);
1717
+ console.log(` Direction: ${direction}`);
1718
+ console.log(` Risk: ${result.risk}`);
1719
+ console.log(` Direct deps: ${result.summary.directDeps}, Total affected: ${result.summary.totalAffected}, Flows: ${result.summary.flowsAffected}`);
1720
+ console.log('');
1721
+
1722
+ for (const [depth, symbols] of Object.entries(result.byDepth)) {
1723
+ if (symbols.length > 0) {
1724
+ console.log(`Depth ${depth} (${symbols.length}):`);
1725
+ for (const s of symbols) {
1726
+ console.log(` ${s.kind} ${s.name} (${s.filePath}) [${s.edgeType}, confidence=${s.confidence}]`);
1727
+ }
1728
+ }
1729
+ }
1730
+
1731
+ if (result.affectedFlows.length > 0) {
1732
+ console.log('');
1733
+ console.log(`Affected Flows (${result.affectedFlows.length}):`);
1734
+ for (const f of result.affectedFlows) {
1735
+ console.log(` ${f.flowType}: ${f.label}`);
1736
+ }
1737
+ }
1738
+
1739
+ db.close();
1740
+ }
1741
+
1742
+ async function handleDetectChanges(globalOpts: GlobalOptions, commandArgs: string[]): Promise<void> {
1743
+ let scope: 'unstaged' | 'staged' | 'all' = 'all';
1744
+ let format: 'text' | 'json' = 'text';
1745
+
1746
+ for (const arg of commandArgs) {
1747
+ if (arg.startsWith('--scope=')) {
1748
+ const val = arg.substring(8);
1749
+ if (val === 'unstaged' || val === 'staged' || val === 'all') scope = val;
1750
+ } else if (arg === '--json') {
1751
+ format = 'json';
1752
+ }
1753
+ }
1754
+
1755
+ log('cli', 'detect-changes scope=' + scope);
1756
+ const workspaceRoot = process.cwd();
1757
+ const projectHash = crypto.createHash('sha256').update(workspaceRoot).digest('hex').substring(0, 12);
1758
+ const resolvedDbPath = resolveDbPath(globalOpts.dbPath, workspaceRoot);
1759
+ const db = new Database(resolvedDbPath);
1760
+
1761
+ if (warnIfEmptySymbolGraph(db, projectHash)) {
1762
+ db.close();
1763
+ return;
1764
+ }
1765
+
1766
+ const graph = new SymbolGraph(db);
1767
+ const result = graph.handleDetectChanges({ scope, workspaceRoot, projectHash });
1768
+
1769
+ if (format === 'json') {
1770
+ console.log(JSON.stringify(result, null, 2));
1771
+ db.close();
1772
+ return;
1773
+ }
1774
+
1775
+ if (result.changedFiles.length === 0) {
1776
+ console.log('No changed files detected.');
1777
+ db.close();
1778
+ return;
1779
+ }
1780
+
1781
+ console.log(`Risk Level: ${result.riskLevel}`);
1782
+ console.log('');
1783
+
1784
+ console.log(`Changed Files (${result.changedFiles.length}):`);
1785
+ for (const f of result.changedFiles) {
1786
+ console.log(` ${f}`);
1787
+ }
1788
+ console.log('');
1789
+
1790
+ if (result.changedSymbols.length > 0) {
1791
+ console.log(`Changed Symbols (${result.changedSymbols.length}):`);
1792
+ for (const s of result.changedSymbols) {
1793
+ console.log(` ${s.kind} ${s.name} (${s.filePath})`);
1794
+ }
1795
+ console.log('');
1796
+ }
1797
+
1798
+ if (result.affectedFlows.length > 0) {
1799
+ console.log(`Affected Flows (${result.affectedFlows.length}):`);
1800
+ for (const f of result.affectedFlows) {
1801
+ console.log(` ${f.flowType}: ${f.label}`);
1802
+ }
1803
+ }
1804
+
1805
+ db.close();
1806
+ }
1807
+
1808
+ async function handleReindex(globalOpts: GlobalOptions, commandArgs: string[]): Promise<void> {
1809
+ let root = process.cwd();
1810
+
1811
+ for (const arg of commandArgs) {
1812
+ if (arg.startsWith('--root=')) {
1813
+ root = arg.substring(7);
1814
+ }
1815
+ }
1816
+
1817
+ log('cli', 'reindex root=' + root);
1818
+ const resolvedDbPath = resolveDbPath(globalOpts.dbPath, root);
1819
+ const store = createStore(resolvedDbPath);
1820
+ const projectHash = crypto.createHash('sha256').update(root).digest('hex').substring(0, 12);
1821
+
1822
+ const config = loadCollectionConfig(globalOpts.configPath);
1823
+ const wsConfig = getWorkspaceConfig(config, root);
1824
+ const codebaseConfig = wsConfig?.codebase ?? { enabled: true };
1825
+
1826
+ console.log(`Reindexing codebase: ${root}`);
1827
+ const db = new Database(resolvedDbPath);
1828
+ const stats = await indexCodebase(store, root, codebaseConfig, projectHash, undefined, db);
1829
+ console.log(` Files: ${stats.filesIndexed} indexed, ${stats.filesSkippedUnchanged} unchanged`);
1830
+
1831
+ // Report symbol graph stats
1832
+ const symbolCount = (db.prepare('SELECT COUNT(*) as cnt FROM code_symbols WHERE project_hash = ?').get(projectHash) as { cnt: number }).cnt;
1833
+ const edgeCount = (db.prepare('SELECT COUNT(*) as cnt FROM symbol_edges WHERE project_hash = ?').get(projectHash) as { cnt: number }).cnt;
1834
+ console.log(` Symbols: ${symbolCount}, Edges: ${edgeCount}`);
1835
+
1836
+ if (symbolCount === 0 && isTreeSitterAvailable()) {
1837
+ console.log(' ⚠️ No symbols indexed. Check if your files contain supported languages.');
1838
+ } else if (!isTreeSitterAvailable()) {
1839
+ console.log(' ⚠️ Tree-sitter not available — symbol graph skipped.');
1840
+ }
1841
+
1842
+ db.close();
1843
+ store.close();
1844
+ console.log('✅ Reindex complete.');
1845
+ }
1846
+
1499
1847
  async function handleCache(globalOpts: GlobalOptions, commandArgs: string[]): Promise<void> {
1500
1848
  const subcommand = commandArgs[0];
1501
1849
 
@@ -2800,6 +3148,14 @@ async function main() {
2800
3148
  return handleReset(globalOpts, commandArgs);
2801
3149
  case 'rm':
2802
3150
  return handleRm(globalOpts, commandArgs);
3151
+ case 'context':
3152
+ return handleContext(globalOpts, commandArgs);
3153
+ case 'code-impact':
3154
+ return handleCodeImpact(globalOpts, commandArgs);
3155
+ case 'detect-changes':
3156
+ return handleDetectChanges(globalOpts, commandArgs);
3157
+ case 'reindex':
3158
+ return handleReindex(globalOpts, commandArgs);
2803
3159
  default:
2804
3160
  console.error(`Unknown command: ${command}`);
2805
3161
  showHelp();
package/src/server.ts CHANGED
@@ -31,6 +31,7 @@ export interface ServerOptions {
31
31
  httpPort?: number;
32
32
  httpHost?: string;
33
33
  daemon?: boolean;
34
+ root?: string;
34
35
  }
35
36
 
36
37
  export interface ServerDeps {
@@ -1417,7 +1418,7 @@ function setupSingletonGuard(pidPath: string, store: Store, stopWatcher: () => v
1417
1418
  }
1418
1419
 
1419
1420
  export async function startServer(options: ServerOptions): Promise<void> {
1420
- const { dbPath, configPath, httpPort, httpHost = '127.0.0.1', daemon } = options;
1421
+ const { dbPath, configPath, httpPort, httpHost = '127.0.0.1', daemon, root } = options;
1421
1422
 
1422
1423
  const homeDir = os.homedir();
1423
1424
  const nanoBrainHome = path.join(homeDir, '.nano-brain');
@@ -1432,11 +1433,20 @@ export async function startServer(options: ServerOptions): Promise<void> {
1432
1433
  const storageConfig = parseStorageConfig(config?.storage);
1433
1434
  let resolvedWorkspaceRoot: string;
1434
1435
  if (daemon && config?.workspaces && Object.keys(config.workspaces).length > 0) {
1435
- resolvedWorkspaceRoot = Object.keys(config.workspaces)[0];
1436
- log('server', 'Daemon mode: using first configured workspace as primary');
1437
- console.error(`[memory] Daemon mode: primary workspace = ${resolvedWorkspaceRoot}`);
1436
+ const configuredWorkspaces = Object.keys(config.workspaces);
1437
+ const cwd = root || process.cwd();
1438
+ const cwdMatch = configuredWorkspaces.find(ws => cwd === ws || cwd.startsWith(ws + '/'));
1439
+ if (cwdMatch) {
1440
+ resolvedWorkspaceRoot = cwdMatch;
1441
+ log('server', 'Daemon mode: cwd matches configured workspace');
1442
+ console.error(`[memory] Daemon mode: workspace from cwd = ${resolvedWorkspaceRoot}`);
1443
+ } else {
1444
+ resolvedWorkspaceRoot = configuredWorkspaces[0];
1445
+ log('server', 'Daemon mode: cwd does not match any workspace, using first configured');
1446
+ console.error(`[memory] Daemon mode: primary workspace = ${resolvedWorkspaceRoot}`);
1447
+ }
1438
1448
  } else {
1439
- resolvedWorkspaceRoot = process.cwd();
1449
+ resolvedWorkspaceRoot = root || process.cwd();
1440
1450
  }
1441
1451
  const wsConfig = getWorkspaceConfig(config, resolvedWorkspaceRoot);
1442
1452
  const resolvedCodebaseConfig = wsConfig.codebase;