claude-brain 0.30.2 → 0.30.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/README.md +241 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +29 -29
- package/package.json +7 -3
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/scripts/postinstall.mjs +531 -531
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +210 -205
- package/src/cli/auto-setup.ts +75 -75
- package/src/cli/auto-start.ts +266 -266
- package/src/cli/bin.ts +264 -264
- package/src/cli/commands/autostart.ts +90 -90
- package/src/cli/commands/chroma.ts +578 -577
- package/src/cli/commands/export-training.ts +70 -70
- package/src/cli/commands/export.ts +130 -130
- package/src/cli/commands/git-hook.ts +183 -183
- package/src/cli/commands/hooks.ts +217 -217
- package/src/cli/commands/init.ts +123 -123
- package/src/cli/commands/install-mcp.ts +122 -111
- package/src/cli/commands/models.ts +979 -979
- package/src/cli/commands/pack.ts +200 -200
- package/src/cli/commands/refresh.ts +344 -339
- package/src/cli/commands/reindex.ts +120 -120
- package/src/cli/commands/serve.ts +466 -463
- package/src/cli/commands/start.ts +44 -44
- package/src/cli/commands/status.ts +220 -203
- package/src/cli/commands/uninstall-mcp.ts +45 -41
- package/src/cli/commands/update.ts +130 -124
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/code-intelligence/indexer.ts +352 -352
- package/src/code-intelligence/linker.ts +178 -178
- package/src/code-intelligence/parser.ts +484 -484
- package/src/code-intelligence/query.ts +291 -291
- package/src/code-intelligence/schema.ts +83 -83
- package/src/code-intelligence/types.ts +95 -95
- package/src/config/defaults.ts +52 -52
- package/src/config/home.ts +56 -56
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +192 -192
- package/src/config/schema.ts +446 -415
- package/src/config/validator.ts +182 -182
- package/src/context/assembler.ts +407 -400
- package/src/context/index.ts +79 -79
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +122 -121
- package/src/health/index.ts +233 -232
- package/src/hooks/brain-hook.ts +134 -131
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/claude-code-mastery.md +112 -112
- package/src/hooks/context-hook.ts +260 -245
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +211 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +306 -288
- package/src/hooks/interceptor-hook.ts +204 -201
- package/src/hooks/passive-classifier.ts +397 -397
- package/src/hooks/queue.ts +160 -129
- package/src/hooks/session-tracker.ts +312 -312
- package/src/hooks/types.ts +52 -52
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +7 -7
- package/src/intelligence/hf-downloader.ts +222 -222
- package/src/intelligence/hf-manifest.json +78 -78
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/inference-router.ts +762 -762
- package/src/intelligence/model-manager.ts +263 -245
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +213 -207
- package/src/intelligence/prediction/index.ts +7 -7
- package/src/intelligence/prediction/recommender.ts +276 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
- package/src/intelligence/reasoning/index.ts +7 -7
- package/src/intelligence/temporal/evolution.ts +193 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +272 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/intelligence/tokenizer.ts +118 -118
- package/src/knowledge/entity-extractor.ts +447 -443
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +166 -166
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +211 -192
- package/src/memory/chroma/collection-manager.ts +92 -92
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +177 -175
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +319 -315
- package/src/memory/chroma/store.ts +755 -747
- package/src/memory/compression.ts +121 -121
- package/src/memory/consolidation/archiver.ts +162 -165
- package/src/memory/consolidation/merger.ts +182 -186
- package/src/memory/consolidation/scorer.ts +136 -136
- package/src/memory/database.ts +9 -0
- package/src/memory/dual-write.ts +145 -0
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +347 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/fts5-search.ts +692 -633
- package/src/memory/index.ts +943 -1060
- package/src/memory/migrations/add-fts5.ts +118 -108
- package/src/memory/patterns.ts +438 -438
- package/src/memory/pruning.ts +60 -60
- package/src/memory/schema.ts +88 -88
- package/src/memory/store.ts +911 -787
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -297
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +221 -221
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +221 -221
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +165 -165
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +203 -203
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +252 -252
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +189 -188
- package/src/retrieval/reranker/model.ts +99 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +454 -454
- package/src/routing/handlers/exploration-handler.ts +369 -0
- package/src/routing/handlers/index.ts +19 -0
- package/src/routing/handlers/memory-handler.ts +273 -0
- package/src/routing/handlers/mutation-handler.ts +241 -0
- package/src/routing/handlers/recall-handler.ts +642 -0
- package/src/routing/handlers/shared.ts +515 -0
- package/src/routing/handlers/types.ts +48 -0
- package/src/routing/intent-classifier.ts +552 -552
- package/src/routing/response-filter.ts +399 -391
- package/src/routing/router.ts +245 -2193
- package/src/routing/search-engine.ts +521 -514
- package/src/routing/types.ts +104 -94
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/auto-updater.ts +283 -276
- package/src/server/handlers/call-tool.ts +159 -159
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/auto-remember.ts +165 -165
- package/src/server/handlers/tools/brain.ts +86 -86
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/get-code-standards.ts +123 -123
- package/src/server/handlers/tools/get-corrections.ts +152 -152
- package/src/server/handlers/tools/get-patterns.ts +156 -156
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/index.ts +30 -30
- package/src/server/handlers/tools/init-project.ts +756 -756
- package/src/server/handlers/tools/list-projects.ts +126 -126
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +132 -132
- package/src/server/handlers/tools/record-correction.ts +131 -131
- package/src/server/handlers/tools/remember-decision.ts +168 -168
- package/src/server/handlers/tools/schemas.ts +179 -179
- package/src/server/handlers/tools/search-code.ts +122 -122
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/http-api.ts +215 -1229
- package/src/server/mcp-proxy.ts +85 -84
- package/src/server/mcp-server.ts +285 -284
- package/src/server/middleware/auth.ts +39 -0
- package/src/server/middleware/error-handler.ts +37 -0
- package/src/server/middleware/rate-limit.ts +53 -0
- package/src/server/middleware/validate.ts +42 -0
- package/src/server/pid-manager.ts +137 -136
- package/src/server/providers/resources.ts +581 -581
- package/src/server/routes/code.ts +228 -0
- package/src/server/routes/context.ts +26 -0
- package/src/server/routes/health.ts +19 -0
- package/src/server/routes/helpers.ts +100 -0
- package/src/server/routes/hooks.ts +197 -0
- package/src/server/routes/mcp.ts +47 -0
- package/src/server/routes/memory.ts +397 -0
- package/src/server/routes/models.ts +96 -0
- package/src/server/routes/projects.ts +89 -0
- package/src/server/routes/types.ts +21 -0
- package/src/server/schemas/api-schemas.ts +202 -0
- package/src/server/services.ts +720 -720
- package/src/server/utils/memory-indicator.ts +84 -84
- package/src/server/utils/response-formatter.ts +129 -129
- package/src/server/web-viewer.ts +1145 -1115
- package/src/setup/index.ts +38 -38
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.ts +666 -666
- package/src/tools/types.ts +412 -412
- package/src/training/data-store.ts +320 -298
- package/src/training/retrain-pipeline.ts +399 -394
- package/src/utils/error-handler.ts +136 -136
- package/src/utils/index.ts +58 -58
- package/src/utils/kill-port.ts +55 -53
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/safe-path.ts +43 -0
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/index.ts +4 -3
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +4 -1
- package/src/vault/reader.ts +44 -1
- package/src/vault/watcher.ts +24 -1
- package/src/vault/writer.ts +487 -413
- package/skills/persistent-memory/SKILL.md +0 -148
- package/skills/persistent-memory/references/tool-reference.md +0 -90
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Handler Utilities
|
|
3
|
+
* Task 3.1: Helper functions extracted from BrainRouter for use by domain handlers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { HandlerContext, RecentDecision, BrainExtractedEntities, BrainResponse, TierResults, FilterableResult } from './types'
|
|
7
|
+
import { DEFAULT_PROJECT, RECENT_DECISION_WINDOW_MS } from './types'
|
|
8
|
+
import {
|
|
9
|
+
getMemoryService,
|
|
10
|
+
getVaultService,
|
|
11
|
+
getEpisodeService,
|
|
12
|
+
getCodeLinker,
|
|
13
|
+
getCodeQuery
|
|
14
|
+
} from '@/server/services'
|
|
15
|
+
|
|
16
|
+
/** Return a "services not ready" BrainResponse */
|
|
17
|
+
export function servicesNotReady(): BrainResponse {
|
|
18
|
+
return {
|
|
19
|
+
action: 'none',
|
|
20
|
+
summary: 'Services not initialized',
|
|
21
|
+
content: 'Claude Brain services are not ready. The server may still be starting up.',
|
|
22
|
+
relevantItems: 0
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Write decision to vault (fire-and-forget) */
|
|
27
|
+
export async function writeToVault(
|
|
28
|
+
project: string,
|
|
29
|
+
decision: string,
|
|
30
|
+
reasoning: string,
|
|
31
|
+
context: string,
|
|
32
|
+
alternatives?: string
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
const vault = getVaultService()
|
|
36
|
+
const projectPaths = vault.getProjectPaths(project)
|
|
37
|
+
const date = new Date().toISOString().split('T')[0]
|
|
38
|
+
const entry = `### Decision: ${decision.slice(0, 100)}\n\n**Date:** ${date}\n**Context:** ${context}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}\n${alternatives ? `**Alternatives:** ${alternatives}\n` : ''}\n---\n\n`
|
|
39
|
+
await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
|
|
40
|
+
} catch {
|
|
41
|
+
// Vault write failed
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Link a stored decision/pattern/correction to the active episode */
|
|
46
|
+
export function linkToActiveEpisode(project: string, id: string, type: 'decision' | 'pattern' | 'correction'): void {
|
|
47
|
+
try {
|
|
48
|
+
const episodeManager = getEpisodeService()
|
|
49
|
+
if (!episodeManager) return
|
|
50
|
+
const activeEpisode = episodeManager.getActiveEpisode(project)
|
|
51
|
+
if (!activeEpisode) return
|
|
52
|
+
|
|
53
|
+
if (type === 'decision') episodeManager.linkDecision(activeEpisode.id, id)
|
|
54
|
+
else if (type === 'pattern') episodeManager.linkPattern(activeEpisode.id, id)
|
|
55
|
+
else if (type === 'correction') episodeManager.linkCorrection(activeEpisode.id, id)
|
|
56
|
+
} catch {
|
|
57
|
+
// Episode manager not available
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Link a stored observation to code files (non-blocking, fire-and-forget) */
|
|
62
|
+
export function linkToCodeFiles(ctx: HandlerContext, observationId: string, content: string, project: string): void {
|
|
63
|
+
try {
|
|
64
|
+
const linker = getCodeLinker()
|
|
65
|
+
if (linker) {
|
|
66
|
+
linker.linkObservation(observationId, content, project).catch(err => {
|
|
67
|
+
ctx.logger.debug({ err }, 'Code linkage failed (non-fatal)')
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Linker not available
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Register a message with the episode manager */
|
|
76
|
+
export function registerEpisodeMessage(ctx: HandlerContext, message: string, project?: string, role: string = 'user'): void {
|
|
77
|
+
try {
|
|
78
|
+
const episodeManager = getEpisodeService()
|
|
79
|
+
if (!episodeManager) return
|
|
80
|
+
episodeManager.processMessage(
|
|
81
|
+
{ role: role as 'user' | 'assistant' | 'system', content: message, timestamp: new Date().toISOString() },
|
|
82
|
+
project
|
|
83
|
+
).catch((error) => {
|
|
84
|
+
ctx.logger.warn({ error }, 'Background episode message processing failed')
|
|
85
|
+
})
|
|
86
|
+
} catch {
|
|
87
|
+
// Episode manager not available
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Store as a new decision (fallback for update when no match found) */
|
|
92
|
+
export async function storeAsNew(
|
|
93
|
+
ctx: HandlerContext,
|
|
94
|
+
memory: import('@/memory').MemoryManager,
|
|
95
|
+
project: string,
|
|
96
|
+
message: string,
|
|
97
|
+
topic: string,
|
|
98
|
+
entities: BrainExtractedEntities,
|
|
99
|
+
reason: string
|
|
100
|
+
): Promise<BrainResponse> {
|
|
101
|
+
const decisionId = await memory.rememberDecision(
|
|
102
|
+
project,
|
|
103
|
+
topic.slice(0, 200),
|
|
104
|
+
message,
|
|
105
|
+
entities.reasoning || '',
|
|
106
|
+
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
ctx.setLastStored(decisionId, project)
|
|
110
|
+
ctx.searchEngine.invalidateCache(project)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
action: 'stored',
|
|
114
|
+
summary: `Stored as new (${reason})`,
|
|
115
|
+
content: `Stored as new decision (ID: ${decisionId})\n\n**Project:** ${project}\n**Content:** ${message}`,
|
|
116
|
+
relevantItems: 1
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Look up a single observation by ID (FTS5 first, then ChromaDB) */
|
|
121
|
+
export async function lookupById(id: string): Promise<Record<string, unknown> | null> {
|
|
122
|
+
const memory = getMemoryService()
|
|
123
|
+
|
|
124
|
+
// Try FTS5 first (fast, direct lookup)
|
|
125
|
+
if (memory.fts5) {
|
|
126
|
+
const result = memory.fts5.getById(id)
|
|
127
|
+
if (result) return result
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fallback: search ChromaDB by ID if available
|
|
131
|
+
if (memory.isChromaDBEnabled()) {
|
|
132
|
+
try {
|
|
133
|
+
const collections = memory.chroma.collections
|
|
134
|
+
// Try decisions collection
|
|
135
|
+
const decisions = await collections.getDecisions()
|
|
136
|
+
if (decisions) {
|
|
137
|
+
try {
|
|
138
|
+
const result = await decisions.get({ ids: [id] })
|
|
139
|
+
if (result?.documents?.[0]) {
|
|
140
|
+
const meta = (result.metadatas?.[0] || {}) as Record<string, unknown>
|
|
141
|
+
return {
|
|
142
|
+
id,
|
|
143
|
+
content: result.documents[0],
|
|
144
|
+
metadata: meta,
|
|
145
|
+
category: 'decision',
|
|
146
|
+
project: (meta.project as string) || '',
|
|
147
|
+
created_at: (meta.created_at as string) || '',
|
|
148
|
+
reasoning: (meta.reasoning as string) || null,
|
|
149
|
+
tags: [],
|
|
150
|
+
confidence: 0.8
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch { /* ID not in this collection */ }
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// ChromaDB lookup failed
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Increment access count for an observation */
|
|
164
|
+
export function incrementAccess(id: string): void {
|
|
165
|
+
try {
|
|
166
|
+
const memory = getMemoryService()
|
|
167
|
+
if (memory.fts5) {
|
|
168
|
+
memory.fts5.recordAccess(id)
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Non-critical
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Fetch recent observations in a date range */
|
|
176
|
+
export async function fetchRecentObservations(
|
|
177
|
+
project: string | undefined,
|
|
178
|
+
start: Date,
|
|
179
|
+
end: Date
|
|
180
|
+
): Promise<Record<string, unknown>[]> {
|
|
181
|
+
const memory = getMemoryService()
|
|
182
|
+
|
|
183
|
+
// Try FTS5 first (efficient SQL-level date filtering)
|
|
184
|
+
if (memory.fts5 && project) {
|
|
185
|
+
return memory.fts5.fetchByTimeRange(project, start, end)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Also try FTS5 without project filter
|
|
189
|
+
if (memory.fts5 && !project) {
|
|
190
|
+
// FTS5 fetchByTimeRange requires project, use fetchAll with date filtering
|
|
191
|
+
try {
|
|
192
|
+
const all = memory.fts5.search('*', undefined, 100)
|
|
193
|
+
return all.filter(r => {
|
|
194
|
+
const date = new Date(r.created_at)
|
|
195
|
+
if (isNaN(date.getTime())) return false
|
|
196
|
+
return date >= start && date <= end
|
|
197
|
+
}).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
198
|
+
} catch {
|
|
199
|
+
// FTS5 search failed, fall through
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Fallback: use memory fetchAll with date filtering
|
|
204
|
+
try {
|
|
205
|
+
const allDecisions = await memory.fetchAllDecisions(project)
|
|
206
|
+
const allPatterns = await memory.fetchAllPatterns(project)
|
|
207
|
+
const allCorrections = await memory.fetchAllCorrections(project)
|
|
208
|
+
|
|
209
|
+
const allItems = [
|
|
210
|
+
...allDecisions.map((d: Record<string, unknown>) => ({
|
|
211
|
+
id: d.id || d.decision_id,
|
|
212
|
+
content: d.decision || d.document || d.content || '',
|
|
213
|
+
category: 'decision',
|
|
214
|
+
project: d.project || project || DEFAULT_PROJECT,
|
|
215
|
+
created_at: d.created_at || d.date || '',
|
|
216
|
+
})),
|
|
217
|
+
...allPatterns.map((p: Record<string, unknown>) => ({
|
|
218
|
+
id: p.id,
|
|
219
|
+
content: p.description || p.document || p.content || '',
|
|
220
|
+
category: p.pattern_type || 'pattern',
|
|
221
|
+
project: p.project || project || DEFAULT_PROJECT,
|
|
222
|
+
created_at: p.created_at || '',
|
|
223
|
+
})),
|
|
224
|
+
...allCorrections.map((c: Record<string, unknown>) => ({
|
|
225
|
+
id: c.id,
|
|
226
|
+
content: c.original || c.document || c.content || '',
|
|
227
|
+
category: 'correction',
|
|
228
|
+
project: c.project || project || DEFAULT_PROJECT,
|
|
229
|
+
created_at: c.created_at || '',
|
|
230
|
+
})),
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
return allItems.filter(item => {
|
|
234
|
+
if (!item.created_at) return true // include items without dates
|
|
235
|
+
const date = new Date(item.created_at)
|
|
236
|
+
if (isNaN(date.getTime())) return true
|
|
237
|
+
return date >= start && date <= end
|
|
238
|
+
}).sort((a, b) => {
|
|
239
|
+
const dateA = new Date(a.created_at || 0).getTime()
|
|
240
|
+
const dateB = new Date(b.created_at || 0).getTime()
|
|
241
|
+
return dateB - dateA
|
|
242
|
+
})
|
|
243
|
+
} catch {
|
|
244
|
+
return []
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Query code intelligence for symbols and files matching a query */
|
|
249
|
+
export function queryCodeIntelligence(ctx: HandlerContext, query: string, project: string | undefined): TierResults | null {
|
|
250
|
+
try {
|
|
251
|
+
const codeQuery = getCodeQuery()
|
|
252
|
+
if (!codeQuery) return null
|
|
253
|
+
|
|
254
|
+
// Extract meaningful keywords from query for code search
|
|
255
|
+
const keywords = extractCodeKeywords(query)
|
|
256
|
+
if (keywords.length === 0) return null
|
|
257
|
+
|
|
258
|
+
const results: FilterableResult[] = []
|
|
259
|
+
|
|
260
|
+
for (const keyword of keywords) {
|
|
261
|
+
// Search symbols across all indexed projects (project may not match code index project name)
|
|
262
|
+
const symbolResults = codeQuery.findSymbols(keyword, project || '', 5)
|
|
263
|
+
|
|
264
|
+
// If no results with project filter, try without (code index may use path-based project names)
|
|
265
|
+
const symbols = symbolResults.length > 0
|
|
266
|
+
? symbolResults
|
|
267
|
+
: codeQuery.findSymbols(keyword, '', 5)
|
|
268
|
+
|
|
269
|
+
for (const sym of symbols.slice(0, 3)) {
|
|
270
|
+
const location = sym.lineEnd
|
|
271
|
+
? `${sym.filePath}:${sym.lineStart}-${sym.lineEnd}`
|
|
272
|
+
: `${sym.filePath}:${sym.lineStart}`
|
|
273
|
+
const sigPart = sym.signature ? ` — \`${sym.signature}\`` : ''
|
|
274
|
+
results.push({
|
|
275
|
+
content: `**${sym.symbol}** (${sym.type}) at \`${location}\`${sigPart}`,
|
|
276
|
+
score: sym.confidence * 0.6, // Scale down so memories rank higher
|
|
277
|
+
source: 'Code Index',
|
|
278
|
+
metadata: {
|
|
279
|
+
filePath: sym.filePath,
|
|
280
|
+
lineStart: sym.lineStart,
|
|
281
|
+
lineEnd: sym.lineEnd,
|
|
282
|
+
symbolType: sym.type,
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Also search files by name/summary
|
|
288
|
+
const fileResults = codeQuery.findFiles(keyword, project || '')
|
|
289
|
+
const files = fileResults.length > 0
|
|
290
|
+
? fileResults
|
|
291
|
+
: codeQuery.findFiles(keyword, '')
|
|
292
|
+
|
|
293
|
+
for (const file of files.slice(0, 3)) {
|
|
294
|
+
// Skip if we already have symbol results from this file
|
|
295
|
+
if (results.some(r => (r.metadata as Record<string, unknown> | undefined)?.filePath === file.filePath)) continue
|
|
296
|
+
const summaryPart = file.summary ? ` — ${file.summary}` : ''
|
|
297
|
+
results.push({
|
|
298
|
+
content: `**${file.filePath}** (${file.language || 'unknown'}, ${file.symbolCount} symbols, ${file.lineCount} lines)${summaryPart}`,
|
|
299
|
+
score: 0.4, // File matches are lower confidence
|
|
300
|
+
source: 'Code Index',
|
|
301
|
+
metadata: {
|
|
302
|
+
filePath: file.filePath,
|
|
303
|
+
symbolCount: file.symbolCount,
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Deduplicate by filePath
|
|
310
|
+
const seen = new Set<string>()
|
|
311
|
+
const deduped = results.filter(r => {
|
|
312
|
+
const fp = (r.metadata as Record<string, unknown> | undefined)?.filePath
|
|
313
|
+
if (fp && seen.has(fp)) return false
|
|
314
|
+
if (fp) seen.add(fp)
|
|
315
|
+
return true
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
if (deduped.length === 0) return null
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
label: 'Code Intelligence',
|
|
322
|
+
results: deduped.slice(0, 5)
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
ctx.logger.debug({ error }, 'Code intelligence query failed (non-fatal)')
|
|
326
|
+
return null
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Extract meaningful keywords from a natural language query for code search */
|
|
331
|
+
export function extractCodeKeywords(query: string): string[] {
|
|
332
|
+
const STOP_WORDS = new Set([
|
|
333
|
+
'how', 'what', 'where', 'when', 'why', 'who', 'which', 'are', 'is', 'was',
|
|
334
|
+
'were', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did',
|
|
335
|
+
'doing', 'will', 'would', 'could', 'should', 'shall', 'can', 'may', 'might',
|
|
336
|
+
'must', 'the', 'a', 'an', 'and', 'but', 'or', 'nor', 'for', 'yet', 'so',
|
|
337
|
+
'in', 'on', 'at', 'to', 'from', 'by', 'with', 'about', 'into', 'through',
|
|
338
|
+
'during', 'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over',
|
|
339
|
+
'under', 'again', 'further', 'then', 'once', 'here', 'there', 'all', 'each',
|
|
340
|
+
'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'only', 'own',
|
|
341
|
+
'same', 'than', 'too', 'very', 'just', 'because', 'not', 'this', 'that', 'these',
|
|
342
|
+
'those', 'its', 'our', 'their', 'your', 'my', 'me', 'we', 'you', 'they', 'them',
|
|
343
|
+
'know', 'tell', 'show', 'find', 'get', 'make', 'use', 'using', 'used', 'managing',
|
|
344
|
+
'managed', 'handle', 'handling', 'handled', 'work', 'working', 'works', 'implement',
|
|
345
|
+
'implementing', 'implemented', 'doing', 'done', 'project', 'code', 'file', 'function',
|
|
346
|
+
])
|
|
347
|
+
|
|
348
|
+
const words = query.toLowerCase()
|
|
349
|
+
.replace(/[^a-z0-9\s-_]/g, ' ')
|
|
350
|
+
.split(/\s+/)
|
|
351
|
+
.filter(w => w.length > 2 && !STOP_WORDS.has(w))
|
|
352
|
+
|
|
353
|
+
// Also try multi-word phrases (camelCase/kebab-case patterns the user might reference)
|
|
354
|
+
const phrases: string[] = []
|
|
355
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
356
|
+
// Create camelCase combo: "brain router" -> "brainRouter"
|
|
357
|
+
const w1 = words[i]!
|
|
358
|
+
const w2 = words[i + 1]!
|
|
359
|
+
phrases.push(w1 + w2.charAt(0).toUpperCase() + w2.slice(1))
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Return unique keywords, single words first then phrases
|
|
363
|
+
return [...new Set([...words, ...phrases])].slice(0, 5)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Check if a delete/update message has descriptive content beyond just the command words */
|
|
367
|
+
export function hasDescriptiveContent(message: string): boolean {
|
|
368
|
+
const COMMAND_WORDS = [
|
|
369
|
+
'forget', 'delete', 'remove', 'discard', 'erase', 'undo', 'clear', 'drop',
|
|
370
|
+
'actually', 'correction', 'update', 'change', 'replace', 'modify', 'revise',
|
|
371
|
+
'amend', 'override', 'scratch', 'no', 'wait', 'instead',
|
|
372
|
+
'that', 'this', 'the', 'it', 'about', 'memory', 'decision', 'to', 'a', 'an'
|
|
373
|
+
]
|
|
374
|
+
const words = message.toLowerCase().split(/[\s,;:.!?]+/).filter(w => w.length > 1)
|
|
375
|
+
const meaningfulWords = words.filter(w => !COMMAND_WORDS.includes(w))
|
|
376
|
+
return meaningfulWords.length >= 2
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Detect session recall queries */
|
|
380
|
+
export function isSessionRecallQuery(message: string): boolean {
|
|
381
|
+
const lower = message.toLowerCase()
|
|
382
|
+
const SESSION_RECALL_PHRASES = [
|
|
383
|
+
'what have we discussed',
|
|
384
|
+
'what did we discuss',
|
|
385
|
+
'what have we talked about',
|
|
386
|
+
'what did we talk about',
|
|
387
|
+
'this session',
|
|
388
|
+
'current session',
|
|
389
|
+
'session so far',
|
|
390
|
+
'what have we done',
|
|
391
|
+
'what did we do',
|
|
392
|
+
'session summary',
|
|
393
|
+
'summarize this session',
|
|
394
|
+
'recap this session',
|
|
395
|
+
'what happened this session',
|
|
396
|
+
'what have we covered'
|
|
397
|
+
]
|
|
398
|
+
return SESSION_RECALL_PHRASES.some(p => lower.includes(p))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Detect complex multi-part questions */
|
|
402
|
+
export function isComplexQuestion(message: string): boolean {
|
|
403
|
+
// Multiple question marks
|
|
404
|
+
const questionMarks = (message.match(/\?/g) || []).length
|
|
405
|
+
if (questionMarks >= 2) return true
|
|
406
|
+
// Very long question (likely multi-part)
|
|
407
|
+
if (message.length > 200 && message.includes('?')) return true
|
|
408
|
+
// Multiple clauses with "and" or "also"
|
|
409
|
+
if (message.includes(' and ') && message.includes('?') && message.length > 100) return true
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Detect category-based intent from question messages */
|
|
414
|
+
export function detectCategoryIntent(message: string): 'decision' | 'pattern' | 'correction' | null {
|
|
415
|
+
const lower = message.toLowerCase()
|
|
416
|
+
|
|
417
|
+
// Decision-oriented queries
|
|
418
|
+
if (
|
|
419
|
+
lower.includes('what decisions') || lower.includes('my decisions') ||
|
|
420
|
+
lower.includes('what have i decided') || lower.includes('what did i decide') ||
|
|
421
|
+
lower.includes('what choices') || lower.includes('decisions i') ||
|
|
422
|
+
lower.includes('list decisions') || lower.includes('show decisions') ||
|
|
423
|
+
lower.includes('all decisions') || lower.includes('what did i choose')
|
|
424
|
+
) {
|
|
425
|
+
return 'decision'
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Pattern-oriented queries
|
|
429
|
+
if (
|
|
430
|
+
lower.includes('what patterns') || lower.includes('my patterns') ||
|
|
431
|
+
lower.includes('best practices') || lower.includes('conventions') ||
|
|
432
|
+
lower.includes('list patterns') || lower.includes('show patterns') ||
|
|
433
|
+
lower.includes('all patterns')
|
|
434
|
+
) {
|
|
435
|
+
return 'pattern'
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Correction-oriented queries
|
|
439
|
+
if (
|
|
440
|
+
lower.includes('what mistakes') || lower.includes('my mistakes') ||
|
|
441
|
+
lower.includes('what bugs') || lower.includes('lessons learned') ||
|
|
442
|
+
lower.includes('what corrections') || lower.includes('my corrections') ||
|
|
443
|
+
lower.includes('list corrections') || lower.includes('show corrections') ||
|
|
444
|
+
lower.includes('all corrections') || lower.includes('what have i fixed') ||
|
|
445
|
+
lower.includes('what did i fix')
|
|
446
|
+
) {
|
|
447
|
+
return 'correction'
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return null
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Generate a task ID from a title string */
|
|
454
|
+
export function generateTaskId(title: string): string {
|
|
455
|
+
return title
|
|
456
|
+
.toLowerCase()
|
|
457
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
458
|
+
.replace(/^-+|-+$/g, '')
|
|
459
|
+
.slice(0, 50)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Optionally compress content before storing */
|
|
463
|
+
export async function maybeCompress(
|
|
464
|
+
ctx: HandlerContext,
|
|
465
|
+
content: string,
|
|
466
|
+
category: string
|
|
467
|
+
): Promise<{ content: string; rawContent?: string }> {
|
|
468
|
+
if (!ctx.compressor) return { content }
|
|
469
|
+
try {
|
|
470
|
+
const result = await ctx.compressor.compress(content, category)
|
|
471
|
+
if (result.compressed) {
|
|
472
|
+
return { content: result.summary, rawContent: result.original }
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
// Compression failed, use original
|
|
476
|
+
}
|
|
477
|
+
return { content }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Find recent decisions matching a query by keyword overlap */
|
|
481
|
+
export function findRecentDecisions(ctx: HandlerContext, query: string, project?: string): RecentDecision[] {
|
|
482
|
+
const now = Date.now()
|
|
483
|
+
// Prune expired
|
|
484
|
+
ctx.recentDecisions = ctx.recentDecisions.filter(
|
|
485
|
+
d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
const queryWords = new Set(
|
|
489
|
+
query.toLowerCase().split(/\s+/).filter(w => w.length > 2)
|
|
490
|
+
)
|
|
491
|
+
if (queryWords.size === 0) return []
|
|
492
|
+
|
|
493
|
+
const matches: RecentDecision[] = []
|
|
494
|
+
for (const d of ctx.recentDecisions) {
|
|
495
|
+
// Project filter: skip if searching a specific project and it doesn't match
|
|
496
|
+
if (project && d.project !== project) continue
|
|
497
|
+
const contentWords = new Set(d.content.toLowerCase().split(/\s+/).filter(w => w.length > 2))
|
|
498
|
+
const overlap = [...queryWords].filter(w => contentWords.has(w)).length
|
|
499
|
+
if (overlap >= 1) {
|
|
500
|
+
const overlapScore = overlap / queryWords.size
|
|
501
|
+
matches.push({ ...d, overlapScore })
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return matches
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Track a recently stored decision for recency fast path */
|
|
508
|
+
export function trackRecentDecision(ctx: HandlerContext, content: string, project: string): void {
|
|
509
|
+
const now = Date.now()
|
|
510
|
+
// Prune expired entries
|
|
511
|
+
ctx.recentDecisions = ctx.recentDecisions.filter(
|
|
512
|
+
d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
|
|
513
|
+
)
|
|
514
|
+
ctx.recentDecisions.push({ content, project, storedAt: now })
|
|
515
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler Types
|
|
3
|
+
* Task 3.1: Shared types for domain handler modules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from 'pino'
|
|
7
|
+
import type { BrainExtractedEntities } from '../entity-extractor'
|
|
8
|
+
import type { ClassificationResult, IntentClassifier } from '../intent-classifier'
|
|
9
|
+
import type { BrainResponse } from '../response-filter'
|
|
10
|
+
import type { SearchEngine } from '../search-engine'
|
|
11
|
+
import type { ObservationCompressor } from '@/memory/compression'
|
|
12
|
+
|
|
13
|
+
export interface HandlerContext {
|
|
14
|
+
logger: Logger
|
|
15
|
+
searchEngine: SearchEngine
|
|
16
|
+
classifier: IntentClassifier
|
|
17
|
+
compressor: ObservationCompressor | null
|
|
18
|
+
lastStoredId: string | null
|
|
19
|
+
lastStoredProject: string | null
|
|
20
|
+
recentDecisions: RecentDecision[]
|
|
21
|
+
setLastStored(id: string, project: string): void
|
|
22
|
+
trackRecentDecision(content: string, project: string): void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RecentDecision {
|
|
26
|
+
content: string
|
|
27
|
+
project: string
|
|
28
|
+
storedAt: number
|
|
29
|
+
/** Keyword overlap ratio with query (0-1), computed by findRecentDecisions */
|
|
30
|
+
overlapScore?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_PROJECT = 'general'
|
|
34
|
+
export const DELETE_MIN_SIMILARITY = 0.3
|
|
35
|
+
export const UPDATE_MIN_SIMILARITY = 0.3
|
|
36
|
+
export const RECENT_DECISION_WINDOW_MS = 60_000
|
|
37
|
+
|
|
38
|
+
export type IntentHandler = (
|
|
39
|
+
message: string,
|
|
40
|
+
project: string | undefined,
|
|
41
|
+
entities: BrainExtractedEntities,
|
|
42
|
+
classification?: ClassificationResult
|
|
43
|
+
) => Promise<BrainResponse>
|
|
44
|
+
|
|
45
|
+
// Re-export types used by handlers for convenience
|
|
46
|
+
export type { BrainExtractedEntities } from '../entity-extractor'
|
|
47
|
+
export type { ClassificationResult } from '../intent-classifier'
|
|
48
|
+
export type { BrainResponse, TierResults, FilterableResult } from '../response-filter'
|