claude-brain 0.5.0 → 0.8.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.
Files changed (46) hide show
  1. package/VERSION +1 -1
  2. package/assets/CLAUDE-unified.md +11 -0
  3. package/package.json +2 -1
  4. package/packs/backend/node.json +173 -0
  5. package/packs/core/javascript.json +176 -0
  6. package/packs/core/typescript.json +222 -0
  7. package/packs/frontend/react.json +254 -0
  8. package/packs/meta/testing.json +172 -0
  9. package/src/cli/bin.ts +14 -0
  10. package/src/cli/commands/chroma.ts +53 -17
  11. package/src/cli/commands/hooks.ts +214 -0
  12. package/src/cli/commands/pack.ts +197 -0
  13. package/src/cli/commands/serve.ts +34 -0
  14. package/src/config/defaults.ts +1 -1
  15. package/src/config/schema.ts +85 -2
  16. package/src/hooks/brain-hook.ts +110 -0
  17. package/src/hooks/capture.ts +161 -0
  18. package/src/hooks/deduplicator.ts +72 -0
  19. package/src/hooks/index.ts +19 -0
  20. package/src/hooks/installer.ts +181 -0
  21. package/src/hooks/passive-classifier.ts +366 -0
  22. package/src/hooks/queue.ts +122 -0
  23. package/src/hooks/session-tracker.ts +199 -0
  24. package/src/hooks/types.ts +47 -0
  25. package/src/memory/chroma/client.ts +1 -1
  26. package/src/memory/chroma/index.ts +1 -1
  27. package/src/memory/chroma/store.ts +29 -9
  28. package/src/memory/index.ts +1 -0
  29. package/src/memory/store.ts +1 -0
  30. package/src/packs/index.ts +9 -0
  31. package/src/packs/loader.ts +134 -0
  32. package/src/packs/manager.ts +204 -0
  33. package/src/packs/ranker.ts +78 -0
  34. package/src/packs/types.ts +81 -0
  35. package/src/routing/entity-extractor.ts +410 -0
  36. package/src/routing/intent-classifier.ts +229 -0
  37. package/src/routing/response-filter.ts +221 -0
  38. package/src/routing/router.ts +671 -0
  39. package/src/server/handlers/call-tool.ts +7 -0
  40. package/src/server/handlers/list-tools.ts +22 -5
  41. package/src/server/handlers/tools/brain.ts +85 -0
  42. package/src/server/handlers/tools/init-project.ts +47 -0
  43. package/src/server/handlers/tools/schemas.ts +12 -0
  44. package/src/server/http-api.ts +188 -0
  45. package/src/tools/registry.ts +9 -0
  46. package/src/tools/schemas.ts +33 -1
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Phase 18: Knowledge Pack Types & Schemas
3
+ * Defines the format for pre-seeded knowledge packs
4
+ */
5
+
6
+ import { z } from 'zod'
7
+
8
+ /** Valid entry types in a knowledge pack */
9
+ export const PackEntryTypeSchema = z.enum([
10
+ 'pattern',
11
+ 'anti-pattern',
12
+ 'best-practice',
13
+ 'common-issue',
14
+ 'decision-template'
15
+ ])
16
+
17
+ export type PackEntryType = z.infer<typeof PackEntryTypeSchema>
18
+
19
+ /** A single entry in a knowledge pack */
20
+ export const PackEntrySchema = z.object({
21
+ type: PackEntryTypeSchema,
22
+ category: z.string().min(1),
23
+ title: z.string().min(1),
24
+ content: z.string().min(1),
25
+ confidence: z.number().min(0).max(1).default(0.85),
26
+ tags: z.array(z.string()).default([]),
27
+ example: z.string().optional()
28
+ })
29
+
30
+ export type PackEntry = z.infer<typeof PackEntrySchema>
31
+
32
+ /** A complete knowledge pack with metadata */
33
+ export const KnowledgePackSchema = z.object({
34
+ id: z.string().min(1),
35
+ name: z.string().min(1),
36
+ version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver'),
37
+ stack: z.array(z.string()).min(1),
38
+ description: z.string().min(1),
39
+ author: z.string().default('claude-brain'),
40
+ entries: z.array(PackEntrySchema).min(1),
41
+ metadata: z.record(z.string(), z.unknown()).optional()
42
+ })
43
+
44
+ export type KnowledgePack = z.infer<typeof KnowledgePackSchema>
45
+
46
+ /** Tracks which packs have been loaded for a project */
47
+ export interface PackManifestEntry {
48
+ packId: string
49
+ version: string
50
+ entriesLoaded: number
51
+ loadedAt: string
52
+ }
53
+
54
+ export interface PackManifest {
55
+ project: string
56
+ packs: PackManifestEntry[]
57
+ lastUpdated: string
58
+ }
59
+
60
+ /** Result of loading packs for a project */
61
+ export interface PackLoadResult {
62
+ packsLoaded: number
63
+ entriesLoaded: number
64
+ packDetails: Array<{
65
+ packId: string
66
+ name: string
67
+ entriesLoaded: number
68
+ }>
69
+ skipped: Array<{
70
+ packId: string
71
+ reason: string
72
+ }>
73
+ }
74
+
75
+ /** Maps pack entry types to the storage pattern_type values */
76
+ export const ENTRY_TYPE_TO_PATTERN_TYPE: Record<string, 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'> = {
77
+ 'pattern': 'solution',
78
+ 'anti-pattern': 'anti-pattern',
79
+ 'best-practice': 'best-practice',
80
+ 'common-issue': 'common-issue'
81
+ }
@@ -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
+ }