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 CHANGED
@@ -1 +1 @@
1
- 0.22.1
1
+ 0.22.3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.22.1",
3
+ "version": "0.22.3",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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 decisions = (db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE category = 'decision' AND archived = 0").get() as any)?.cnt ?? 0
45
- const patterns = (db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE category = 'pattern' AND archived = 0").get() as any)?.cnt ?? 0
46
- const corrections = (db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE category = 'correction' AND archived = 0").get() as any)?.cnt ?? 0
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} (${decisions} decisions, ${patterns} patterns, ${corrections} corrections)`,
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(indexed_at) as latest FROM code_files').get() as any)?.latest
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: string, category?: ObservationCategory): ObservationResult[] {
249
+ fetchAll(project?: string, category?: ObservationCategory): ObservationResult[] {
249
250
  let sql: string
250
- const params: any[] = [project]
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 project = ? AND archived = 0 ORDER BY created_at DESC`
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
- // Split into words, filter short/stop words
352
- const words = cleaned.split(/\s+/).filter(w => w.length >= 2)
353
- if (words.length === 0) return ''
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
- // Join with OR for broad matching
356
- return words.map(w => `"${w}"`).join(' OR ')
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 typical BM25 range to 0.5-0.85
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.5 + normalized * 0.35
395
+ return 0.2 + normalized * 0.7
368
396
  }
369
397
 
370
398
  /**
@@ -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 && project) {
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 && project) {
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 && project) {
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 && project) {
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 && project) {
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
- // Delete old version fires onDecisionDeletedCallbacks (graph + episode cleanup)
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'],
@@ -225,7 +225,7 @@ export class ResponseFilter {
225
225
  ? (left + right) / 2
226
226
  : right
227
227
 
228
- const threshold = Math.max(median * 0.7, 0.35)
228
+ const threshold = Math.max(median * 0.7, 0.45)
229
229
 
230
230
  return results.filter(r => r.score >= threshold)
231
231
  }
@@ -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.5
37
- const UPDATE_MIN_SIMILARITY = 0.5
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()
@@ -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)
@@ -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)
@@ -600,7 +600,7 @@ export const TOOLS = {
600
600
  },
601
601
  project: {
602
602
  type: 'string',
603
- description: 'Project name (optional auto-detected from message if omitted)'
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']