claude-brain 0.22.1 → 0.22.3
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 +1 -1
- package/src/cli/commands/status.ts +7 -6
- package/src/memory/chroma/store.ts +6 -3
- package/src/memory/fts5-search.ts +41 -13
- package/src/memory/index.ts +61 -17
- package/src/retrieval/query/expander.ts +5 -0
- package/src/routing/response-filter.ts +1 -1
- package/src/routing/router.ts +121 -3
- package/src/server/http-api.ts +35 -11
- package/src/server/web-viewer.ts +1 -1
- package/src/tools/schemas.ts +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.22.
|
|
1
|
+
0.22.3
|
package/package.json
CHANGED
|
@@ -39,16 +39,17 @@ export async function runStatus() {
|
|
|
39
39
|
const { Database } = await import('bun:sqlite')
|
|
40
40
|
const db = new Database(dbPath, { readonly: true })
|
|
41
41
|
|
|
42
|
-
// Count observations by category
|
|
42
|
+
// Count observations by category (dynamic — includes all categories)
|
|
43
43
|
const total = (db.prepare('SELECT COUNT(*) as cnt FROM observations WHERE archived = 0').get() as any)?.cnt ?? 0
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const catRows = db.prepare(
|
|
45
|
+
'SELECT category, COUNT(*) as cnt FROM observations WHERE archived = 0 GROUP BY category'
|
|
46
|
+
).all() as any[]
|
|
47
|
+
const breakdown = catRows.map((r: any) => `${r.cnt} ${r.category}s`).join(', ')
|
|
47
48
|
|
|
48
49
|
items.push({ label: 'Storage', value: 'SQLite FTS5', status: 'success' })
|
|
49
50
|
items.push({
|
|
50
51
|
label: 'Observations',
|
|
51
|
-
value: `${total}
|
|
52
|
+
value: `${total}${breakdown ? ` (${breakdown})` : ''}`,
|
|
52
53
|
status: total > 0 ? 'success' : 'warning'
|
|
53
54
|
})
|
|
54
55
|
|
|
@@ -79,7 +80,7 @@ export async function runStatus() {
|
|
|
79
80
|
const symbols = (codeDb.prepare('SELECT COUNT(*) as cnt FROM code_symbols').get() as any)?.cnt ?? 0
|
|
80
81
|
|
|
81
82
|
// Last indexed time
|
|
82
|
-
const latest = (codeDb.prepare('SELECT MAX(
|
|
83
|
+
const latest = (codeDb.prepare('SELECT MAX(last_indexed) as latest FROM code_files').get() as any)?.latest
|
|
83
84
|
const lastIndexed = latest ? formatAge(latest) : 'never'
|
|
84
85
|
|
|
85
86
|
items.push({
|
|
@@ -25,6 +25,7 @@ function sanitizeMetadata(metadata: Record<string, any>): Record<string, string
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface StoreDecisionInput {
|
|
28
|
+
id?: string
|
|
28
29
|
project: string
|
|
29
30
|
context: string
|
|
30
31
|
decision: string
|
|
@@ -141,7 +142,7 @@ export class ChromaMemoryStore {
|
|
|
141
142
|
return firstDuplicate.id
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
const id = randomUUID()
|
|
145
|
+
const id = input.id || randomUUID()
|
|
145
146
|
const now = new Date().toISOString()
|
|
146
147
|
|
|
147
148
|
const metadata: Record<string, any> = {
|
|
@@ -225,6 +226,7 @@ export class ChromaMemoryStore {
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
async storePattern(input: {
|
|
229
|
+
id?: string
|
|
228
230
|
project: string
|
|
229
231
|
pattern_type: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
|
|
230
232
|
description: string
|
|
@@ -233,7 +235,7 @@ export class ChromaMemoryStore {
|
|
|
233
235
|
context?: string
|
|
234
236
|
source?: string
|
|
235
237
|
}): Promise<string> {
|
|
236
|
-
const id = randomUUID()
|
|
238
|
+
const id = input.id || randomUUID()
|
|
237
239
|
const now = new Date().toISOString()
|
|
238
240
|
|
|
239
241
|
const metadata: Record<string, any> = {
|
|
@@ -300,6 +302,7 @@ export class ChromaMemoryStore {
|
|
|
300
302
|
}
|
|
301
303
|
|
|
302
304
|
async storeCorrection(input: {
|
|
305
|
+
id?: string
|
|
303
306
|
project: string
|
|
304
307
|
original: string
|
|
305
308
|
correction: string
|
|
@@ -307,7 +310,7 @@ export class ChromaMemoryStore {
|
|
|
307
310
|
context?: string
|
|
308
311
|
confidence: number
|
|
309
312
|
}): Promise<string> {
|
|
310
|
-
const id = randomUUID()
|
|
313
|
+
const id = input.id || randomUUID()
|
|
311
314
|
const now = new Date().toISOString()
|
|
312
315
|
|
|
313
316
|
const metadata: Record<string, any> = {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { randomUUID } from 'crypto'
|
|
8
8
|
import type { Database } from 'bun:sqlite'
|
|
9
9
|
import type { Logger } from 'pino'
|
|
10
|
+
import { expandQuery } from '@/retrieval/query/expander'
|
|
10
11
|
|
|
11
12
|
export type ObservationCategory = 'decision' | 'pattern' | 'correction' | 'insight' | 'preference'
|
|
12
13
|
|
|
@@ -174,8 +175,8 @@ export class FTS5Search {
|
|
|
174
175
|
/**
|
|
175
176
|
* Store a new observation. Returns the generated ID.
|
|
176
177
|
*/
|
|
177
|
-
store(observation: NewObservation): string {
|
|
178
|
-
const id = randomUUID()
|
|
178
|
+
store(observation: NewObservation, providedId?: string): string {
|
|
179
|
+
const id = providedId || randomUUID()
|
|
179
180
|
const now = new Date().toISOString()
|
|
180
181
|
|
|
181
182
|
const stmt = this.db.prepare(`
|
|
@@ -245,15 +246,21 @@ export class FTS5Search {
|
|
|
245
246
|
/**
|
|
246
247
|
* Fetch all observations for a project, optionally filtered by category.
|
|
247
248
|
*/
|
|
248
|
-
fetchAll(project
|
|
249
|
+
fetchAll(project?: string, category?: ObservationCategory): ObservationResult[] {
|
|
249
250
|
let sql: string
|
|
250
|
-
const params: any[] = [
|
|
251
|
+
const params: any[] = []
|
|
251
252
|
|
|
252
|
-
if (category) {
|
|
253
|
+
if (project && category) {
|
|
253
254
|
sql = `SELECT * FROM observations WHERE project = ? AND category = ? AND archived = 0 ORDER BY created_at DESC`
|
|
255
|
+
params.push(project, category)
|
|
256
|
+
} else if (project) {
|
|
257
|
+
sql = `SELECT * FROM observations WHERE project = ? AND archived = 0 ORDER BY created_at DESC`
|
|
258
|
+
params.push(project)
|
|
259
|
+
} else if (category) {
|
|
260
|
+
sql = `SELECT * FROM observations WHERE category = ? AND archived = 0 ORDER BY created_at DESC`
|
|
254
261
|
params.push(category)
|
|
255
262
|
} else {
|
|
256
|
-
sql = `SELECT * FROM observations WHERE
|
|
263
|
+
sql = `SELECT * FROM observations WHERE archived = 0 ORDER BY created_at DESC`
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
const rows = this.db.prepare(sql).all(...params) as any[]
|
|
@@ -337,6 +344,26 @@ export class FTS5Search {
|
|
|
337
344
|
this.recordAccess(id)
|
|
338
345
|
}
|
|
339
346
|
|
|
347
|
+
/**
|
|
348
|
+
* BUG-002: Search within a specific category, optionally scoped to a project.
|
|
349
|
+
* Returns results ordered by creation date (most recent first).
|
|
350
|
+
*/
|
|
351
|
+
searchByCategory(category: ObservationCategory, project?: string, limit: number = 10): ObservationResult[] {
|
|
352
|
+
let sql: string
|
|
353
|
+
const params: any[] = []
|
|
354
|
+
|
|
355
|
+
if (project) {
|
|
356
|
+
sql = `SELECT * FROM observations WHERE category = ? AND project = ? AND archived = 0 ORDER BY created_at DESC LIMIT ?`
|
|
357
|
+
params.push(category, project, limit)
|
|
358
|
+
} else {
|
|
359
|
+
sql = `SELECT * FROM observations WHERE category = ? AND archived = 0 ORDER BY created_at DESC LIMIT ?`
|
|
360
|
+
params.push(category, limit)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const rows = this.db.prepare(sql).all(...params) as any[]
|
|
364
|
+
return rows.map(row => this.rowToResult(row))
|
|
365
|
+
}
|
|
366
|
+
|
|
340
367
|
// --- Private helpers ---
|
|
341
368
|
|
|
342
369
|
/**
|
|
@@ -348,12 +375,13 @@ export class FTS5Search {
|
|
|
348
375
|
const cleaned = query.replace(/[":*^~(){}[\]]/g, ' ').trim()
|
|
349
376
|
if (!cleaned) return ''
|
|
350
377
|
|
|
351
|
-
//
|
|
352
|
-
const
|
|
353
|
-
|
|
378
|
+
// Expand query with synonyms (e.g., "database" → also search "storage", "persistence")
|
|
379
|
+
const expanded = expandQuery(cleaned, { useSynonyms: true, maxExpansions: 8 })
|
|
380
|
+
const allWords = expanded.combined.split(/\s+/).filter(w => w.length >= 2)
|
|
381
|
+
const unique = [...new Set(allWords)]
|
|
354
382
|
|
|
355
|
-
|
|
356
|
-
return
|
|
383
|
+
if (unique.length === 0) return ''
|
|
384
|
+
return unique.map(w => `"${w}"`).join(' OR ')
|
|
357
385
|
}
|
|
358
386
|
|
|
359
387
|
/**
|
|
@@ -362,9 +390,9 @@ export class FTS5Search {
|
|
|
362
390
|
* Typical range: -20 (excellent) to 0 (poor).
|
|
363
391
|
*/
|
|
364
392
|
private normalizeBM25(score: number): number {
|
|
365
|
-
// Map
|
|
393
|
+
// Map BM25 range to 0.2-0.9 (wider range, allows low scores to be filtered)
|
|
366
394
|
const normalized = Math.min(1, Math.max(0, score / 20))
|
|
367
|
-
return 0.
|
|
395
|
+
return 0.2 + normalized * 0.7
|
|
368
396
|
}
|
|
369
397
|
|
|
370
398
|
/**
|
package/src/memory/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Unified memory system manager that combines all components
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { randomUUID } from 'crypto'
|
|
8
9
|
import type { Logger } from 'pino'
|
|
9
10
|
import { MemoryDatabase } from './database'
|
|
10
11
|
import { EmbeddingService } from './embeddings'
|
|
@@ -244,6 +245,9 @@ export class MemoryManager {
|
|
|
244
245
|
reasoning: string,
|
|
245
246
|
options?: { alternatives?: string; tags?: string[] }
|
|
246
247
|
): Promise<string> {
|
|
248
|
+
// BUG-001 fix: Generate a single shared ID for all backends
|
|
249
|
+
const sharedId = randomUUID()
|
|
250
|
+
|
|
247
251
|
// Phase 26: Always store in FTS5 if available
|
|
248
252
|
let fts5Id: string | undefined
|
|
249
253
|
if (this._fts5) {
|
|
@@ -262,7 +266,7 @@ export class MemoryManager {
|
|
|
262
266
|
reasoning,
|
|
263
267
|
context,
|
|
264
268
|
tags: options?.tags
|
|
265
|
-
})
|
|
269
|
+
}, sharedId)
|
|
266
270
|
} catch (error) {
|
|
267
271
|
this.logger.warn({ error }, 'FTS5 store failed, continuing with other backends')
|
|
268
272
|
}
|
|
@@ -272,6 +276,7 @@ export class MemoryManager {
|
|
|
272
276
|
if (this.useChromaDB) {
|
|
273
277
|
try {
|
|
274
278
|
const chromaId = await this.chroma.store.storeDecision({
|
|
279
|
+
id: sharedId,
|
|
275
280
|
project,
|
|
276
281
|
context,
|
|
277
282
|
decision,
|
|
@@ -456,6 +461,9 @@ export class MemoryManager {
|
|
|
456
461
|
context?: string
|
|
457
462
|
source?: string
|
|
458
463
|
}): Promise<string> {
|
|
464
|
+
// BUG-001 fix: Generate a single shared ID for all backends
|
|
465
|
+
const sharedId = randomUUID()
|
|
466
|
+
|
|
459
467
|
// Phase 26: Dual-write to FTS5
|
|
460
468
|
let fts5Id: string | undefined
|
|
461
469
|
if (this._fts5) {
|
|
@@ -467,7 +475,7 @@ export class MemoryManager {
|
|
|
467
475
|
context: input.context,
|
|
468
476
|
confidence: input.confidence,
|
|
469
477
|
source: input.source
|
|
470
|
-
})
|
|
478
|
+
}, sharedId)
|
|
471
479
|
} catch (error) {
|
|
472
480
|
this.logger.warn({ error }, 'FTS5 pattern store failed')
|
|
473
481
|
}
|
|
@@ -475,7 +483,7 @@ export class MemoryManager {
|
|
|
475
483
|
|
|
476
484
|
if (this.useChromaDB) {
|
|
477
485
|
try {
|
|
478
|
-
const chromaId = await this.chroma.store.storePattern(input)
|
|
486
|
+
const chromaId = await this.chroma.store.storePattern({ ...input, id: sharedId })
|
|
479
487
|
return fts5Id || chromaId
|
|
480
488
|
} catch (error) {
|
|
481
489
|
this.logger.warn({ error }, 'ChromaDB pattern store failed')
|
|
@@ -500,6 +508,9 @@ export class MemoryManager {
|
|
|
500
508
|
context?: string
|
|
501
509
|
confidence: number
|
|
502
510
|
}): Promise<string> {
|
|
511
|
+
// BUG-001 fix: Generate a single shared ID for all backends
|
|
512
|
+
const sharedId = randomUUID()
|
|
513
|
+
|
|
503
514
|
// Phase 26: Dual-write to FTS5
|
|
504
515
|
let fts5Id: string | undefined
|
|
505
516
|
if (this._fts5) {
|
|
@@ -511,7 +522,7 @@ export class MemoryManager {
|
|
|
511
522
|
reasoning: input.reasoning,
|
|
512
523
|
context: input.context,
|
|
513
524
|
confidence: input.confidence
|
|
514
|
-
})
|
|
525
|
+
}, sharedId)
|
|
515
526
|
} catch (error) {
|
|
516
527
|
this.logger.warn({ error }, 'FTS5 correction store failed')
|
|
517
528
|
}
|
|
@@ -519,7 +530,7 @@ export class MemoryManager {
|
|
|
519
530
|
|
|
520
531
|
if (this.useChromaDB) {
|
|
521
532
|
try {
|
|
522
|
-
const chromaId = await this.chroma.store.storeCorrection(input)
|
|
533
|
+
const chromaId = await this.chroma.store.storeCorrection({ ...input, id: sharedId })
|
|
523
534
|
return fts5Id || chromaId
|
|
524
535
|
} catch (error) {
|
|
525
536
|
this.logger.warn({ error }, 'ChromaDB correction store failed')
|
|
@@ -543,8 +554,8 @@ export class MemoryManager {
|
|
|
543
554
|
limit?: number
|
|
544
555
|
}
|
|
545
556
|
): Promise<any[]> {
|
|
546
|
-
// Phase 26: Try FTS5 first
|
|
547
|
-
if (this._fts5
|
|
557
|
+
// Phase 26: Try FTS5 first (BUG-002: works with or without project)
|
|
558
|
+
if (this._fts5) {
|
|
548
559
|
const results = this._fts5.fetchAll(project, 'pattern')
|
|
549
560
|
if (results.length > 0) {
|
|
550
561
|
return results.map(r => ({
|
|
@@ -580,8 +591,8 @@ export class MemoryManager {
|
|
|
580
591
|
project?: string,
|
|
581
592
|
options?: { limit?: number }
|
|
582
593
|
): Promise<any[]> {
|
|
583
|
-
// Phase 26: Try FTS5 first
|
|
584
|
-
if (this._fts5
|
|
594
|
+
// Phase 26: Try FTS5 first (BUG-002: works with or without project)
|
|
595
|
+
if (this._fts5) {
|
|
585
596
|
const results = this._fts5.fetchAll(project, 'correction')
|
|
586
597
|
if (results.length > 0) {
|
|
587
598
|
return results.map(r => ({
|
|
@@ -615,8 +626,8 @@ export class MemoryManager {
|
|
|
615
626
|
* Used by analytical tools that need bulk access to decision data
|
|
616
627
|
*/
|
|
617
628
|
async fetchAllDecisions(project?: string): Promise<any[]> {
|
|
618
|
-
// Phase 26: Try FTS5 first
|
|
619
|
-
if (this._fts5
|
|
629
|
+
// Phase 26: Try FTS5 first (BUG-002: works with or without project)
|
|
630
|
+
if (this._fts5) {
|
|
620
631
|
const results = this._fts5.fetchAll(project, 'decision')
|
|
621
632
|
if (results.length > 0) {
|
|
622
633
|
return results.map(r => ({
|
|
@@ -664,8 +675,8 @@ export class MemoryManager {
|
|
|
664
675
|
* Fetch all patterns with content — routes to FTS5, ChromaDB, or legacy SQLite
|
|
665
676
|
*/
|
|
666
677
|
async fetchAllPatterns(project?: string): Promise<any[]> {
|
|
667
|
-
// Phase 26: Try FTS5 first
|
|
668
|
-
if (this._fts5
|
|
678
|
+
// Phase 26: Try FTS5 first (BUG-002: works with or without project)
|
|
679
|
+
if (this._fts5) {
|
|
669
680
|
const results = this._fts5.fetchAll(project, 'pattern')
|
|
670
681
|
if (results.length > 0) {
|
|
671
682
|
return results.map(r => ({
|
|
@@ -713,8 +724,8 @@ export class MemoryManager {
|
|
|
713
724
|
* Fetch all corrections with content — routes to FTS5, ChromaDB, or legacy SQLite
|
|
714
725
|
*/
|
|
715
726
|
async fetchAllCorrections(project?: string): Promise<any[]> {
|
|
716
|
-
// Phase 26: Try FTS5 first
|
|
717
|
-
if (this._fts5
|
|
727
|
+
// Phase 26: Try FTS5 first (BUG-002: works with or without project)
|
|
728
|
+
if (this._fts5) {
|
|
718
729
|
const results = this._fts5.fetchAll(project, 'correction')
|
|
719
730
|
if (results.length > 0) {
|
|
720
731
|
return results.map(r => ({
|
|
@@ -873,14 +884,47 @@ export class MemoryManager {
|
|
|
873
884
|
reasoning: string,
|
|
874
885
|
options?: { alternatives?: string; tags?: string[] }
|
|
875
886
|
): Promise<string> {
|
|
876
|
-
//
|
|
887
|
+
// BUG-001: True in-place update using FTS5 (preserves original ID)
|
|
888
|
+
if (this._fts5) {
|
|
889
|
+
try {
|
|
890
|
+
this._fts5.update(oldId, {
|
|
891
|
+
content: decision,
|
|
892
|
+
reasoning,
|
|
893
|
+
context,
|
|
894
|
+
tags: options?.tags
|
|
895
|
+
})
|
|
896
|
+
this.logger.debug({ oldId }, 'Decision updated in-place via FTS5')
|
|
897
|
+
|
|
898
|
+
// Also update ChromaDB if available (best-effort)
|
|
899
|
+
if (this.useChromaDB) {
|
|
900
|
+
try {
|
|
901
|
+
await this.chroma.store.deleteDecision(oldId)
|
|
902
|
+
await this.chroma.store.storeDecision({
|
|
903
|
+
project,
|
|
904
|
+
context,
|
|
905
|
+
decision,
|
|
906
|
+
reasoning,
|
|
907
|
+
alternatives: options?.alternatives,
|
|
908
|
+
tags: options?.tags
|
|
909
|
+
})
|
|
910
|
+
} catch {
|
|
911
|
+
// ChromaDB sync failed, FTS5 is source of truth
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return oldId // SAME ID preserved
|
|
916
|
+
} catch (error) {
|
|
917
|
+
this.logger.warn({ error, oldId }, 'FTS5 in-place update failed, falling back to delete+store')
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Fallback: delete + store (legacy behavior for non-FTS5 backends)
|
|
877
922
|
try {
|
|
878
923
|
await this.deleteDecision(oldId)
|
|
879
924
|
this.logger.debug({ oldId }, 'Old decision deleted for update')
|
|
880
925
|
} catch (error) {
|
|
881
926
|
this.logger.warn({ error, oldId }, 'Failed to delete old decision during update, storing new version anyway')
|
|
882
927
|
}
|
|
883
|
-
// Store new version — fires onDecisionStoredCallbacks (graph rebuild)
|
|
884
928
|
const newId = await this.rememberDecision(project, context, decision, reasoning, options)
|
|
885
929
|
this.logger.debug({ oldId, newId }, 'Decision updated: old deleted, new stored')
|
|
886
930
|
return newId
|
|
@@ -59,11 +59,16 @@ const SYNONYMS: Record<string, string[]> = {
|
|
|
59
59
|
deploy: ['deployment', 'release', 'ship', 'publish'],
|
|
60
60
|
build: ['compile', 'bundle', 'package'],
|
|
61
61
|
|
|
62
|
+
// Memory terms
|
|
63
|
+
memory: ['storage', 'store', 'cache', 'array'],
|
|
64
|
+
|
|
62
65
|
// Common verbs
|
|
63
66
|
create: ['add', 'new', 'generate', 'make'],
|
|
64
67
|
update: ['modify', 'change', 'edit', 'patch'],
|
|
65
68
|
delete: ['remove', 'destroy', 'drop'],
|
|
66
69
|
get: ['fetch', 'retrieve', 'read', 'query'],
|
|
70
|
+
choose: ['select', 'pick', 'decide', 'opt', 'chose'],
|
|
71
|
+
chose: ['choose', 'select', 'pick', 'decide', 'opted'],
|
|
67
72
|
|
|
68
73
|
// Framework terms
|
|
69
74
|
react: ['reactjs', 'react.js', 'component'],
|
package/src/routing/router.ts
CHANGED
|
@@ -33,8 +33,8 @@ import type { ObservationCompressor } from '@/memory/compression'
|
|
|
33
33
|
const DEFAULT_PROJECT = 'general'
|
|
34
34
|
|
|
35
35
|
/** Phase 19 D4: Minimum similarity for destructive operations */
|
|
36
|
-
const DELETE_MIN_SIMILARITY = 0.
|
|
37
|
-
const UPDATE_MIN_SIMILARITY = 0.
|
|
36
|
+
const DELETE_MIN_SIMILARITY = 0.3
|
|
37
|
+
const UPDATE_MIN_SIMILARITY = 0.3
|
|
38
38
|
|
|
39
39
|
export interface BrainInput {
|
|
40
40
|
message: string
|
|
@@ -879,7 +879,23 @@ export class BrainRouter {
|
|
|
879
879
|
relevantItems: 0
|
|
880
880
|
}
|
|
881
881
|
} else if (results.length > 0 && matchSimilarity < DELETE_MIN_SIMILARITY) {
|
|
882
|
-
// D4: No confident match
|
|
882
|
+
// D4: No confident match — try direct FTS5 content search as fallback
|
|
883
|
+
const memory = getMemoryService()
|
|
884
|
+
if (memory.fts5) {
|
|
885
|
+
const ftsResults = memory.fts5.search(topic, effectiveProject, 3)
|
|
886
|
+
if (ftsResults.length > 0) {
|
|
887
|
+
const ftsTarget = ftsResults[0]
|
|
888
|
+
await memory.deleteDecision(ftsTarget.id)
|
|
889
|
+
if (this.lastStoredId === ftsTarget.id) this.lastStoredId = null
|
|
890
|
+
this.searchEngine.invalidateCache(effectiveProject)
|
|
891
|
+
return {
|
|
892
|
+
action: 'stored',
|
|
893
|
+
summary: `Deleted memory`,
|
|
894
|
+
content: `Deleted: "${ftsTarget.content.slice(0, 100)}" (ID: ${ftsTarget.id})`,
|
|
895
|
+
relevantItems: 0
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
883
899
|
return {
|
|
884
900
|
action: 'none',
|
|
885
901
|
summary: 'No confident match to delete',
|
|
@@ -936,6 +952,12 @@ export class BrainRouter {
|
|
|
936
952
|
return this.servicesNotReady()
|
|
937
953
|
}
|
|
938
954
|
|
|
955
|
+
// BUG-002: Detect category-based queries and route to fetchAll
|
|
956
|
+
const categoryIntent = this.detectCategoryIntent(message)
|
|
957
|
+
if (categoryIntent) {
|
|
958
|
+
return this.handleCategoryQuery(message, project, categoryIntent)
|
|
959
|
+
}
|
|
960
|
+
|
|
939
961
|
// Phase 25: Use undefined for search (no project filter = search all) when no project detected
|
|
940
962
|
const searchProject = project || undefined
|
|
941
963
|
const displayProject = project || DEFAULT_PROJECT
|
|
@@ -1832,6 +1854,102 @@ export class BrainRouter {
|
|
|
1832
1854
|
return { content }
|
|
1833
1855
|
}
|
|
1834
1856
|
|
|
1857
|
+
/**
|
|
1858
|
+
* BUG-002: Detect category-based intent from question messages.
|
|
1859
|
+
* Returns the category if the user is asking about a specific type of memory.
|
|
1860
|
+
*/
|
|
1861
|
+
private detectCategoryIntent(message: string): 'decision' | 'pattern' | 'correction' | null {
|
|
1862
|
+
const lower = message.toLowerCase()
|
|
1863
|
+
|
|
1864
|
+
// Decision-oriented queries
|
|
1865
|
+
if (
|
|
1866
|
+
lower.includes('what decisions') || lower.includes('my decisions') ||
|
|
1867
|
+
lower.includes('what have i decided') || lower.includes('what did i decide') ||
|
|
1868
|
+
lower.includes('what choices') || lower.includes('decisions i') ||
|
|
1869
|
+
lower.includes('list decisions') || lower.includes('show decisions') ||
|
|
1870
|
+
lower.includes('all decisions') || lower.includes('what did i choose')
|
|
1871
|
+
) {
|
|
1872
|
+
return 'decision'
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Pattern-oriented queries
|
|
1876
|
+
if (
|
|
1877
|
+
lower.includes('what patterns') || lower.includes('my patterns') ||
|
|
1878
|
+
lower.includes('best practices') || lower.includes('conventions') ||
|
|
1879
|
+
lower.includes('list patterns') || lower.includes('show patterns') ||
|
|
1880
|
+
lower.includes('all patterns')
|
|
1881
|
+
) {
|
|
1882
|
+
return 'pattern'
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Correction-oriented queries
|
|
1886
|
+
if (
|
|
1887
|
+
lower.includes('what mistakes') || lower.includes('my mistakes') ||
|
|
1888
|
+
lower.includes('what bugs') || lower.includes('lessons learned') ||
|
|
1889
|
+
lower.includes('what corrections') || lower.includes('my corrections') ||
|
|
1890
|
+
lower.includes('list corrections') || lower.includes('show corrections') ||
|
|
1891
|
+
lower.includes('all corrections') || lower.includes('what have i fixed') ||
|
|
1892
|
+
lower.includes('what did i fix')
|
|
1893
|
+
) {
|
|
1894
|
+
return 'correction'
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
return null
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
/**
|
|
1901
|
+
* BUG-002: Handle category-scoped queries by fetching all items of that category.
|
|
1902
|
+
*/
|
|
1903
|
+
private async handleCategoryQuery(
|
|
1904
|
+
message: string,
|
|
1905
|
+
project: string | undefined,
|
|
1906
|
+
category: 'decision' | 'pattern' | 'correction'
|
|
1907
|
+
): Promise<BrainResponse> {
|
|
1908
|
+
const memory = getMemoryService()
|
|
1909
|
+
const projectLabel = project || 'all projects'
|
|
1910
|
+
|
|
1911
|
+
let items: any[]
|
|
1912
|
+
switch (category) {
|
|
1913
|
+
case 'decision':
|
|
1914
|
+
items = await memory.fetchAllDecisions(project)
|
|
1915
|
+
break
|
|
1916
|
+
case 'pattern':
|
|
1917
|
+
items = await memory.fetchAllPatterns(project)
|
|
1918
|
+
break
|
|
1919
|
+
case 'correction':
|
|
1920
|
+
items = await memory.fetchAllCorrections(project)
|
|
1921
|
+
break
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
if (items.length === 0) {
|
|
1925
|
+
return {
|
|
1926
|
+
action: 'none',
|
|
1927
|
+
summary: `No ${category}s found`,
|
|
1928
|
+
content: `No ${category}s found for ${projectLabel}.`,
|
|
1929
|
+
relevantItems: 0
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// Format as compact items
|
|
1934
|
+
const allItems = items.map(item => ({
|
|
1935
|
+
id: item.id || item.decision_id,
|
|
1936
|
+
content: typeof (item.decision || item.description || item.correction || item.original || item.document || item.content) === 'string'
|
|
1937
|
+
? (item.decision || item.description || item.correction || item.original || item.document || item.content)
|
|
1938
|
+
: JSON.stringify(item.decision || item.description || item.correction || item.original || item.document || item.content || ''),
|
|
1939
|
+
category,
|
|
1940
|
+
project: item.project || project || 'general',
|
|
1941
|
+
created_at: item.created_at || item.date || '',
|
|
1942
|
+
}))
|
|
1943
|
+
|
|
1944
|
+
const compactContent = formatCompactResponse(allItems, `${category}s for ${projectLabel}`)
|
|
1945
|
+
return {
|
|
1946
|
+
action: 'retrieved',
|
|
1947
|
+
summary: `${items.length} ${category}s for ${projectLabel}`,
|
|
1948
|
+
content: compactContent,
|
|
1949
|
+
relevantItems: items.length
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1835
1953
|
private generateTaskId(title: string): string {
|
|
1836
1954
|
return title
|
|
1837
1955
|
.toLowerCase()
|
package/src/server/http-api.ts
CHANGED
|
@@ -288,7 +288,7 @@ export class HttpApiServer {
|
|
|
288
288
|
|
|
289
289
|
private async handleRecallSimilar(c: any): Promise<Response> {
|
|
290
290
|
try {
|
|
291
|
-
const query = c.req.query('query') || ''
|
|
291
|
+
const query = c.req.query('query') || c.req.query('q') || ''
|
|
292
292
|
|
|
293
293
|
const memoryService = getMemoryService()
|
|
294
294
|
|
|
@@ -583,6 +583,16 @@ export class HttpApiServer {
|
|
|
583
583
|
}
|
|
584
584
|
}
|
|
585
585
|
|
|
586
|
+
/** Normalize project param — extract basename if full path provided */
|
|
587
|
+
private normalizeProject(raw: string): string {
|
|
588
|
+
if (!raw) return raw
|
|
589
|
+
// If it looks like a path (contains /), extract the last segment
|
|
590
|
+
if (raw.includes('/')) {
|
|
591
|
+
return raw.split('/').filter(Boolean).pop() || raw
|
|
592
|
+
}
|
|
593
|
+
return raw
|
|
594
|
+
}
|
|
595
|
+
|
|
586
596
|
// ─── Phase 28: Code Intelligence Endpoints ─────────────────
|
|
587
597
|
|
|
588
598
|
private async handleCodeIndex(c: any): Promise<Response> {
|
|
@@ -595,7 +605,7 @@ export class HttpApiServer {
|
|
|
595
605
|
}
|
|
596
606
|
|
|
597
607
|
const body = await c.req.json()
|
|
598
|
-
const { projectPath, projectName } = body
|
|
608
|
+
const { projectPath, projectName, force } = body
|
|
599
609
|
|
|
600
610
|
if (!projectPath || !projectName) {
|
|
601
611
|
return Response.json(
|
|
@@ -604,6 +614,20 @@ export class HttpApiServer {
|
|
|
604
614
|
)
|
|
605
615
|
}
|
|
606
616
|
|
|
617
|
+
// BUG-004: Clear existing index data when force flag is set
|
|
618
|
+
if (force && this.codeQuery) {
|
|
619
|
+
try {
|
|
620
|
+
const db = (this.codeQuery as any).db
|
|
621
|
+
if (db) {
|
|
622
|
+
db.run('DELETE FROM code_files WHERE project = ?', projectName)
|
|
623
|
+
db.run('DELETE FROM code_symbols WHERE project = ?', projectName)
|
|
624
|
+
this.logger.info({ projectName }, 'Force reindex: cleared existing data')
|
|
625
|
+
}
|
|
626
|
+
} catch (error) {
|
|
627
|
+
this.logger.warn({ error, projectName }, 'Failed to clear index data for force reindex')
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
607
631
|
const result = await this.codeIndexer.indexProject(projectPath, projectName)
|
|
608
632
|
return Response.json({ success: true, data: result })
|
|
609
633
|
} catch (error) {
|
|
@@ -654,8 +678,8 @@ export class HttpApiServer {
|
|
|
654
678
|
)
|
|
655
679
|
}
|
|
656
680
|
|
|
657
|
-
const query = c.req.query('query') || ''
|
|
658
|
-
const project = c.req.query('project') || ''
|
|
681
|
+
const query = c.req.query('query') || c.req.query('q') || ''
|
|
682
|
+
const project = this.normalizeProject(c.req.query('project') || '')
|
|
659
683
|
const limit = parseInt(c.req.query('limit') || '20', 10)
|
|
660
684
|
|
|
661
685
|
if (!query || !project) {
|
|
@@ -685,11 +709,11 @@ export class HttpApiServer {
|
|
|
685
709
|
)
|
|
686
710
|
}
|
|
687
711
|
|
|
688
|
-
const project = c.req.query('project') || ''
|
|
712
|
+
const project = this.normalizeProject(c.req.query('project') || '')
|
|
689
713
|
|
|
690
714
|
if (!project) {
|
|
691
715
|
return Response.json(
|
|
692
|
-
{ success: false, error: 'project is required' },
|
|
716
|
+
{ success: false, error: 'project query parameter is required (e.g., ?project=my-app)' },
|
|
693
717
|
{ status: 400 }
|
|
694
718
|
)
|
|
695
719
|
}
|
|
@@ -716,11 +740,11 @@ export class HttpApiServer {
|
|
|
716
740
|
}
|
|
717
741
|
|
|
718
742
|
const file = c.req.query('file') || ''
|
|
719
|
-
const project = c.req.query('project') || ''
|
|
743
|
+
const project = this.normalizeProject(c.req.query('project') || '')
|
|
720
744
|
|
|
721
745
|
if (!file || !project) {
|
|
722
746
|
return Response.json(
|
|
723
|
-
{ success: false, error: 'file and project are required' },
|
|
747
|
+
{ success: false, error: 'file and project query parameters are required (e.g., ?file=src/index.ts&project=my-app)' },
|
|
724
748
|
{ status: 400 }
|
|
725
749
|
)
|
|
726
750
|
}
|
|
@@ -745,11 +769,11 @@ export class HttpApiServer {
|
|
|
745
769
|
)
|
|
746
770
|
}
|
|
747
771
|
|
|
748
|
-
const project = c.req.query('project') || ''
|
|
772
|
+
const project = this.normalizeProject(c.req.query('project') || '')
|
|
749
773
|
|
|
750
774
|
if (!project) {
|
|
751
775
|
return Response.json(
|
|
752
|
-
{ success: false, error: 'project is required' },
|
|
776
|
+
{ success: false, error: 'project query parameter is required (e.g., ?project=my-app)' },
|
|
753
777
|
{ status: 400 }
|
|
754
778
|
)
|
|
755
779
|
}
|
|
@@ -769,7 +793,7 @@ export class HttpApiServer {
|
|
|
769
793
|
|
|
770
794
|
private async handleContextQuery(c: any): Promise<Response> {
|
|
771
795
|
try {
|
|
772
|
-
const query = c.req.query('query') || ''
|
|
796
|
+
const query = c.req.query('query') || c.req.query('q') || ''
|
|
773
797
|
const type = c.req.query('type') || ''
|
|
774
798
|
const cwd = c.req.query('cwd') || ''
|
|
775
799
|
const limit = parseInt(c.req.query('limit') || '5', 10)
|
package/src/server/web-viewer.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function setupWebViewer(app: Hono): void {
|
|
|
30
30
|
// Search endpoint — queries FTS5
|
|
31
31
|
app.get('/api/memory/search', async (c) => {
|
|
32
32
|
try {
|
|
33
|
-
const query = c.req.query('q') || ''
|
|
33
|
+
const query = c.req.query('query') || c.req.query('q') || ''
|
|
34
34
|
const project = c.req.query('project') || undefined
|
|
35
35
|
const category = c.req.query('category') || undefined
|
|
36
36
|
const limit = parseInt(c.req.query('limit') || '20', 10)
|
package/src/tools/schemas.ts
CHANGED
|
@@ -600,7 +600,7 @@ export const TOOLS = {
|
|
|
600
600
|
},
|
|
601
601
|
project: {
|
|
602
602
|
type: 'string',
|
|
603
|
-
description: 'Project name (
|
|
603
|
+
description: 'Project name (IMPORTANT: pass this to scope memories correctly, e.g. "my-app"). Auto-detected from message if omitted, defaults to "general".'
|
|
604
604
|
}
|
|
605
605
|
},
|
|
606
606
|
required: ['message']
|