claude-brain 0.17.13 → 0.22.0
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/VERSION +1 -1
- package/package.json +3 -1
- package/scripts/postinstall.mjs +80 -104
- package/src/cli/auto-setup.ts +1 -9
- package/src/cli/bin.ts +23 -2
- package/src/cli/commands/export.ts +130 -0
- package/src/cli/commands/reindex.ts +107 -0
- package/src/cli/commands/serve.ts +54 -0
- package/src/cli/commands/status.ts +158 -0
- package/src/code-intelligence/indexer.ts +315 -0
- package/src/code-intelligence/linker.ts +178 -0
- package/src/code-intelligence/parser.ts +484 -0
- package/src/code-intelligence/query.ts +291 -0
- package/src/code-intelligence/schema.ts +83 -0
- package/src/code-intelligence/types.ts +95 -0
- package/src/config/defaults.ts +3 -3
- package/src/config/loader.ts +6 -0
- package/src/config/schema.ts +28 -2
- package/src/health/index.ts +5 -2
- package/src/hooks/brain-hook.ts +4 -1
- package/src/hooks/context-hook.ts +69 -10
- package/src/hooks/installer.ts +4 -7
- package/src/intelligence/cross-project/index.ts +1 -7
- package/src/intelligence/prediction/index.ts +1 -7
- package/src/intelligence/reasoning/index.ts +1 -7
- package/src/memory/compression.ts +105 -0
- package/src/memory/fts5-search.ts +456 -0
- package/src/memory/index.ts +342 -38
- package/src/memory/migrations/add-fts5.ts +98 -0
- package/src/memory/pruning.ts +60 -0
- package/src/routing/intent-classifier.ts +58 -1
- package/src/routing/response-filter.ts +128 -0
- package/src/routing/router.ts +457 -54
- package/src/server/http-api.ts +319 -1
- package/src/server/providers/resources.ts +1 -42
- package/src/server/services.ts +113 -12
- package/src/server/web-viewer.ts +1115 -0
- package/src/setup/index.ts +12 -22
- package/src/tools/schemas.ts +1 -1
- package/src/intelligence/cross-project/affinity.ts +0 -159
- package/src/intelligence/cross-project/transfer.ts +0 -201
- package/src/intelligence/prediction/context-anticipator.ts +0 -198
- package/src/intelligence/prediction/decision-predictor.ts +0 -184
- package/src/intelligence/reasoning/counterfactual.ts +0 -248
- package/src/intelligence/reasoning/synthesizer.ts +0 -167
- package/src/setup/wizard.ts +0 -459
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite'
|
|
2
|
+
import type { Logger } from 'pino'
|
|
3
|
+
import type { SymbolResult, FileResult, DependencyResult, FileMapEntry, IndexStats } from './types'
|
|
4
|
+
|
|
5
|
+
export class CodeQuery {
|
|
6
|
+
private db: Database
|
|
7
|
+
private logger: Logger
|
|
8
|
+
private memoryDb?: Database
|
|
9
|
+
|
|
10
|
+
constructor(db: Database, logger: Logger) {
|
|
11
|
+
this.db = db
|
|
12
|
+
this.logger = logger.child({ component: 'code-query' })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Phase 29: Set memory DB for file-linked memory annotations */
|
|
16
|
+
setMemoryDb(db: Database): void {
|
|
17
|
+
this.memoryDb = db
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
findSymbols(query: string, project: string, limit: number = 20): SymbolResult[] {
|
|
21
|
+
if (!query.trim()) return []
|
|
22
|
+
|
|
23
|
+
// First try exact match
|
|
24
|
+
const exact = this.db.query(
|
|
25
|
+
`SELECT symbol_name, symbol_type, file_path, line_start, line_end, signature
|
|
26
|
+
FROM code_symbols
|
|
27
|
+
WHERE project = ? AND symbol_name = ?
|
|
28
|
+
LIMIT ?`
|
|
29
|
+
).all(project, query, limit) as RawSymbolRow[]
|
|
30
|
+
|
|
31
|
+
if (exact.length > 0) {
|
|
32
|
+
return exact.map(row => ({
|
|
33
|
+
symbol: row.symbol_name,
|
|
34
|
+
type: row.symbol_type as any,
|
|
35
|
+
filePath: row.file_path,
|
|
36
|
+
lineStart: row.line_start,
|
|
37
|
+
lineEnd: row.line_end ?? undefined,
|
|
38
|
+
signature: row.signature ?? undefined,
|
|
39
|
+
confidence: 1.0,
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try prefix match
|
|
44
|
+
const prefix = this.db.query(
|
|
45
|
+
`SELECT symbol_name, symbol_type, file_path, line_start, line_end, signature
|
|
46
|
+
FROM code_symbols
|
|
47
|
+
WHERE project = ? AND symbol_name LIKE ? || '%'
|
|
48
|
+
LIMIT ?`
|
|
49
|
+
).all(project, query, limit) as RawSymbolRow[]
|
|
50
|
+
|
|
51
|
+
if (prefix.length > 0) {
|
|
52
|
+
return prefix.map(row => ({
|
|
53
|
+
symbol: row.symbol_name,
|
|
54
|
+
type: row.symbol_type as any,
|
|
55
|
+
filePath: row.file_path,
|
|
56
|
+
lineStart: row.line_start,
|
|
57
|
+
lineEnd: row.line_end ?? undefined,
|
|
58
|
+
signature: row.signature ?? undefined,
|
|
59
|
+
confidence: 0.9,
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fall back to FTS5 search
|
|
64
|
+
try {
|
|
65
|
+
const ftsResults = this.db.query(
|
|
66
|
+
`SELECT s.symbol_name, s.symbol_type, s.file_path, s.line_start, s.line_end, s.signature,
|
|
67
|
+
rank
|
|
68
|
+
FROM code_symbols_fts fts
|
|
69
|
+
JOIN code_symbols s ON s.id = fts.rowid
|
|
70
|
+
WHERE code_symbols_fts MATCH ? AND s.project = ?
|
|
71
|
+
ORDER BY rank
|
|
72
|
+
LIMIT ?`
|
|
73
|
+
).all(query, project, limit) as (RawSymbolRow & { rank: number })[]
|
|
74
|
+
|
|
75
|
+
if (ftsResults.length === 0) return []
|
|
76
|
+
|
|
77
|
+
// Normalize FTS5 rank to 0.5-0.85 confidence
|
|
78
|
+
const maxRank = Math.abs(ftsResults[0].rank) || 1
|
|
79
|
+
return ftsResults.map(row => ({
|
|
80
|
+
symbol: row.symbol_name,
|
|
81
|
+
type: row.symbol_type as any,
|
|
82
|
+
filePath: row.file_path,
|
|
83
|
+
lineStart: row.line_start,
|
|
84
|
+
lineEnd: row.line_end ?? undefined,
|
|
85
|
+
signature: row.signature ?? undefined,
|
|
86
|
+
confidence: 0.5 + 0.35 * (Math.abs(row.rank) / maxRank),
|
|
87
|
+
}))
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// FTS5 can throw on invalid query syntax
|
|
90
|
+
this.logger.debug({ error, query }, 'FTS5 query failed, falling back to LIKE')
|
|
91
|
+
|
|
92
|
+
const likeResults = this.db.query(
|
|
93
|
+
`SELECT symbol_name, symbol_type, file_path, line_start, line_end, signature
|
|
94
|
+
FROM code_symbols
|
|
95
|
+
WHERE project = ? AND symbol_name LIKE '%' || ? || '%'
|
|
96
|
+
LIMIT ?`
|
|
97
|
+
).all(project, query, limit) as RawSymbolRow[]
|
|
98
|
+
|
|
99
|
+
return likeResults.map(row => ({
|
|
100
|
+
symbol: row.symbol_name,
|
|
101
|
+
type: row.symbol_type as any,
|
|
102
|
+
filePath: row.file_path,
|
|
103
|
+
lineStart: row.line_start,
|
|
104
|
+
lineEnd: row.line_end ?? undefined,
|
|
105
|
+
signature: row.signature ?? undefined,
|
|
106
|
+
confidence: 0.5,
|
|
107
|
+
}))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
findFiles(query: string, project: string): FileResult[] {
|
|
112
|
+
if (!query.trim()) return []
|
|
113
|
+
|
|
114
|
+
const rows = this.db.query(
|
|
115
|
+
`SELECT file_path, language, summary, symbol_count, line_count
|
|
116
|
+
FROM code_files
|
|
117
|
+
WHERE project = ? AND (file_path LIKE '%' || ? || '%' OR summary LIKE '%' || ? || '%')
|
|
118
|
+
ORDER BY file_path
|
|
119
|
+
LIMIT 50`
|
|
120
|
+
).all(project, query, query) as RawFileRow[]
|
|
121
|
+
|
|
122
|
+
return rows.map(row => ({
|
|
123
|
+
filePath: row.file_path,
|
|
124
|
+
language: row.language ?? undefined,
|
|
125
|
+
summary: row.summary ?? undefined,
|
|
126
|
+
symbolCount: row.symbol_count,
|
|
127
|
+
lineCount: row.line_count,
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getDependencies(filePath: string, project: string): DependencyResult {
|
|
132
|
+
const imports = this.db.query(
|
|
133
|
+
`SELECT target_file, import_names
|
|
134
|
+
FROM code_dependencies
|
|
135
|
+
WHERE project = ? AND source_file = ?`
|
|
136
|
+
).all(project, filePath) as { target_file: string; import_names: string | null }[]
|
|
137
|
+
|
|
138
|
+
const importedBy = this.db.query(
|
|
139
|
+
`SELECT source_file, import_names
|
|
140
|
+
FROM code_dependencies
|
|
141
|
+
WHERE project = ? AND target_file = ?`
|
|
142
|
+
).all(project, filePath) as { source_file: string; import_names: string | null }[]
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
file: filePath,
|
|
146
|
+
imports: imports.map(row => ({
|
|
147
|
+
file: row.target_file,
|
|
148
|
+
names: row.import_names ? row.import_names.split(', ').filter(Boolean) : [],
|
|
149
|
+
})),
|
|
150
|
+
importedBy: importedBy.map(row => ({
|
|
151
|
+
file: row.source_file,
|
|
152
|
+
names: row.import_names ? row.import_names.split(', ').filter(Boolean) : [],
|
|
153
|
+
})),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getDependents(filePath: string, project: string): DependencyResult {
|
|
158
|
+
// getDependents is the inverse view — same data, different perspective
|
|
159
|
+
return this.getDependencies(filePath, project)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getFileMap(project: string, maxEntries: number = 100): FileMapEntry[] {
|
|
163
|
+
const rows = this.db.query(
|
|
164
|
+
`SELECT f.file_path, f.language, f.summary,
|
|
165
|
+
GROUP_CONCAT(CASE WHEN s.exported = 1 THEN s.symbol_name END) as symbols
|
|
166
|
+
FROM code_files f
|
|
167
|
+
LEFT JOIN code_symbols s ON s.project = f.project AND s.file_path = f.file_path
|
|
168
|
+
WHERE f.project = ?
|
|
169
|
+
GROUP BY f.file_path
|
|
170
|
+
ORDER BY f.file_path
|
|
171
|
+
LIMIT ?`
|
|
172
|
+
).all(project, maxEntries) as {
|
|
173
|
+
file_path: string
|
|
174
|
+
language: string | null
|
|
175
|
+
summary: string | null
|
|
176
|
+
symbols: string | null
|
|
177
|
+
}[]
|
|
178
|
+
|
|
179
|
+
// Phase 29: Count linked memories per file from observations table
|
|
180
|
+
const memoryCounts = this.getMemoryCounts(project)
|
|
181
|
+
|
|
182
|
+
return rows.map(row => ({
|
|
183
|
+
path: row.file_path,
|
|
184
|
+
language: row.language ?? undefined,
|
|
185
|
+
summary: row.summary ?? undefined,
|
|
186
|
+
symbols: row.symbols ? [...new Set(row.symbols.split(',').filter(Boolean))] : [],
|
|
187
|
+
linkedMemories: memoryCounts.get(row.file_path) ?? undefined,
|
|
188
|
+
}))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Phase 29: Query observations table for file path reference counts */
|
|
192
|
+
private getMemoryCounts(project: string): Map<string, number> {
|
|
193
|
+
const counts = new Map<string, number>()
|
|
194
|
+
if (!this.memoryDb) return counts
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const rows = this.memoryDb.prepare(
|
|
198
|
+
`SELECT file_paths FROM observations
|
|
199
|
+
WHERE project = ? AND file_paths IS NOT NULL AND archived = 0`
|
|
200
|
+
).all(project) as { file_paths: string }[]
|
|
201
|
+
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
try {
|
|
204
|
+
const paths: string[] = JSON.parse(row.file_paths)
|
|
205
|
+
if (Array.isArray(paths)) {
|
|
206
|
+
for (const fp of paths) {
|
|
207
|
+
counts.set(fp, (counts.get(fp) || 0) + 1)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Skip malformed JSON
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
this.logger.debug({ error }, 'Failed to query memory counts from observations')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return counts
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
formatFileMap(entries: FileMapEntry[]): string {
|
|
222
|
+
const lines: string[] = []
|
|
223
|
+
let totalSize = 0
|
|
224
|
+
const MAX_SIZE = 4096 // 4KB budget
|
|
225
|
+
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
let line = entry.path
|
|
228
|
+
if (entry.symbols.length > 0) {
|
|
229
|
+
const symbolStr = entry.symbols.length <= 5
|
|
230
|
+
? entry.symbols.join(', ')
|
|
231
|
+
: `${entry.symbols.slice(0, 5).join(', ')} +${entry.symbols.length - 5}`
|
|
232
|
+
line += ` — ${symbolStr}`
|
|
233
|
+
}
|
|
234
|
+
if (entry.summary) {
|
|
235
|
+
line += ` — ${entry.summary}`
|
|
236
|
+
}
|
|
237
|
+
// Phase 29: Append linked memory count annotation
|
|
238
|
+
if (entry.linkedMemories && entry.linkedMemories > 0) {
|
|
239
|
+
line += ` [${entry.linkedMemories} ${entry.linkedMemories === 1 ? 'memory' : 'memories'}]`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if adding this line would exceed budget
|
|
243
|
+
if (totalSize + line.length + 1 > MAX_SIZE) break
|
|
244
|
+
totalSize += line.length + 1
|
|
245
|
+
lines.push(line)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return lines.join('\n')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getStats(project: string): IndexStats {
|
|
252
|
+
const fileRow = this.db.query(
|
|
253
|
+
'SELECT COUNT(*) as cnt FROM code_files WHERE project = ?'
|
|
254
|
+
).get(project) as { cnt: number } | null
|
|
255
|
+
|
|
256
|
+
const symbolRow = this.db.query(
|
|
257
|
+
'SELECT COUNT(*) as cnt FROM code_symbols WHERE project = ?'
|
|
258
|
+
).get(project) as { cnt: number } | null
|
|
259
|
+
|
|
260
|
+
const lastRow = this.db.query(
|
|
261
|
+
'SELECT MAX(last_indexed) as last_indexed FROM code_files WHERE project = ?'
|
|
262
|
+
).get(project) as { last_indexed: string | null } | null
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
project,
|
|
266
|
+
filesIndexed: fileRow?.cnt ?? 0,
|
|
267
|
+
totalSymbols: symbolRow?.cnt ?? 0,
|
|
268
|
+
lastIndexed: lastRow?.last_indexed ?? null,
|
|
269
|
+
staleFiles: 0,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Internal Row Types ─────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
interface RawSymbolRow {
|
|
277
|
+
symbol_name: string
|
|
278
|
+
symbol_type: string
|
|
279
|
+
file_path: string
|
|
280
|
+
line_start: number
|
|
281
|
+
line_end: number | null
|
|
282
|
+
signature: string | null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface RawFileRow {
|
|
286
|
+
file_path: string
|
|
287
|
+
language: string | null
|
|
288
|
+
summary: string | null
|
|
289
|
+
symbol_count: number
|
|
290
|
+
line_count: number
|
|
291
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite'
|
|
2
|
+
|
|
3
|
+
const CODE_INTELLIGENCE_SCHEMA = `
|
|
4
|
+
-- File summaries
|
|
5
|
+
CREATE TABLE IF NOT EXISTS code_files (
|
|
6
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
project TEXT NOT NULL,
|
|
8
|
+
file_path TEXT NOT NULL,
|
|
9
|
+
language TEXT,
|
|
10
|
+
summary TEXT,
|
|
11
|
+
symbol_count INTEGER DEFAULT 0,
|
|
12
|
+
line_count INTEGER DEFAULT 0,
|
|
13
|
+
last_indexed TEXT NOT NULL,
|
|
14
|
+
file_hash TEXT,
|
|
15
|
+
UNIQUE(project, file_path)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- Symbols: functions, classes, variables, exports
|
|
19
|
+
CREATE TABLE IF NOT EXISTS code_symbols (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
project TEXT NOT NULL,
|
|
22
|
+
file_path TEXT NOT NULL,
|
|
23
|
+
symbol_name TEXT NOT NULL,
|
|
24
|
+
symbol_type TEXT NOT NULL,
|
|
25
|
+
line_start INTEGER NOT NULL,
|
|
26
|
+
line_end INTEGER,
|
|
27
|
+
parent_symbol TEXT,
|
|
28
|
+
signature TEXT,
|
|
29
|
+
exported BOOLEAN DEFAULT 0,
|
|
30
|
+
updated_at TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
-- Full-text search index on symbol names
|
|
34
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS code_symbols_fts USING fts5(
|
|
35
|
+
symbol_name, file_path, symbol_type, signature,
|
|
36
|
+
content='code_symbols', content_rowid='id'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
-- FTS5 triggers to keep FTS in sync with code_symbols
|
|
40
|
+
CREATE TRIGGER IF NOT EXISTS code_symbols_ai AFTER INSERT ON code_symbols BEGIN
|
|
41
|
+
INSERT INTO code_symbols_fts(rowid, symbol_name, file_path, symbol_type, signature)
|
|
42
|
+
VALUES (new.id, new.symbol_name, new.file_path, new.symbol_type, new.signature);
|
|
43
|
+
END;
|
|
44
|
+
|
|
45
|
+
CREATE TRIGGER IF NOT EXISTS code_symbols_ad AFTER DELETE ON code_symbols BEGIN
|
|
46
|
+
INSERT INTO code_symbols_fts(code_symbols_fts, rowid, symbol_name, file_path, symbol_type, signature)
|
|
47
|
+
VALUES ('delete', old.id, old.symbol_name, old.file_path, old.symbol_type, old.signature);
|
|
48
|
+
END;
|
|
49
|
+
|
|
50
|
+
CREATE TRIGGER IF NOT EXISTS code_symbols_au AFTER UPDATE ON code_symbols BEGIN
|
|
51
|
+
INSERT INTO code_symbols_fts(code_symbols_fts, rowid, symbol_name, file_path, symbol_type, signature)
|
|
52
|
+
VALUES ('delete', old.id, old.symbol_name, old.file_path, old.symbol_type, old.signature);
|
|
53
|
+
INSERT INTO code_symbols_fts(rowid, symbol_name, file_path, symbol_type, signature)
|
|
54
|
+
VALUES (new.id, new.symbol_name, new.file_path, new.symbol_type, new.signature);
|
|
55
|
+
END;
|
|
56
|
+
|
|
57
|
+
-- Import/export relationships
|
|
58
|
+
CREATE TABLE IF NOT EXISTS code_dependencies (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
project TEXT NOT NULL,
|
|
61
|
+
source_file TEXT NOT NULL,
|
|
62
|
+
target_file TEXT NOT NULL,
|
|
63
|
+
import_names TEXT,
|
|
64
|
+
import_type TEXT
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
-- Indexes
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_project ON code_symbols(project);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name ON code_symbols(symbol_name);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_file ON code_symbols(file_path);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_files_project ON code_files(project);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_files_path ON code_files(project, file_path);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_deps_source ON code_dependencies(source_file);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_deps_target ON code_dependencies(target_file);
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create the code intelligence schema tables in the given SQLite database.
|
|
79
|
+
* Safe to call multiple times — all statements use IF NOT EXISTS.
|
|
80
|
+
*/
|
|
81
|
+
export function createCodeSchema(db: Database): void {
|
|
82
|
+
db.exec(CODE_INTELLIGENCE_SCHEMA)
|
|
83
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Supported languages for code intelligence */
|
|
2
|
+
export type Language =
|
|
3
|
+
| 'typescript' | 'javascript' | 'tsx' | 'jsx'
|
|
4
|
+
| 'python' | 'go' | 'rust' | 'vue'
|
|
5
|
+
| 'html' | 'css' | 'json' | 'yaml'
|
|
6
|
+
|
|
7
|
+
/** Symbol types extracted from source files */
|
|
8
|
+
export type SymbolType =
|
|
9
|
+
| 'function' | 'class' | 'variable' | 'export'
|
|
10
|
+
| 'import' | 'interface' | 'type' | 'method'
|
|
11
|
+
| 'enum' | 'constant'
|
|
12
|
+
|
|
13
|
+
/** A symbol extracted from a source file */
|
|
14
|
+
export interface CodeSymbol {
|
|
15
|
+
name: string
|
|
16
|
+
type: SymbolType
|
|
17
|
+
filePath: string
|
|
18
|
+
lineStart: number
|
|
19
|
+
lineEnd?: number
|
|
20
|
+
parentSymbol?: string // for methods inside classes
|
|
21
|
+
signature?: string // function signature
|
|
22
|
+
exported: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Parsed file result */
|
|
26
|
+
export interface ParsedFile {
|
|
27
|
+
filePath: string
|
|
28
|
+
language: Language
|
|
29
|
+
symbols: CodeSymbol[]
|
|
30
|
+
imports: ImportInfo[]
|
|
31
|
+
lineCount: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Import information */
|
|
35
|
+
export interface ImportInfo {
|
|
36
|
+
source: string // the module path (e.g., './auth', 'express')
|
|
37
|
+
names: string[] // imported names (e.g., ['Router', 'Request'])
|
|
38
|
+
importType: 'import' | 'require' | 'dynamic'
|
|
39
|
+
line: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Result of a symbol search */
|
|
43
|
+
export interface SymbolResult {
|
|
44
|
+
symbol: string
|
|
45
|
+
type: SymbolType
|
|
46
|
+
filePath: string
|
|
47
|
+
lineStart: number
|
|
48
|
+
lineEnd?: number
|
|
49
|
+
signature?: string
|
|
50
|
+
confidence: number // 0-1, how well this matches the query
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Result of a file search */
|
|
54
|
+
export interface FileResult {
|
|
55
|
+
filePath: string
|
|
56
|
+
language?: string
|
|
57
|
+
summary?: string
|
|
58
|
+
symbolCount: number
|
|
59
|
+
lineCount: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Result of a dependency query */
|
|
63
|
+
export interface DependencyResult {
|
|
64
|
+
file: string
|
|
65
|
+
imports: Array<{ file: string; names: string[] }>
|
|
66
|
+
importedBy: Array<{ file: string; names: string[] }>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Indexing result */
|
|
70
|
+
export interface IndexResult {
|
|
71
|
+
project: string
|
|
72
|
+
filesIndexed: number
|
|
73
|
+
filesSkipped: number
|
|
74
|
+
symbolsFound: number
|
|
75
|
+
durationMs: number
|
|
76
|
+
errors: string[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** File map entry for session injection */
|
|
80
|
+
export interface FileMapEntry {
|
|
81
|
+
path: string
|
|
82
|
+
language?: string
|
|
83
|
+
summary?: string
|
|
84
|
+
symbols: string[] // top-level exported symbol names
|
|
85
|
+
linkedMemories?: number // Phase 29: count of observations referencing this file
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Index statistics for the status endpoint */
|
|
89
|
+
export interface IndexStats {
|
|
90
|
+
project: string
|
|
91
|
+
filesIndexed: number
|
|
92
|
+
totalSymbols: number
|
|
93
|
+
lastIndexed: string | null
|
|
94
|
+
staleFiles: number
|
|
95
|
+
}
|
package/src/config/defaults.ts
CHANGED
|
@@ -3,16 +3,16 @@ import type { PartialConfig } from './schema'
|
|
|
3
3
|
|
|
4
4
|
/** Default configuration values for Claude Brain */
|
|
5
5
|
export const defaultConfig: PartialConfig = {
|
|
6
|
-
vaultPath: '',
|
|
6
|
+
vaultPath: '', // Auto-detected from ~/.claude-brain/vault if empty
|
|
7
7
|
serverName: 'claude-brain',
|
|
8
8
|
serverVersion: pkg.version,
|
|
9
|
-
logLevel: '
|
|
9
|
+
logLevel: 'warn',
|
|
10
10
|
logFilePath: './logs/claude-brain.log',
|
|
11
11
|
dbPath: './data/memory.db',
|
|
12
12
|
port: 3000,
|
|
13
13
|
enableFileWatch: true,
|
|
14
14
|
cacheSize: 100,
|
|
15
|
-
nodeEnv: '
|
|
15
|
+
nodeEnv: 'production',
|
|
16
16
|
retrieval: {
|
|
17
17
|
enabled: false,
|
|
18
18
|
dense: {
|
package/src/config/loader.ts
CHANGED
|
@@ -167,6 +167,12 @@ export async function loadConfig(basePath: string = process.cwd()): Promise<Conf
|
|
|
167
167
|
data.knowledge.graph.persistPath = resolveHomePath(data.knowledge.graph.persistPath)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// Phase 30: Auto-detect vaultPath if not configured
|
|
171
|
+
if (!data.vaultPath) {
|
|
172
|
+
const { getHomePaths } = await import('./home')
|
|
173
|
+
data.vaultPath = getHomePaths().vault
|
|
174
|
+
}
|
|
175
|
+
|
|
170
176
|
return data
|
|
171
177
|
}
|
|
172
178
|
|
package/src/config/schema.ts
CHANGED
|
@@ -280,8 +280,8 @@ export type AdvancedIntelligenceConfig = z.infer<typeof AdvancedIntelligenceConf
|
|
|
280
280
|
|
|
281
281
|
/** Main configuration schema for Claude Brain */
|
|
282
282
|
export const ConfigSchema = z.object({
|
|
283
|
-
/** Absolute path to the Obsidian vault */
|
|
284
|
-
vaultPath: z.string().
|
|
283
|
+
/** Absolute path to the Obsidian vault (auto-detected if empty) */
|
|
284
|
+
vaultPath: z.string().default(''),
|
|
285
285
|
|
|
286
286
|
/** MCP server identifier */
|
|
287
287
|
serverName: z.string().default('claude-brain'),
|
|
@@ -332,6 +332,32 @@ export const ConfigSchema = z.object({
|
|
|
332
332
|
intelligence: z.object({
|
|
333
333
|
/** Enable consolidated intelligence features */
|
|
334
334
|
enabled: z.boolean().default(false)
|
|
335
|
+
}).default({} as any),
|
|
336
|
+
|
|
337
|
+
/** Phase 26: ChromaDB configuration (now optional, SQLite FTS5 is primary) */
|
|
338
|
+
chromadb: z.object({
|
|
339
|
+
/** Enable ChromaDB vector storage backend */
|
|
340
|
+
enabled: z.boolean().default(false)
|
|
341
|
+
}).default({} as any),
|
|
342
|
+
|
|
343
|
+
/** Phase 28: Code intelligence configuration */
|
|
344
|
+
codeIntelligence: z.object({
|
|
345
|
+
/** Enable code intelligence (project indexing and file maps) */
|
|
346
|
+
enabled: z.boolean().default(true),
|
|
347
|
+
}).default({} as any),
|
|
348
|
+
|
|
349
|
+
/** Phase 30: Optional LLM compression for observations */
|
|
350
|
+
compression: z.object({
|
|
351
|
+
/** Enable LLM-based compression of long observations */
|
|
352
|
+
enabled: z.boolean().default(false),
|
|
353
|
+
/** LLM provider for compression */
|
|
354
|
+
provider: z.enum(['claude', 'ollama']).default('claude'),
|
|
355
|
+
/** Model to use for compression */
|
|
356
|
+
model: z.string().default('claude-haiku-4-5-20251001'),
|
|
357
|
+
/** API key (falls back to ANTHROPIC_API_KEY env var) */
|
|
358
|
+
apiKey: z.string().optional(),
|
|
359
|
+
/** Minimum content length to trigger compression */
|
|
360
|
+
minContentLength: z.number().default(500),
|
|
335
361
|
}).default({} as any)
|
|
336
362
|
})
|
|
337
363
|
|
package/src/health/index.ts
CHANGED
|
@@ -115,7 +115,9 @@ export class HealthChecker {
|
|
|
115
115
|
private async checkMemory(): Promise<ComponentHealth> {
|
|
116
116
|
try {
|
|
117
117
|
const config = await loadConfig()
|
|
118
|
-
|
|
118
|
+
// Phase 26: Respect chromadb.enabled config
|
|
119
|
+
const useChromaDB = config.chromadb?.enabled ?? false
|
|
120
|
+
const memory = new MemoryManager(config.dbPath, this.logger, useChromaDB)
|
|
119
121
|
|
|
120
122
|
await memory.initialize()
|
|
121
123
|
|
|
@@ -156,7 +158,8 @@ export class HealthChecker {
|
|
|
156
158
|
const vault = new VaultManager(config.vaultPath, this.logger, { backupDir: resolveHomePath('./data/backups') })
|
|
157
159
|
await vault.initialize()
|
|
158
160
|
|
|
159
|
-
const
|
|
161
|
+
const useChromaDB2 = config.chromadb?.enabled ?? false
|
|
162
|
+
const memory = new MemoryManager(config.dbPath, this.logger, useChromaDB2)
|
|
160
163
|
await memory.initialize()
|
|
161
164
|
|
|
162
165
|
const context = new ContextManager(this.logger, vault, memory)
|
package/src/hooks/brain-hook.ts
CHANGED
|
@@ -52,7 +52,10 @@ export async function main(): Promise<void> {
|
|
|
52
52
|
return
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// BUG-006: Read port from --port arg (cross-platform) or env var fallback
|
|
56
|
+
const portIdx = process.argv.indexOf('--port')
|
|
57
|
+
const portArg = portIdx >= 0 ? process.argv[portIdx + 1] : undefined
|
|
58
|
+
const port = parseInt(portArg || process.env.CLAUDE_BRAIN_PORT || '3000', 10)
|
|
56
59
|
|
|
57
60
|
// For Stop events: trigger session-end summarization regardless of classification
|
|
58
61
|
if (input.hook_event_name === 'Stop' && input.session_id) {
|