claude-brain 0.5.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -0
- package/package.json +5 -1
- package/packs/backend/node.json +173 -0
- package/packs/core/javascript.json +176 -0
- package/packs/core/typescript.json +222 -0
- package/packs/frontend/react.json +254 -0
- package/packs/meta/testing.json +172 -0
- package/src/cli/bin.ts +14 -0
- package/src/cli/commands/hooks.ts +214 -0
- package/src/cli/commands/pack.ts +197 -0
- package/src/cli/commands/serve.ts +34 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +85 -2
- package/src/hooks/brain-hook.ts +110 -0
- package/src/hooks/capture.ts +161 -0
- package/src/hooks/deduplicator.ts +72 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/installer.ts +181 -0
- package/src/hooks/passive-classifier.ts +366 -0
- package/src/hooks/queue.ts +122 -0
- package/src/hooks/session-tracker.ts +199 -0
- package/src/hooks/types.ts +47 -0
- package/src/memory/chroma/store.ts +2 -1
- package/src/memory/index.ts +1 -0
- package/src/memory/store.ts +1 -0
- package/src/packs/index.ts +9 -0
- package/src/packs/loader.ts +134 -0
- package/src/packs/manager.ts +204 -0
- package/src/packs/ranker.ts +78 -0
- package/src/packs/types.ts +81 -0
- package/src/routing/entity-extractor.ts +410 -0
- package/src/routing/intent-classifier.ts +229 -0
- package/src/routing/response-filter.ts +221 -0
- package/src/routing/router.ts +671 -0
- package/src/server/handlers/call-tool.ts +7 -0
- package/src/server/handlers/list-tools.ts +22 -5
- package/src/server/handlers/tools/brain.ts +85 -0
- package/src/server/handlers/tools/init-project.ts +47 -0
- package/src/server/handlers/tools/schemas.ts +12 -0
- package/src/server/http-api.ts +188 -0
- package/src/tools/registry.ts +9 -0
- package/src/tools/schemas.ts +33 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain Entity Extractor
|
|
3
|
+
* Phase 16: Extracts structured data from natural language messages
|
|
4
|
+
* for the unified brain() tool routing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getVaultService, isServicesInitialized } from '@/server/services'
|
|
8
|
+
|
|
9
|
+
// Reuse phrase lists from decision detector
|
|
10
|
+
const DECISION_PHRASES = [
|
|
11
|
+
'i recommend',
|
|
12
|
+
'you should use',
|
|
13
|
+
'the best approach',
|
|
14
|
+
'i suggest',
|
|
15
|
+
'better to use',
|
|
16
|
+
'prefer using',
|
|
17
|
+
'go with',
|
|
18
|
+
'choose',
|
|
19
|
+
'instead of',
|
|
20
|
+
'the right choice',
|
|
21
|
+
'decided to',
|
|
22
|
+
"let's use",
|
|
23
|
+
'we will use',
|
|
24
|
+
'the solution is',
|
|
25
|
+
'implement using',
|
|
26
|
+
'going with',
|
|
27
|
+
'switching to',
|
|
28
|
+
'adopting',
|
|
29
|
+
'we chose',
|
|
30
|
+
'the plan is to'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const REASONING_PHRASES = [
|
|
34
|
+
'because',
|
|
35
|
+
'since',
|
|
36
|
+
'due to',
|
|
37
|
+
'as it',
|
|
38
|
+
'which provides',
|
|
39
|
+
'this allows',
|
|
40
|
+
'this ensures',
|
|
41
|
+
'given that',
|
|
42
|
+
'considering',
|
|
43
|
+
'the reason is',
|
|
44
|
+
'this way'
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const ALTERNATIVE_PHRASES = [
|
|
48
|
+
'instead of',
|
|
49
|
+
'rather than',
|
|
50
|
+
'over',
|
|
51
|
+
' vs ',
|
|
52
|
+
'compared to',
|
|
53
|
+
'unlike',
|
|
54
|
+
'as opposed to'
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
// Compact tech dictionary for entity extraction (most common technologies)
|
|
58
|
+
// Full dictionary is in knowledge/entity-extractor.ts — we use a subset for speed
|
|
59
|
+
const COMMON_TECH: Set<string> = new Set([
|
|
60
|
+
'typescript', 'javascript', 'python', 'rust', 'go', 'java', 'ruby', 'php', 'swift', 'kotlin',
|
|
61
|
+
'react', 'vue', 'angular', 'svelte', 'nextjs', 'nuxt', 'remix', 'astro', 'solid',
|
|
62
|
+
'express', 'fastify', 'hono', 'nestjs', 'django', 'flask', 'fastapi', 'rails', 'spring',
|
|
63
|
+
'mongodb', 'redis', 'postgresql', 'postgres', 'mysql', 'sqlite', 'dynamodb', 'firebase', 'supabase',
|
|
64
|
+
'prisma', 'drizzle', 'typeorm', 'sequelize', 'chromadb', 'pinecone',
|
|
65
|
+
'docker', 'kubernetes', 'aws', 'gcp', 'azure', 'vercel', 'netlify',
|
|
66
|
+
'webpack', 'vite', 'esbuild', 'bun', 'deno', 'node', 'npm', 'yarn', 'pnpm',
|
|
67
|
+
'jest', 'vitest', 'cypress', 'playwright',
|
|
68
|
+
'tailwind', 'bootstrap', 'zod', 'trpc', 'graphql', 'rest',
|
|
69
|
+
'jwt', 'oauth', 'openai', 'anthropic', 'langchain',
|
|
70
|
+
'git', 'github', 'gitlab', 'eslint', 'prettier',
|
|
71
|
+
'zustand', 'redux', 'pinia', 'mobx', 'jotai', 'recoil',
|
|
72
|
+
'storybook', 'turborepo', 'nx',
|
|
73
|
+
'microservices', 'serverless', 'monolith', 'ssr', 'ssg', 'spa', 'pwa', 'mcp', 'rag'
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
// Aliases for common tech names
|
|
77
|
+
const TECH_ALIASES: Record<string, string> = {
|
|
78
|
+
'ts': 'typescript', 'js': 'javascript', 'py': 'python',
|
|
79
|
+
'react.js': 'react', 'reactjs': 'react', 'vue.js': 'vue', 'vuejs': 'vue',
|
|
80
|
+
'next.js': 'nextjs', 'nuxt.js': 'nuxt', 'nest.js': 'nestjs',
|
|
81
|
+
'express.js': 'express', 'node.js': 'node', 'nodejs': 'node',
|
|
82
|
+
'mongo': 'mongodb', 'pg': 'postgresql', 'k8s': 'kubernetes',
|
|
83
|
+
'tailwindcss': 'tailwind', 'tailwind-css': 'tailwind',
|
|
84
|
+
'gql': 'graphql', 'golang': 'go',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface BrainExtractedEntities {
|
|
88
|
+
project?: string
|
|
89
|
+
technologies: string[]
|
|
90
|
+
decision?: string
|
|
91
|
+
reasoning?: string
|
|
92
|
+
alternatives?: string
|
|
93
|
+
topic?: string
|
|
94
|
+
completedTask?: string
|
|
95
|
+
nextSteps?: string
|
|
96
|
+
original?: string
|
|
97
|
+
correction?: string
|
|
98
|
+
patternType?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class BrainEntityExtractor {
|
|
102
|
+
/**
|
|
103
|
+
* Extract all entities from a natural language message
|
|
104
|
+
*/
|
|
105
|
+
async extract(message: string, knownProject?: string): Promise<BrainExtractedEntities> {
|
|
106
|
+
const entities: BrainExtractedEntities = {
|
|
107
|
+
technologies: []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Use provided project or try to detect from message
|
|
111
|
+
entities.project = knownProject || await this.extractProject(message)
|
|
112
|
+
entities.technologies = this.extractTechnologies(message)
|
|
113
|
+
entities.topic = this.extractTopic(message)
|
|
114
|
+
|
|
115
|
+
// Extract decision components if present
|
|
116
|
+
const decisionParts = this.extractDecision(message)
|
|
117
|
+
if (decisionParts) {
|
|
118
|
+
entities.decision = decisionParts.decision
|
|
119
|
+
entities.reasoning = decisionParts.reasoning
|
|
120
|
+
entities.alternatives = decisionParts.alternatives
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract progress components
|
|
124
|
+
const progress = this.extractProgress(message)
|
|
125
|
+
if (progress) {
|
|
126
|
+
entities.completedTask = progress.completedTask
|
|
127
|
+
entities.nextSteps = progress.nextSteps
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract correction components
|
|
131
|
+
const correction = this.extractCorrection(message)
|
|
132
|
+
if (correction) {
|
|
133
|
+
entities.original = correction.original
|
|
134
|
+
entities.correction = correction.correction
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Extract pattern type
|
|
138
|
+
entities.patternType = this.extractPatternType(message)
|
|
139
|
+
|
|
140
|
+
return entities
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract project name from message
|
|
145
|
+
*/
|
|
146
|
+
private async extractProject(message: string): Promise<string | undefined> {
|
|
147
|
+
const lower = message.toLowerCase()
|
|
148
|
+
|
|
149
|
+
// Pattern: "in project X", "for project X", "on project X"
|
|
150
|
+
const projectPatterns = [
|
|
151
|
+
/(?:in|for|on)\s+(?:the\s+)?project\s+["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
152
|
+
/project\s*:\s*["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
153
|
+
/(?:in|for|on)\s+["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?\s+project/i,
|
|
154
|
+
// "working on X", "starting X", "resuming X"
|
|
155
|
+
/(?:working\s+on|starting|resuming|picking\s+up)\s+(?:the\s+)?["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
for (const pattern of projectPatterns) {
|
|
159
|
+
const match = message.match(pattern)
|
|
160
|
+
if (match && match[1]) {
|
|
161
|
+
const candidate = match[1].toLowerCase()
|
|
162
|
+
// Filter out common false positives
|
|
163
|
+
if (!COMMON_TECH.has(candidate) && candidate.length > 1 && candidate.length < 50) {
|
|
164
|
+
return candidate
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try to match against known projects via vault service
|
|
170
|
+
if (isServicesInitialized()) {
|
|
171
|
+
try {
|
|
172
|
+
const vault = getVaultService()
|
|
173
|
+
const projects = await vault.listProjects()
|
|
174
|
+
for (const project of projects) {
|
|
175
|
+
if (lower.includes(project.toLowerCase())) {
|
|
176
|
+
return project
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Vault service not available, skip project matching
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extract technology mentions
|
|
189
|
+
*/
|
|
190
|
+
private extractTechnologies(message: string): string[] {
|
|
191
|
+
const lower = message.toLowerCase()
|
|
192
|
+
const words = lower.split(/[\s,;:()[\]{}"'`|/\\]+/)
|
|
193
|
+
const found = new Set<string>()
|
|
194
|
+
|
|
195
|
+
for (const word of words) {
|
|
196
|
+
const cleaned = word.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '')
|
|
197
|
+
if (cleaned.length < 2) continue
|
|
198
|
+
|
|
199
|
+
// Direct match
|
|
200
|
+
if (COMMON_TECH.has(cleaned)) {
|
|
201
|
+
found.add(cleaned)
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Alias match
|
|
206
|
+
const alias = TECH_ALIASES[cleaned]
|
|
207
|
+
if (alias) {
|
|
208
|
+
found.add(alias)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check multi-word aliases
|
|
213
|
+
for (const [alias, normalized] of Object.entries(TECH_ALIASES)) {
|
|
214
|
+
if (alias.includes('.') || alias.includes('-')) {
|
|
215
|
+
if (lower.includes(alias)) {
|
|
216
|
+
found.add(normalized)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return Array.from(found)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Extract decision, reasoning, and alternatives
|
|
226
|
+
*/
|
|
227
|
+
private extractDecision(message: string): { decision?: string; reasoning?: string; alternatives?: string } | null {
|
|
228
|
+
const lower = message.toLowerCase()
|
|
229
|
+
|
|
230
|
+
const hasDecision = DECISION_PHRASES.some(p => lower.includes(p))
|
|
231
|
+
if (!hasDecision) return null
|
|
232
|
+
|
|
233
|
+
// Find the decision phrase and extract text after it
|
|
234
|
+
let decision: string | undefined
|
|
235
|
+
for (const phrase of DECISION_PHRASES) {
|
|
236
|
+
const index = lower.indexOf(phrase)
|
|
237
|
+
if (index !== -1) {
|
|
238
|
+
const afterPhrase = message.substring(index + phrase.length).trim()
|
|
239
|
+
// Take until reasoning or sentence end
|
|
240
|
+
const endIndex = this.findClauseEnd(afterPhrase, REASONING_PHRASES)
|
|
241
|
+
decision = afterPhrase.substring(0, endIndex).trim()
|
|
242
|
+
if (decision.length > 5) break
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Extract reasoning
|
|
247
|
+
let reasoning: string | undefined
|
|
248
|
+
for (const phrase of REASONING_PHRASES) {
|
|
249
|
+
const index = lower.indexOf(phrase)
|
|
250
|
+
if (index !== -1) {
|
|
251
|
+
const afterPhrase = message.substring(index).trim()
|
|
252
|
+
const endIndex = Math.min(afterPhrase.length, 300)
|
|
253
|
+
const sentenceEnd = afterPhrase.search(/\n\n/)
|
|
254
|
+
reasoning = afterPhrase.substring(0, sentenceEnd !== -1 ? sentenceEnd : endIndex).trim()
|
|
255
|
+
if (reasoning.length > 10) break
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Extract alternatives
|
|
260
|
+
let alternatives: string | undefined
|
|
261
|
+
for (const phrase of ALTERNATIVE_PHRASES) {
|
|
262
|
+
const index = lower.indexOf(phrase)
|
|
263
|
+
if (index !== -1) {
|
|
264
|
+
const afterPhrase = message.substring(index + phrase.length).trim()
|
|
265
|
+
const endMatch = afterPhrase.match(/[.!?\n]/)
|
|
266
|
+
const endIndex = endMatch ? endMatch.index! + 1 : Math.min(afterPhrase.length, 150)
|
|
267
|
+
alternatives = afterPhrase.substring(0, endIndex).trim()
|
|
268
|
+
if (alternatives.length > 3) break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!decision && !reasoning) return null
|
|
273
|
+
|
|
274
|
+
return { decision, reasoning, alternatives }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Extract progress info: completed task and next steps
|
|
279
|
+
*/
|
|
280
|
+
private extractProgress(message: string): { completedTask?: string; nextSteps?: string } | null {
|
|
281
|
+
const completedPatterns = [
|
|
282
|
+
/(?:finished|completed|done with|implemented|built|created|added|fixed|resolved|shipped)\s+(.+?)(?:\.|,\s*next|$)/i,
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
const nextPatterns = [
|
|
286
|
+
/(?:next\s+(?:is|step|up|i'll|we'll|will\s+be)?|then\s+(?:i'll|we'll|will)|moving\s+(?:on\s+)?to)\s+(.+?)(?:\.|$)/i,
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
let completedTask: string | undefined
|
|
290
|
+
let nextSteps: string | undefined
|
|
291
|
+
|
|
292
|
+
for (const pattern of completedPatterns) {
|
|
293
|
+
const match = message.match(pattern)
|
|
294
|
+
if (match && match[1] && match[1].length > 3) {
|
|
295
|
+
completedTask = match[1].trim()
|
|
296
|
+
break
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const pattern of nextPatterns) {
|
|
301
|
+
const match = message.match(pattern)
|
|
302
|
+
if (match && match[1] && match[1].length > 3) {
|
|
303
|
+
nextSteps = match[1].trim()
|
|
304
|
+
break
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!completedTask) return null
|
|
309
|
+
return { completedTask, nextSteps }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Extract correction: original mistake and what should have been done
|
|
314
|
+
*/
|
|
315
|
+
private extractCorrection(message: string): { original?: string; correction?: string } | null {
|
|
316
|
+
const lower = message.toLowerCase()
|
|
317
|
+
|
|
318
|
+
const correctionIndicators = [
|
|
319
|
+
'the bug was', 'the issue was', 'the problem was', 'mistake was',
|
|
320
|
+
'should have', 'should not have', "shouldn't have",
|
|
321
|
+
'lesson learned', "don't use", 'avoid using', 'never use',
|
|
322
|
+
'the fix is', 'the fix was', 'fixed by', 'solved by'
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
const hasCorrection = correctionIndicators.some(p => lower.includes(p))
|
|
326
|
+
if (!hasCorrection) return null
|
|
327
|
+
|
|
328
|
+
// Extract the original problem
|
|
329
|
+
let original: string | undefined
|
|
330
|
+
const problemPatterns = [
|
|
331
|
+
/(?:the\s+(?:bug|issue|problem|mistake)\s+was)\s+(.+?)(?:\.|,|;|$)/i,
|
|
332
|
+
/(?:should\s+(?:have|not\s+have)|shouldn't\s+have)\s+(.+?)(?:\.|,|;|$)/i
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
for (const pattern of problemPatterns) {
|
|
336
|
+
const match = message.match(pattern)
|
|
337
|
+
if (match && match[1] && match[1].length > 3) {
|
|
338
|
+
original = match[1].trim()
|
|
339
|
+
break
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Extract the correction
|
|
344
|
+
let correction: string | undefined
|
|
345
|
+
const fixPatterns = [
|
|
346
|
+
/(?:the\s+fix\s+(?:is|was)|fixed\s+by|solved\s+by|instead\s+(?:should|use))\s+(.+?)(?:\.|,|;|$)/i,
|
|
347
|
+
/(?:don't\s+use|avoid\s+using|never\s+use)\s+(.+?)(?:\.|,|;|$)/i
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
for (const pattern of fixPatterns) {
|
|
351
|
+
const match = message.match(pattern)
|
|
352
|
+
if (match && match[1] && match[1].length > 3) {
|
|
353
|
+
correction = match[1].trim()
|
|
354
|
+
break
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!original && !correction) return null
|
|
359
|
+
return { original: original || message.slice(0, 200), correction }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Extract pattern type from message
|
|
364
|
+
*/
|
|
365
|
+
private extractPatternType(message: string): BrainExtractedEntities['patternType'] {
|
|
366
|
+
const lower = message.toLowerCase()
|
|
367
|
+
if (lower.includes('anti-pattern') || lower.includes('antipattern')) return 'anti-pattern'
|
|
368
|
+
if (lower.includes('best practice') || lower.includes('best-practice')) return 'best-practice'
|
|
369
|
+
if (lower.includes('common issue') || lower.includes('common-issue') || lower.includes('common problem')) return 'common-issue'
|
|
370
|
+
if (lower.includes('pattern') || lower.includes('reusable solution') || lower.includes('reusable approach')) return 'solution'
|
|
371
|
+
return undefined
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Extract topic from message (main subject without noise words)
|
|
376
|
+
*/
|
|
377
|
+
private extractTopic(message: string): string | undefined {
|
|
378
|
+
// For short messages, the whole thing is the topic
|
|
379
|
+
if (message.length < 80) {
|
|
380
|
+
const cleaned = message.replace(/[?.!]+$/, '').trim()
|
|
381
|
+
if (cleaned.length > 3) return cleaned
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// For longer messages, extract the first meaningful clause
|
|
385
|
+
const sentences = message.split(/[.!?\n]+/).filter(s => s.trim().length > 3)
|
|
386
|
+
const first = sentences[0]
|
|
387
|
+
if (first) {
|
|
388
|
+
return first.trim().slice(0, 200)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return undefined
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Find where a clause ends (at a reasoning phrase or sentence boundary)
|
|
396
|
+
*/
|
|
397
|
+
private findClauseEnd(text: string, stopPhrases: string[]): number {
|
|
398
|
+
const lower = text.toLowerCase()
|
|
399
|
+
|
|
400
|
+
const phraseStart = stopPhrases.reduce((min, phrase) => {
|
|
401
|
+
const idx = lower.indexOf(phrase)
|
|
402
|
+
return idx !== -1 ? Math.min(min, idx) : min
|
|
403
|
+
}, text.length)
|
|
404
|
+
|
|
405
|
+
const sentenceEnd = text.search(/[.!?\n]/)
|
|
406
|
+
const end = sentenceEnd !== -1 ? sentenceEnd : text.length
|
|
407
|
+
|
|
408
|
+
return Math.min(phraseStart, end, 200)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain Intent Classifier
|
|
3
|
+
* Phase 16: Rule-based intent classification for the unified brain() tool
|
|
4
|
+
*
|
|
5
|
+
* Priority-ordered — first confident match wins.
|
|
6
|
+
* No LLM calls, pure pattern matching.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Intent =
|
|
10
|
+
| 'session_start'
|
|
11
|
+
| 'context_needed'
|
|
12
|
+
| 'decision_made'
|
|
13
|
+
| 'pattern_found'
|
|
14
|
+
| 'mistake_learned'
|
|
15
|
+
| 'progress_update'
|
|
16
|
+
| 'question'
|
|
17
|
+
| 'comparison'
|
|
18
|
+
| 'exploration'
|
|
19
|
+
| 'no_action'
|
|
20
|
+
|
|
21
|
+
export interface ClassificationResult {
|
|
22
|
+
primary: Intent
|
|
23
|
+
confidence: number
|
|
24
|
+
secondary: Intent[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Decision-indicating phrases (from automation/decision-detector.ts)
|
|
28
|
+
const DECISION_PHRASES = [
|
|
29
|
+
'i recommend', 'you should use', 'the best approach',
|
|
30
|
+
'i suggest', 'better to use', 'prefer using',
|
|
31
|
+
'go with', 'decided to', "let's use", 'we will use',
|
|
32
|
+
'the solution is', 'implement using', 'going with',
|
|
33
|
+
'switching to', 'adopting', 'we chose', 'the plan is to'
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const REASONING_PHRASES = [
|
|
37
|
+
'because', 'since', 'due to', 'as it', 'which provides',
|
|
38
|
+
'this allows', 'this ensures', 'given that', 'considering',
|
|
39
|
+
'the reason is', 'this way'
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
// Mistake/correction indicators
|
|
43
|
+
const MISTAKE_PHRASES = [
|
|
44
|
+
'bug was', 'the issue was', 'the problem was', 'mistake was',
|
|
45
|
+
'should have', "shouldn't have", 'should not have',
|
|
46
|
+
'lesson learned', "don't use", 'avoid using', 'never use',
|
|
47
|
+
'the fix is', 'the fix was', 'fixed by', 'solved by',
|
|
48
|
+
'was wrong', 'was broken', 'was incorrect'
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
// Progress indicators
|
|
52
|
+
const PROGRESS_PHRASES = [
|
|
53
|
+
'finished', 'completed', 'done with', 'implemented',
|
|
54
|
+
'built', 'created', 'added', 'fixed', 'resolved',
|
|
55
|
+
'shipped', 'deployed', 'merged', 'released'
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
// Comparison indicators
|
|
59
|
+
const COMPARISON_PHRASES = [
|
|
60
|
+
' vs ', ' versus ', 'or should', 'compared to', 'comparing ',
|
|
61
|
+
'which is better', 'should i use', 'should we use',
|
|
62
|
+
'what if we switch', 'what if we change', 'what if we replace',
|
|
63
|
+
'pros and cons', 'tradeoffs', 'trade-offs'
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
// Pattern indicators
|
|
67
|
+
const PATTERN_PHRASES = [
|
|
68
|
+
'pattern:', 'best practice:', 'anti-pattern:', 'reusable solution',
|
|
69
|
+
'reusable approach', 'common issue:', 'this pattern',
|
|
70
|
+
'recognized pattern', 'document this pattern'
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
// Exploration indicators
|
|
74
|
+
const EXPLORATION_PHRASES = [
|
|
75
|
+
'show me', 'timeline', 'trends', 'evolution of',
|
|
76
|
+
'history of', 'graph', 'how has', 'what happened',
|
|
77
|
+
'decisions about', 'when did we', 'list episodes'
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
// Session start indicators
|
|
81
|
+
const SESSION_START_PHRASES = [
|
|
82
|
+
'starting work', 'beginning work', 'resuming',
|
|
83
|
+
'picking up', 'getting started', 'opening',
|
|
84
|
+
'starting on', 'working on', 'beginning on'
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
// Question indicators
|
|
88
|
+
const QUESTION_WORDS = [
|
|
89
|
+
'what', 'how', 'when', 'why', 'where', 'which',
|
|
90
|
+
'who', 'can', 'does', 'is', 'are', 'should', 'could', 'would'
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
// No-action indicators
|
|
94
|
+
const NO_ACTION_PHRASES = [
|
|
95
|
+
'ok', 'okay', 'thanks', 'thank you', 'got it',
|
|
96
|
+
'sure', 'yes', 'no', 'cool', 'nice', 'great',
|
|
97
|
+
'understood', 'noted', 'alright', 'right'
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
export class IntentClassifier {
|
|
101
|
+
/**
|
|
102
|
+
* Classify the intent of a message
|
|
103
|
+
*/
|
|
104
|
+
classify(message: string): ClassificationResult {
|
|
105
|
+
const lower = message.toLowerCase().trim()
|
|
106
|
+
const secondary: Intent[] = []
|
|
107
|
+
|
|
108
|
+
// Check in priority order — first confident match wins
|
|
109
|
+
|
|
110
|
+
// 1. no_action: very short messages, greetings, acknowledgments
|
|
111
|
+
if (this.isNoAction(lower)) {
|
|
112
|
+
return { primary: 'no_action', confidence: 0.95, secondary: [] }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. decision_made: decision phrases + reasoning
|
|
116
|
+
if (this.isDecisionMade(lower)) {
|
|
117
|
+
if (this.hasComparisonSignal(lower)) secondary.push('comparison')
|
|
118
|
+
return { primary: 'decision_made', confidence: 0.85, secondary }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. mistake_learned: correction/bug/lesson indicators
|
|
122
|
+
if (this.isMistakeLearned(lower)) {
|
|
123
|
+
return { primary: 'mistake_learned', confidence: 0.85, secondary }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 4. progress_update: completed task indicators (NOT questions)
|
|
127
|
+
if (this.isProgressUpdate(lower, message)) {
|
|
128
|
+
if (this.hasSessionSignal(lower)) secondary.push('session_start')
|
|
129
|
+
return { primary: 'progress_update', confidence: 0.85, secondary }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 5. comparison: vs, which is better, etc.
|
|
133
|
+
if (this.isComparison(lower)) {
|
|
134
|
+
if (this.isQuestion(lower, message)) secondary.push('question')
|
|
135
|
+
return { primary: 'comparison', confidence: 0.85, secondary }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 6. pattern_found: explicit pattern documentation
|
|
139
|
+
if (this.isPatternFound(lower)) {
|
|
140
|
+
return { primary: 'pattern_found', confidence: 0.80, secondary }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 7. session_start: starting/resuming work
|
|
144
|
+
if (this.isSessionStart(lower)) {
|
|
145
|
+
secondary.push('context_needed')
|
|
146
|
+
return { primary: 'session_start', confidence: 0.90, secondary }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 8. exploration: timeline, trends, graph, history
|
|
150
|
+
if (this.isExploration(lower)) {
|
|
151
|
+
if (this.isQuestion(lower, message)) secondary.push('question')
|
|
152
|
+
return { primary: 'exploration', confidence: 0.75, secondary }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 9. question: starts with question word or ends with ?
|
|
156
|
+
if (this.isQuestion(lower, message)) {
|
|
157
|
+
if (this.hasComparisonSignal(lower)) secondary.push('comparison')
|
|
158
|
+
if (this.hasExplorationSignal(lower)) secondary.push('exploration')
|
|
159
|
+
return { primary: 'question', confidence: 0.80, secondary }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 10. Default: context_needed
|
|
163
|
+
return { primary: 'context_needed', confidence: 0.60, secondary }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private isNoAction(lower: string): boolean {
|
|
167
|
+
if (lower.length > 30) return false
|
|
168
|
+
const words = lower.split(/\s+/)
|
|
169
|
+
if (words.length > 5) return false
|
|
170
|
+
return NO_ACTION_PHRASES.some(p => lower === p || lower === p + '.' || lower === p + '!')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private isDecisionMade(lower: string): boolean {
|
|
174
|
+
const hasDecision = DECISION_PHRASES.some(p => lower.includes(p))
|
|
175
|
+
if (!hasDecision) return false
|
|
176
|
+
// Higher confidence if reasoning is also present
|
|
177
|
+
const hasReasoning = REASONING_PHRASES.some(p => lower.includes(p))
|
|
178
|
+
return hasReasoning || lower.length > 30
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private isMistakeLearned(lower: string): boolean {
|
|
182
|
+
return MISTAKE_PHRASES.some(p => lower.includes(p))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private isProgressUpdate(lower: string, original: string): boolean {
|
|
186
|
+
// Must not be a question
|
|
187
|
+
if (original.trim().endsWith('?')) return false
|
|
188
|
+
const firstWord = lower.split(/\s+/)[0] || ''
|
|
189
|
+
if (QUESTION_WORDS.includes(firstWord)) return false
|
|
190
|
+
|
|
191
|
+
return PROGRESS_PHRASES.some(p => lower.includes(p))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private isComparison(lower: string): boolean {
|
|
195
|
+
return COMPARISON_PHRASES.some(p => lower.includes(p))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private isPatternFound(lower: string): boolean {
|
|
199
|
+
const hasPattern = PATTERN_PHRASES.some(p => lower.includes(p))
|
|
200
|
+
return hasPattern && lower.length > 50
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private isSessionStart(lower: string): boolean {
|
|
204
|
+
return SESSION_START_PHRASES.some(p => lower.includes(p))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private isExploration(lower: string): boolean {
|
|
208
|
+
return EXPLORATION_PHRASES.some(p => lower.includes(p))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private isQuestion(lower: string, original: string): boolean {
|
|
212
|
+
if (original.trim().endsWith('?')) return true
|
|
213
|
+
const firstWord = lower.split(/\s+/)[0] || ''
|
|
214
|
+
return QUESTION_WORDS.includes(firstWord)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Secondary signal checks
|
|
218
|
+
private hasComparisonSignal(lower: string): boolean {
|
|
219
|
+
return COMPARISON_PHRASES.some(p => lower.includes(p))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private hasExplorationSignal(lower: string): boolean {
|
|
223
|
+
return EXPLORATION_PHRASES.some(p => lower.includes(p))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private hasSessionSignal(lower: string): boolean {
|
|
227
|
+
return SESSION_START_PHRASES.some(p => lower.includes(p))
|
|
228
|
+
}
|
|
229
|
+
}
|