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,214 @@
1
+ /**
2
+ * Phase 17: CLI Hooks Command
3
+ * Manages passive learning hooks (install/uninstall/status/enable/disable)
4
+ */
5
+
6
+ import {
7
+ renderLogo, theme, heading, successText, warningText, errorText, dimText,
8
+ box, summaryPanel, withSpinner,
9
+ } from '@/cli/ui/index.js'
10
+ import { installHooks, uninstallHooks, isHooksInstalled, getHookScriptPath } from '@/hooks/installer'
11
+ import { readQueue } from '@/hooks/queue'
12
+
13
+ export async function runHooks() {
14
+ const subcommand = process.argv[3] || 'status'
15
+
16
+ switch (subcommand) {
17
+ case 'install':
18
+ await handleInstall()
19
+ break
20
+ case 'uninstall':
21
+ await handleUninstall()
22
+ break
23
+ case 'status':
24
+ await handleStatus()
25
+ break
26
+ case 'enable':
27
+ await handleToggle(true)
28
+ break
29
+ case 'disable':
30
+ await handleToggle(false)
31
+ break
32
+ default:
33
+ console.log()
34
+ console.log(errorText(`Unknown hooks subcommand: ${subcommand}`))
35
+ printHooksHelp()
36
+ process.exit(1)
37
+ }
38
+ }
39
+
40
+ async function handleInstall() {
41
+ console.log()
42
+ console.log(renderLogo())
43
+ console.log()
44
+ console.log(heading('Install Passive Learning Hooks'))
45
+ console.log()
46
+
47
+ try {
48
+ let result: { installed: boolean; message: string } | undefined
49
+ await withSpinner('Installing hooks into Claude Code', async () => {
50
+ result = installHooks()
51
+ })
52
+
53
+ console.log()
54
+ if (result?.installed) {
55
+ console.log(box([
56
+ successText(result.message),
57
+ '',
58
+ dimText('Hooks will fire on every tool call in Claude Code.'),
59
+ dimText('Knowledge is captured and sent to the brain server.'),
60
+ '',
61
+ `${theme.dim('Hook script:')} ${getHookScriptPath()}`,
62
+ `${theme.dim('Settings:')} ~/.claude/settings.json`,
63
+ '',
64
+ dimText('Enable passive learning:'),
65
+ ` ${theme.bold('CLAUDE_BRAIN_HOOKS_ENABLED=true')}`,
66
+ '',
67
+ dimText('Or enable in config:'),
68
+ ` ${theme.bold('hooks.enabled: true')} ${dimText('in ~/.claude-brain/.env')}`,
69
+ ].join('\n'), 'Hooks Installed'))
70
+ }
71
+ } catch (err) {
72
+ console.log()
73
+ console.log(box(
74
+ errorText(`Failed to install hooks: ${err instanceof Error ? err.message : String(err)}`),
75
+ 'Error'
76
+ ))
77
+ process.exit(1)
78
+ }
79
+ console.log()
80
+ }
81
+
82
+ async function handleUninstall() {
83
+ console.log()
84
+ console.log(renderLogo())
85
+ console.log()
86
+ console.log(heading('Uninstall Passive Learning Hooks'))
87
+ console.log()
88
+
89
+ try {
90
+ let result: { uninstalled: boolean; message: string } | undefined
91
+ await withSpinner('Removing hooks from Claude Code', async () => {
92
+ result = uninstallHooks()
93
+ })
94
+
95
+ console.log()
96
+ if (result?.uninstalled) {
97
+ console.log(box(
98
+ successText(result.message),
99
+ 'Hooks Removed'
100
+ ))
101
+ }
102
+ } catch (err) {
103
+ console.log()
104
+ console.log(box(
105
+ errorText(`Failed to uninstall hooks: ${err instanceof Error ? err.message : String(err)}`),
106
+ 'Error'
107
+ ))
108
+ process.exit(1)
109
+ }
110
+ console.log()
111
+ }
112
+
113
+ async function handleStatus() {
114
+ console.log()
115
+ console.log(renderLogo())
116
+ console.log()
117
+ console.log(heading('Hook Status'))
118
+ console.log()
119
+
120
+ const installed = isHooksInstalled()
121
+ const queueItems = readQueue()
122
+ const envEnabled = process.env.CLAUDE_BRAIN_HOOKS_ENABLED !== 'false'
123
+
124
+ const items = [
125
+ { label: 'Installed', value: installed ? 'Yes' : 'No', status: installed ? 'success' as const : 'warning' as const },
126
+ { label: 'Env enabled', value: envEnabled ? 'Yes' : 'No', status: envEnabled ? 'success' as const : 'info' as const },
127
+ { label: 'Queue items', value: String(queueItems.length), status: queueItems.length > 0 ? 'warning' as const : 'info' as const },
128
+ { label: 'Hook script', value: getHookScriptPath(), status: 'info' as const },
129
+ ]
130
+
131
+ console.log(summaryPanel('Passive Learning Hooks', items))
132
+
133
+ // Try to fetch live stats from the server
134
+ try {
135
+ const port = parseInt(process.env.CLAUDE_BRAIN_PORT || '3000', 10)
136
+ const res = await fetch(`http://localhost:${port}/api/hooks/status`, {
137
+ signal: AbortSignal.timeout(2000),
138
+ })
139
+
140
+ if (res.ok) {
141
+ const data = await res.json() as any
142
+ if (data.success && data.data) {
143
+ console.log()
144
+ const serverItems = [
145
+ { label: 'Total captured', value: String(data.data.totalCaptured || 0), status: 'info' as const },
146
+ { label: 'Total skipped', value: String(data.data.totalSkipped || 0), status: 'info' as const },
147
+ { label: 'Total merged', value: String(data.data.totalMerged || 0), status: 'info' as const },
148
+ { label: 'Active sessions', value: String(data.data.activeSessions || 0), status: 'info' as const },
149
+ { label: 'Last capture', value: data.data.lastCaptureAt || 'Never', status: 'info' as const },
150
+ ]
151
+ console.log(summaryPanel('Server Stats', serverItems))
152
+ }
153
+ }
154
+ } catch {
155
+ console.log()
156
+ console.log(dimText(' (Server not running — no live stats available)'))
157
+ }
158
+
159
+ console.log()
160
+ }
161
+
162
+ async function handleToggle(enable: boolean) {
163
+ console.log()
164
+ console.log(renderLogo())
165
+ console.log()
166
+
167
+ const action = enable ? 'Enabling' : 'Disabling'
168
+ console.log(heading(`${action} Passive Learning`))
169
+ console.log()
170
+
171
+ // Toggle via env hint (actual config toggle would need config file editing)
172
+ if (enable) {
173
+ console.log(box([
174
+ successText('To enable hooks, set the environment variable:'),
175
+ '',
176
+ ` ${theme.bold('export CLAUDE_BRAIN_HOOKS_ENABLED=true')}`,
177
+ '',
178
+ dimText('Or add to your shell profile (.bashrc, .zshrc):'),
179
+ ` ${dimText('echo \'export CLAUDE_BRAIN_HOOKS_ENABLED=true\' >> ~/.zshrc')}`,
180
+ ].join('\n'), 'Enable Hooks'))
181
+ } else {
182
+ console.log(box([
183
+ warningText('To disable hooks, set the environment variable:'),
184
+ '',
185
+ ` ${theme.bold('export CLAUDE_BRAIN_HOOKS_ENABLED=false')}`,
186
+ '',
187
+ dimText('This stops hook processing without uninstalling.'),
188
+ ].join('\n'), 'Disable Hooks'))
189
+ }
190
+ console.log()
191
+ }
192
+
193
+ function printHooksHelp() {
194
+ console.log()
195
+ const commands = [
196
+ ['install', 'Install hooks into Claude Code settings'],
197
+ ['uninstall', 'Remove hooks from Claude Code settings'],
198
+ ['status', 'Show hook installation and capture stats'],
199
+ ['enable', 'Enable passive learning (show instructions)'],
200
+ ['disable', 'Disable passive learning (show instructions)'],
201
+ ]
202
+
203
+ const cmdLines = commands
204
+ .map(([cmd, desc]) => ` ${theme.primary(cmd!.padEnd(14))} ${dimText(desc!)}`)
205
+ .join('\n')
206
+
207
+ console.log(box([
208
+ theme.bold('Usage:') + ' ' + dimText('claude-brain hooks [subcommand]'),
209
+ '',
210
+ theme.bold('Subcommands:'),
211
+ cmdLines,
212
+ ].join('\n'), 'Hooks Help'))
213
+ console.log()
214
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Phase 18: CLI Pack Command
3
+ * Manages knowledge packs (list/status/reload)
4
+ */
5
+
6
+ import {
7
+ renderLogo, theme, heading, successText, warningText, errorText, dimText,
8
+ box, summaryPanel, withSpinner,
9
+ } from '@/cli/ui/index.js'
10
+ import { readFileSync } from 'node:fs'
11
+ import { resolve, dirname, join } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { PackManager } from '@/packs/manager'
14
+
15
+ const __filename = fileURLToPath(import.meta.url)
16
+ const __dirname = dirname(__filename)
17
+ const PACKAGE_ROOT = resolve(__dirname, '..', '..', '..')
18
+
19
+ function getDataDir(): string {
20
+ return join(
21
+ process.env.CLAUDE_BRAIN_HOME || join(process.env.HOME || '~', '.claude-brain'),
22
+ 'data'
23
+ )
24
+ }
25
+
26
+ function getDefaultConfig() {
27
+ return {
28
+ enabled: true,
29
+ packsDir: 'packs',
30
+ alwaysLoadCore: true,
31
+ alwaysLoadMeta: true,
32
+ communityConfidenceMultiplier: 0.8,
33
+ personalBoost: 1.2,
34
+ projectBoost: 1.15
35
+ }
36
+ }
37
+
38
+ function createPackManager(): PackManager {
39
+ // Lightweight logger for CLI
40
+ const logger = {
41
+ child: () => logger,
42
+ debug: () => {},
43
+ info: () => {},
44
+ warn: () => {},
45
+ error: () => {},
46
+ } as any
47
+
48
+ return new PackManager(logger, getDefaultConfig(), PACKAGE_ROOT, getDataDir())
49
+ }
50
+
51
+ export async function runPack() {
52
+ const subcommand = process.argv[3] || 'list'
53
+
54
+ switch (subcommand) {
55
+ case 'list':
56
+ await handleList()
57
+ break
58
+ case 'status':
59
+ await handleStatus()
60
+ break
61
+ case 'reload':
62
+ await handleReload()
63
+ break
64
+ default:
65
+ console.log()
66
+ console.log(errorText(`Unknown pack subcommand: ${subcommand}`))
67
+ printPackHelp()
68
+ process.exit(1)
69
+ }
70
+ }
71
+
72
+ async function handleList() {
73
+ console.log()
74
+ console.log(renderLogo())
75
+ console.log()
76
+ console.log(heading('Available Knowledge Packs'))
77
+ console.log()
78
+
79
+ const manager = createPackManager()
80
+
81
+ let packs: Array<{ id: string; name: string; description: string; entries: number; version: string }> = []
82
+ await withSpinner('Scanning packs directory', async () => {
83
+ packs = await manager.listAvailablePacks()
84
+ })
85
+
86
+ if (packs.length === 0) {
87
+ console.log(warningText(' No packs found in packs/ directory'))
88
+ console.log()
89
+ return
90
+ }
91
+
92
+ console.log()
93
+ for (const pack of packs) {
94
+ console.log(` ${theme.primary(pack.id.padEnd(24))} ${theme.bold(pack.name)}`)
95
+ console.log(` ${''.padEnd(24)} ${dimText(pack.description)}`)
96
+ console.log(` ${''.padEnd(24)} ${dimText(`v${pack.version} - ${pack.entries} entries`)}`)
97
+ console.log()
98
+ }
99
+
100
+ const totalEntries = packs.reduce((sum, p) => sum + p.entries, 0)
101
+ console.log(dimText(` Total: ${packs.length} packs, ${totalEntries} entries`))
102
+ console.log()
103
+ }
104
+
105
+ async function handleStatus() {
106
+ console.log()
107
+ console.log(renderLogo())
108
+ console.log()
109
+ console.log(heading('Pack Status'))
110
+ console.log()
111
+
112
+ const project = process.argv[4]
113
+ if (!project) {
114
+ console.log(warningText(' Usage: claude-brain pack status <project-name>'))
115
+ console.log(dimText(' Example: claude-brain pack status my-app'))
116
+ console.log()
117
+ return
118
+ }
119
+
120
+ const manager = createPackManager()
121
+ const manifest = await manager.getManifest(project)
122
+
123
+ if (manifest.packs.length === 0) {
124
+ console.log(dimText(` No packs loaded for project "${project}"`))
125
+ console.log(dimText(' Packs are loaded automatically when you run init_project'))
126
+ console.log()
127
+ return
128
+ }
129
+
130
+ const items = manifest.packs.map(p => ({
131
+ label: p.packId,
132
+ value: `v${p.version} (${p.entriesLoaded} entries, ${new Date(p.loadedAt).toLocaleDateString()})`,
133
+ status: 'success' as const
134
+ }))
135
+
136
+ console.log(summaryPanel(`Packs for ${project}`, items))
137
+ console.log()
138
+ console.log(dimText(` Last updated: ${manifest.lastUpdated}`))
139
+ console.log()
140
+ }
141
+
142
+ async function handleReload() {
143
+ console.log()
144
+ console.log(renderLogo())
145
+ console.log()
146
+ console.log(heading('Reload Knowledge Packs'))
147
+ console.log()
148
+
149
+ const project = process.argv[4]
150
+ if (!project) {
151
+ console.log(warningText(' Usage: claude-brain pack reload <project-name>'))
152
+ console.log(dimText(' This clears the manifest so packs are re-loaded on next init_project'))
153
+ console.log()
154
+ return
155
+ }
156
+
157
+ const manager = createPackManager()
158
+
159
+ await withSpinner(`Clearing manifest for "${project}"`, async () => {
160
+ await manager.deleteManifest(project)
161
+ })
162
+
163
+ console.log()
164
+ console.log(box([
165
+ successText(`Manifest cleared for "${project}"`),
166
+ '',
167
+ dimText('Packs will be re-loaded on the next init_project call.'),
168
+ dimText('Or use the init_project MCP tool to reload now.'),
169
+ ].join('\n'), 'Reload'))
170
+ console.log()
171
+ }
172
+
173
+ function printPackHelp() {
174
+ console.log()
175
+ const commands = [
176
+ ['list', 'Show all available knowledge packs'],
177
+ ['status', 'Show loaded packs for a project'],
178
+ ['reload', 'Clear manifest to force pack reload'],
179
+ ]
180
+
181
+ const cmdLines = commands
182
+ .map(([cmd, desc]) => ` ${theme.primary(cmd!.padEnd(14))} ${dimText(desc!)}`)
183
+ .join('\n')
184
+
185
+ console.log(box([
186
+ theme.bold('Usage:') + ' ' + dimText('claude-brain pack [subcommand]'),
187
+ '',
188
+ theme.bold('Subcommands:'),
189
+ cmdLines,
190
+ '',
191
+ theme.bold('Examples:'),
192
+ ` ${dimText('claude-brain pack list')}`,
193
+ ` ${dimText('claude-brain pack status my-project')}`,
194
+ ` ${dimText('claude-brain pack reload my-project')}`,
195
+ ].join('\n'), 'Pack Help'))
196
+ console.log()
197
+ }
@@ -110,11 +110,45 @@ export async function runServe() {
110
110
  await httpServer.stop()
111
111
  })
112
112
 
113
+ // Phase 17: Initialize hook session tracker + queue drain
114
+ let hookSessionTracker: any = null
115
+ if (config.hooks?.enabled !== false) {
116
+ try {
117
+ const { HookSessionTracker } = await import('@/hooks/session-tracker')
118
+ const { getEpisodeService } = await import('@/server/services')
119
+ const episodeManager = getEpisodeService()
120
+ hookSessionTracker = new HookSessionTracker(logger, episodeManager, config.hooks?.sessions)
121
+ httpServer.setSessionTracker(hookSessionTracker)
122
+
123
+ cleanup.register(async () => {
124
+ if (hookSessionTracker) {
125
+ await hookSessionTracker.endAllSessions()
126
+ mainLogger.info('Hook session tracker shut down')
127
+ }
128
+ })
129
+
130
+ mainLogger.info('Hook session tracker initialized')
131
+ } catch (error) {
132
+ mainLogger.warn({ error }, 'Failed to initialize hook session tracker, continuing without passive learning')
133
+ }
134
+ }
135
+
113
136
  // Start HTTP server after MCP server is ready
114
137
  setTimeout(async () => {
115
138
  try {
116
139
  await httpServer.start()
117
140
  mainLogger.info({ port: config.port }, 'HTTP API server started')
141
+
142
+ // Drain hook queue after HTTP server is ready
143
+ try {
144
+ const { drainQueue } = await import('@/hooks/queue')
145
+ const drained = await drainQueue(config.port)
146
+ if (drained > 0) {
147
+ mainLogger.info({ drained }, 'Drained hook queue')
148
+ }
149
+ } catch (error) {
150
+ mainLogger.debug({ error }, 'No hook queue to drain')
151
+ }
118
152
  } catch (error) {
119
153
  mainLogger.error({ error }, 'Failed to start HTTP API server')
120
154
  }
@@ -3,7 +3,7 @@ import type { PartialConfig } from './schema'
3
3
  /** Default configuration values for Claude Brain */
4
4
  export const defaultConfig: PartialConfig = {
5
5
  serverName: 'claude-brain',
6
- serverVersion: '0.5.0',
6
+ serverVersion: '0.8.0',
7
7
  logLevel: 'info',
8
8
  logFilePath: './logs/claude-brain.log',
9
9
  dbPath: './data/memory.db',
@@ -131,6 +131,80 @@ export const KnowledgeConfigSchema = z.object({
131
131
 
132
132
  export type KnowledgeConfig = z.infer<typeof KnowledgeConfigSchema>
133
133
 
134
+ /** Phase 17: Passive Learning via Hooks configuration */
135
+ export const HooksConfigSchema = z.object({
136
+ /** Master switch for hooks passive learning */
137
+ enabled: z.boolean().default(false),
138
+
139
+ /** What to capture from tool calls */
140
+ capture: z.object({
141
+ /** Capture knowledge from tool use (Edit/Write) */
142
+ toolUse: z.boolean().default(true),
143
+ /** Capture knowledge from file edits */
144
+ fileEdits: z.boolean().default(true),
145
+ /** Capture knowledge from bash commands */
146
+ bashCommands: z.boolean().default(true),
147
+ /** Capture knowledge from user messages */
148
+ userMessages: z.boolean().default(true)
149
+ }).default({}),
150
+
151
+ /** Privacy filters */
152
+ privacy: z.object({
153
+ /** File paths to ignore (glob patterns) */
154
+ ignorePaths: z.array(z.string()).default([]),
155
+ /** Projects to ignore */
156
+ ignoreProjects: z.array(z.string()).default([]),
157
+ /** Minimum confidence to store captured knowledge */
158
+ minConfidence: z.number().min(0).max(1).default(0.7)
159
+ }).default({}),
160
+
161
+ /** Session tracking settings */
162
+ sessions: z.object({
163
+ /** Enable session tracking */
164
+ enabled: z.boolean().default(true),
165
+ /** Idle timeout in minutes before summarizing */
166
+ idleTimeoutMinutes: z.number().int().min(1).default(30),
167
+ /** Minimum events required before generating summary */
168
+ minEventsForSummary: z.number().int().min(1).default(3)
169
+ }).default({}),
170
+
171
+ /** Deduplication thresholds */
172
+ deduplication: z.object({
173
+ /** Similarity above this skips storage (exact duplicate) */
174
+ skipThreshold: z.number().min(0).max(1).default(0.95),
175
+ /** Similarity above this merges instead of creating new */
176
+ mergeThreshold: z.number().min(0).max(1).default(0.85)
177
+ }).default({})
178
+ })
179
+
180
+ export type HooksConfig = z.infer<typeof HooksConfigSchema>
181
+
182
+ /** Phase 18: Knowledge Packs configuration */
183
+ export const PacksConfigSchema = z.object({
184
+ /** Master switch for knowledge packs */
185
+ enabled: z.boolean().default(true),
186
+
187
+ /** Directory name for pack files (relative to package root) */
188
+ packsDir: z.string().default('packs'),
189
+
190
+ /** Always load core/ packs regardless of tech stack */
191
+ alwaysLoadCore: z.boolean().default(true),
192
+
193
+ /** Always load meta/ packs regardless of tech stack */
194
+ alwaysLoadMeta: z.boolean().default(true),
195
+
196
+ /** Confidence multiplier for community (pack) entries (dampens scores) */
197
+ communityConfidenceMultiplier: z.number().min(0).max(1).default(0.8),
198
+
199
+ /** Search result boost for personal (user-created) entries */
200
+ personalBoost: z.number().min(1).max(2).default(1.2),
201
+
202
+ /** Search result boost for project-specific entries */
203
+ projectBoost: z.number().min(1).max(2).default(1.15)
204
+ })
205
+
206
+ export type PacksConfig = z.infer<typeof PacksConfigSchema>
207
+
134
208
  /** Phase 15: Advanced Intelligence configuration */
135
209
  export const AdvancedIntelligenceConfigSchema = z.object({
136
210
  /** Enable advanced intelligence features (Phase 15) */
@@ -196,7 +270,7 @@ export const ConfigSchema = z.object({
196
270
  serverName: z.string().default('claude-brain'),
197
271
 
198
272
  /** Server version in semver format */
199
- serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.5.0'),
273
+ serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.8.0'),
200
274
 
201
275
  /** Logging level */
202
276
  logLevel: LogLevelSchema.default('info'),
@@ -248,7 +322,16 @@ export const ConfigSchema = z.object({
248
322
  knowledge: KnowledgeConfigSchema.default({}),
249
323
 
250
324
  /** Advanced intelligence configuration (Phase 15) */
251
- advancedIntelligence: AdvancedIntelligenceConfigSchema.default({})
325
+ advancedIntelligence: AdvancedIntelligenceConfigSchema.default({}),
326
+
327
+ /** Phase 16: Unified tool mode — expose only brain() instead of all 25 tools */
328
+ unifiedToolMode: z.boolean().default(false),
329
+
330
+ /** Phase 17: Passive learning hooks configuration */
331
+ hooks: HooksConfigSchema.default({}),
332
+
333
+ /** Phase 18: Knowledge packs configuration */
334
+ packs: PacksConfigSchema.default({})
252
335
  })
253
336
 
254
337
  export type Config = z.infer<typeof ConfigSchema>
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Phase 17: Brain Hook Worker Script
4
+ * Standalone entry point executed by Claude Code on every tool call.
5
+ *
6
+ * CRITICAL CONSTRAINTS:
7
+ * - Must complete in <200ms (no heavy imports like ChromaDB/embeddings)
8
+ * - NEVER write to stdout (would corrupt Claude Code JSON-RPC)
9
+ * - All errors silently caught with process.exit(0)
10
+ * - Reads stdin JSON from Claude Code, classifies, POSTs to HTTP API
11
+ */
12
+
13
+ import { BrainCapture } from './capture'
14
+ import { appendToQueue } from './queue'
15
+ import type { HookInput } from './types'
16
+
17
+ async function main(): Promise<void> {
18
+ // Parse --event arg
19
+ const eventIdx = process.argv.indexOf('--event')
20
+ const eventName = eventIdx >= 0 ? process.argv[eventIdx + 1] : undefined
21
+
22
+ // Read stdin JSON from Claude Code
23
+ let rawInput: string
24
+ try {
25
+ rawInput = await readStdin()
26
+ } catch {
27
+ process.exit(0)
28
+ return
29
+ }
30
+
31
+ if (!rawInput.trim()) {
32
+ process.exit(0)
33
+ return
34
+ }
35
+
36
+ let input: HookInput
37
+ try {
38
+ input = JSON.parse(rawInput)
39
+ } catch {
40
+ process.exit(0)
41
+ return
42
+ }
43
+
44
+ // Override event name from arg if provided
45
+ if (eventName) {
46
+ input.hook_event_name = eventName as HookInput['hook_event_name']
47
+ }
48
+
49
+ // Load minimal config from env vars
50
+ const enabled = process.env.CLAUDE_BRAIN_HOOKS_ENABLED !== 'false'
51
+ if (!enabled) {
52
+ process.exit(0)
53
+ return
54
+ }
55
+
56
+ const port = parseInt(process.env.CLAUDE_BRAIN_PORT || '3000', 10)
57
+
58
+ // Process the hook event
59
+ const capture = new BrainCapture({ enabled: true })
60
+ const knowledge = capture.process(input)
61
+
62
+ if (!knowledge) {
63
+ process.exit(0)
64
+ return
65
+ }
66
+
67
+ // POST to HTTP API server
68
+ try {
69
+ const res = await fetch(`http://localhost:${port}/api/hooks/ingest`, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({
73
+ knowledge: [knowledge],
74
+ sessionId: input.session_id,
75
+ }),
76
+ signal: AbortSignal.timeout(3000),
77
+ })
78
+
79
+ if (!res.ok) {
80
+ // Server returned error — queue for later
81
+ appendToQueue([knowledge])
82
+ }
83
+ } catch {
84
+ // Server unreachable — append to offline queue
85
+ appendToQueue([knowledge])
86
+ }
87
+
88
+ process.exit(0)
89
+ }
90
+
91
+ /** Read all of stdin as a string */
92
+ function readStdin(): Promise<string> {
93
+ return new Promise((resolve, reject) => {
94
+ const chunks: Buffer[] = []
95
+ const stdin = process.stdin
96
+
97
+ stdin.on('data', (chunk: Buffer) => chunks.push(chunk))
98
+ stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
99
+ stdin.on('error', reject)
100
+
101
+ // Timeout after 2 seconds
102
+ setTimeout(() => {
103
+ stdin.destroy()
104
+ resolve(Buffer.concat(chunks).toString('utf-8'))
105
+ }, 2000)
106
+ })
107
+ }
108
+
109
+ // Execute — all errors caught silently
110
+ main().catch(() => process.exit(0))