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.
- package/.opencode/command/nano-brain-init.md +23 -12
- package/.opencode/command/nano-brain-reindex.md +30 -5
- package/.opencode/command/nano-brain-status.md +21 -4
- package/AGENTS.md +1 -0
- package/package.json +1 -1
- package/src/codebase.ts +4 -0
- package/src/index.ts +357 -1
- package/src/server.ts +15 -5
|
@@ -2,35 +2,46 @@
|
|
|
2
2
|
description: Initialize nano-brain persistent memory for the current workspace.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Steps
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Try MCP first, fall back to CLI.**
|
|
8
8
|
|
|
9
|
-
|
|
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":
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
-
|
|
40
|
+
- Symbol graph: A symbols, B edges
|
|
41
|
+
- Sessions: Y documents
|
|
31
42
|
- Pending embeddings: Z (processing in background)
|
|
32
|
-
- Embedding server:
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
42
|
+
## Output Format
|
|
20
43
|
|
|
21
44
|
```
|
|
22
45
|
Reindex complete:
|
|
23
|
-
- Codebase: X files (Y new, Z
|
|
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
|
-
|
|
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:
|
|
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 | "
|
|
32
|
-
| all good | "Memory system healthy.
|
|
48
|
+
| server disconnected | "Check config.yml embedding settings" |
|
|
49
|
+
| all good | "Memory system healthy." |
|
package/AGENTS.md
CHANGED
package/package.json
CHANGED
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
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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;
|