claude-brain 0.9.3 → 0.13.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 (66) hide show
  1. package/VERSION +1 -1
  2. package/assets/CLAUDE.md +9 -1
  3. package/package.json +1 -1
  4. package/src/automation/phase12-manager.ts +456 -0
  5. package/src/automation/project-detector.ts +13 -0
  6. package/src/automation/repo-scanner.ts +205 -0
  7. package/src/cli/bin.ts +30 -0
  8. package/src/cli/commands/git-hook.ts +189 -0
  9. package/src/cli/commands/hooks.ts +8 -9
  10. package/src/cli/commands/init.ts +98 -0
  11. package/src/cli/commands/serve.ts +7 -20
  12. package/src/cli/commands/update.ts +3 -3
  13. package/src/config/defaults.ts +4 -1
  14. package/src/config/schema.ts +27 -7
  15. package/src/hooks/brain-hook.ts +8 -6
  16. package/src/hooks/capture.ts +9 -2
  17. package/src/hooks/git-capture.ts +94 -0
  18. package/src/hooks/git-hook-installer.ts +197 -0
  19. package/src/hooks/index.ts +1 -0
  20. package/src/hooks/session-tracker.ts +79 -3
  21. package/src/hooks/types.ts +1 -1
  22. package/src/intelligence/index.ts +24 -0
  23. package/src/knowledge/graph/builder.ts +26 -0
  24. package/src/memory/chroma/store.ts +18 -2
  25. package/src/memory/episodic/manager.ts +17 -0
  26. package/src/memory/index.ts +48 -18
  27. package/src/phase12/index.ts +3 -454
  28. package/src/routing/intent-classifier.ts +107 -9
  29. package/src/routing/response-filter.ts +50 -17
  30. package/src/routing/router.ts +472 -224
  31. package/src/routing/search-engine.ts +464 -0
  32. package/src/routing/types.ts +84 -0
  33. package/src/server/handlers/call-tool.ts +4 -49
  34. package/src/server/handlers/tools/analyze-decision-evolution.ts +1 -1
  35. package/src/server/handlers/tools/detect-trends.ts +1 -1
  36. package/src/server/handlers/tools/find-cross-project-patterns.ts +1 -1
  37. package/src/server/handlers/tools/get-decision-timeline.ts +2 -2
  38. package/src/server/handlers/tools/get-recommendations.ts +1 -1
  39. package/src/server/handlers/tools/index.ts +5 -7
  40. package/src/server/handlers/tools/what-if-analysis.ts +1 -1
  41. package/src/server/providers/resources.ts +195 -0
  42. package/src/server/services.ts +81 -6
  43. package/src/tools/schemas.ts +7 -329
  44. package/src/utils/phase12-helper.ts +2 -2
  45. package/src/utils/timing.ts +47 -0
  46. package/src/vault/writer.ts +22 -2
  47. /package/src/{cross-project → intelligence/cross-project}/affinity.ts +0 -0
  48. /package/src/{cross-project → intelligence/cross-project}/generalizer.ts +0 -0
  49. /package/src/{cross-project → intelligence/cross-project}/index.ts +0 -0
  50. /package/src/{cross-project → intelligence/cross-project}/transfer.ts +0 -0
  51. /package/src/{optimization → intelligence/optimization}/index.ts +0 -0
  52. /package/src/{optimization → intelligence/optimization}/precompute.ts +0 -0
  53. /package/src/{optimization → intelligence/optimization}/semantic-cache.ts +0 -0
  54. /package/src/{prediction → intelligence/prediction}/context-anticipator.ts +0 -0
  55. /package/src/{prediction → intelligence/prediction}/decision-predictor.ts +0 -0
  56. /package/src/{prediction → intelligence/prediction}/index.ts +0 -0
  57. /package/src/{prediction → intelligence/prediction}/recommender.ts +0 -0
  58. /package/src/{reasoning → intelligence/reasoning}/chain-retrieval.ts +0 -0
  59. /package/src/{reasoning → intelligence/reasoning}/counterfactual.ts +0 -0
  60. /package/src/{reasoning → intelligence/reasoning}/index.ts +0 -0
  61. /package/src/{reasoning → intelligence/reasoning}/synthesizer.ts +0 -0
  62. /package/src/{temporal → intelligence/temporal}/evolution.ts +0 -0
  63. /package/src/{temporal → intelligence/temporal}/index.ts +0 -0
  64. /package/src/{temporal → intelligence/temporal}/query-processor.ts +0 -0
  65. /package/src/{temporal → intelligence/temporal}/timeline.ts +0 -0
  66. /package/src/{temporal → intelligence/temporal}/trends.ts +0 -0
package/src/cli/bin.ts CHANGED
@@ -34,6 +34,9 @@ function printHelp() {
34
34
  ['update', 'Update package and refresh CLAUDE.md'],
35
35
  ['chroma', 'Manage ChromaDB server (start/stop/status)'],
36
36
  ['hooks', 'Manage passive learning hooks (install/uninstall/status)'],
37
+ ['git-hook', 'Manage git post-commit hook (install/uninstall/status)'],
38
+ ['init', 'Scan a repo and store initial project context'],
39
+ ['capture', 'Capture a hook event (used by Claude Code hooks)'],
37
40
  ['pack', 'Manage knowledge packs (list/status/reload)'],
38
41
  ['health', 'Run health checks'],
39
42
  ['diagnose', 'Run diagnostics'],
@@ -124,6 +127,33 @@ async function main() {
124
127
  break
125
128
  }
126
129
 
130
+ case 'init': {
131
+ const { runInit } = await import('./commands/init')
132
+ await runInit()
133
+ break
134
+ }
135
+
136
+ case 'capture': {
137
+ // Phase 20: Direct capture command for Claude Code hooks
138
+ // Usage: claude-brain capture --event=tool_use < /dev/stdin
139
+ // This is the lightweight path called by the hook script
140
+ const { main: runCapture } = await import('../hooks/brain-hook')
141
+ await runCapture()
142
+ break
143
+ }
144
+
145
+ case 'git-hook': {
146
+ const { runGitHook } = await import('./commands/git-hook')
147
+ await runGitHook()
148
+ break
149
+ }
150
+
151
+ case 'git-capture': {
152
+ const { handleGitCapture } = await import('../hooks/git-capture')
153
+ await handleGitCapture()
154
+ break
155
+ }
156
+
127
157
  case 'pack': {
128
158
  const { runPack } = await import('./commands/pack')
129
159
  await runPack()
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Phase 21: CLI Git Hook Command
3
+ * Manages git post-commit hook installation (install/uninstall/status)
4
+ */
5
+
6
+ import {
7
+ renderLogo, theme, heading, successText, warningText, errorText, dimText,
8
+ box, summaryPanel, withSpinner,
9
+ } from '@/cli/ui/index.js'
10
+ import {
11
+ installGitHook, uninstallGitHook, isGitHookInstalled, installGitHookAll
12
+ } from '@/hooks/git-hook-installer'
13
+
14
+ export async function runGitHook() {
15
+ const subcommand = process.argv[3] || 'status'
16
+
17
+ switch (subcommand) {
18
+ case 'install':
19
+ await handleInstall()
20
+ break
21
+ case 'uninstall':
22
+ await handleUninstall()
23
+ break
24
+ case 'status':
25
+ handleStatus()
26
+ break
27
+ default:
28
+ console.log()
29
+ console.log(errorText(`Unknown git-hook subcommand: ${subcommand}`))
30
+ printHelp()
31
+ process.exit(1)
32
+ }
33
+ }
34
+
35
+ async function handleInstall() {
36
+ console.log()
37
+ console.log(renderLogo())
38
+ console.log()
39
+ console.log(heading('Install Git Hook'))
40
+ console.log()
41
+
42
+ // Check for --all <dir> flag
43
+ const allIdx = process.argv.indexOf('--all')
44
+ if (allIdx >= 0) {
45
+ const dir = process.argv[allIdx + 1]
46
+ if (!dir) {
47
+ console.log(errorText('--all requires a directory path'))
48
+ process.exit(1)
49
+ }
50
+
51
+ let results: { installed: number; skipped: number; errors: number } | undefined
52
+ await withSpinner(`Installing git hooks in all repos under ${dir}`, async () => {
53
+ results = await installGitHookAll(dir)
54
+ })
55
+
56
+ console.log()
57
+ if (results) {
58
+ console.log(box([
59
+ successText(`Installed: ${results.installed}`),
60
+ dimText(`Skipped (already installed): ${results.skipped}`),
61
+ results.errors > 0 ? warningText(`Errors: ${results.errors}`) : '',
62
+ ].filter(Boolean).join('\n'), 'Batch Install'))
63
+ }
64
+ console.log()
65
+ return
66
+ }
67
+
68
+ // Single repo install (current directory or specified path)
69
+ const repoPath = process.argv[4] || undefined
70
+
71
+ try {
72
+ let result: { installed: boolean; message: string; hookPath: string } | undefined
73
+ await withSpinner('Installing post-commit hook', async () => {
74
+ result = installGitHook(repoPath)
75
+ })
76
+
77
+ console.log()
78
+ if (result?.installed) {
79
+ console.log(box([
80
+ successText(result.message),
81
+ '',
82
+ dimText('The hook fires after every git commit.'),
83
+ dimText('Commit data (branch, message, files) is sent to brain.'),
84
+ dimText('The hook runs in the background — no speed impact.'),
85
+ '',
86
+ result.hookPath ? `${theme.dim('Hook:')} ${result.hookPath}` : '',
87
+ ].filter(Boolean).join('\n'), 'Git Hook Installed'))
88
+ } else if (result) {
89
+ console.log(box(warningText(result.message), 'Git Hook'))
90
+ }
91
+ } catch (err) {
92
+ console.log()
93
+ console.log(box(
94
+ errorText(`Failed: ${err instanceof Error ? err.message : String(err)}`),
95
+ 'Error'
96
+ ))
97
+ process.exit(1)
98
+ }
99
+ console.log()
100
+ }
101
+
102
+ async function handleUninstall() {
103
+ console.log()
104
+ console.log(renderLogo())
105
+ console.log()
106
+ console.log(heading('Uninstall Git Hook'))
107
+ console.log()
108
+
109
+ const repoPath = process.argv[4] || undefined
110
+
111
+ try {
112
+ let result: { uninstalled: boolean; message: string } | undefined
113
+ await withSpinner('Removing post-commit hook', async () => {
114
+ result = uninstallGitHook(repoPath)
115
+ })
116
+
117
+ console.log()
118
+ if (result?.uninstalled) {
119
+ console.log(box(successText(result.message), 'Git Hook Removed'))
120
+ }
121
+ } catch (err) {
122
+ console.log()
123
+ console.log(box(
124
+ errorText(`Failed: ${err instanceof Error ? err.message : String(err)}`),
125
+ 'Error'
126
+ ))
127
+ process.exit(1)
128
+ }
129
+ console.log()
130
+ }
131
+
132
+ function handleStatus() {
133
+ console.log()
134
+ console.log(renderLogo())
135
+ console.log()
136
+ console.log(heading('Git Hook Status'))
137
+ console.log()
138
+
139
+ const repoPath = process.argv[4] || undefined
140
+ const installed = isGitHookInstalled(repoPath)
141
+
142
+ const items = [
143
+ {
144
+ label: 'Installed',
145
+ value: installed ? 'Yes' : 'No',
146
+ status: installed ? 'success' as const : 'warning' as const
147
+ },
148
+ {
149
+ label: 'Repo',
150
+ value: repoPath || process.cwd(),
151
+ status: 'info' as const
152
+ },
153
+ {
154
+ label: 'Hook type',
155
+ value: 'post-commit',
156
+ status: 'info' as const
157
+ },
158
+ ]
159
+
160
+ console.log(summaryPanel('Git Hook', items))
161
+ console.log()
162
+ }
163
+
164
+ function printHelp() {
165
+ console.log()
166
+ const commands = [
167
+ ['install', 'Install post-commit hook in current (or specified) repo'],
168
+ ['install --all <dir>', 'Install in all git repos under a directory'],
169
+ ['uninstall', 'Remove post-commit hook from current (or specified) repo'],
170
+ ['status', 'Check if hook is installed'],
171
+ ]
172
+
173
+ const cmdLines = commands
174
+ .map(([cmd, desc]) => ` ${theme.primary(cmd!.padEnd(22))} ${dimText(desc!)}`)
175
+ .join('\n')
176
+
177
+ console.log(box([
178
+ theme.bold('Usage:') + ' ' + dimText('claude-brain git-hook [subcommand]'),
179
+ '',
180
+ theme.bold('Subcommands:'),
181
+ cmdLines,
182
+ '',
183
+ theme.bold('Examples:'),
184
+ ` ${dimText('claude-brain git-hook install')} ${dimText('Install in current repo')}`,
185
+ ` ${dimText('claude-brain git-hook install --all ~/dev')} ${dimText('Install in all repos under ~/dev')}`,
186
+ ` ${dimText('claude-brain git-hook uninstall')} ${dimText('Remove from current repo')}`,
187
+ ].join('\n'), 'Git Hook Help'))
188
+ console.log()
189
+ }
@@ -57,15 +57,13 @@ async function handleInstall() {
57
57
  '',
58
58
  dimText('Hooks will fire on every tool call in Claude Code.'),
59
59
  dimText('Knowledge is captured and sent to the brain server.'),
60
+ dimText('Session summaries are auto-stored when conversations end.'),
60
61
  '',
61
62
  `${theme.dim('Hook script:')} ${getHookScriptPath()}`,
62
63
  `${theme.dim('Settings:')} ~/.claude/settings.json`,
63
64
  '',
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')}`,
65
+ dimText('Hooks are enabled by default. To disable:'),
66
+ ` ${theme.bold('export CLAUDE_BRAIN_HOOKS_ENABLED=false')}`,
69
67
  ].join('\n'), 'Hooks Installed'))
70
68
  }
71
69
  } catch (err) {
@@ -171,12 +169,13 @@ async function handleToggle(enable: boolean) {
171
169
  // Toggle via env hint (actual config toggle would need config file editing)
172
170
  if (enable) {
173
171
  console.log(box([
174
- successText('To enable hooks, set the environment variable:'),
172
+ successText('Hooks are enabled by default since v0.10.0.'),
175
173
  '',
176
- ` ${theme.bold('export CLAUDE_BRAIN_HOOKS_ENABLED=true')}`,
174
+ dimText('If previously disabled, remove the env variable:'),
175
+ ` ${theme.bold('unset CLAUDE_BRAIN_HOOKS_ENABLED')}`,
177
176
  '',
178
- dimText('Or add to your shell profile (.bashrc, .zshrc):'),
179
- ` ${dimText('echo \'export CLAUDE_BRAIN_HOOKS_ENABLED=true\' >> ~/.zshrc')}`,
177
+ dimText('Hooks capture Edit/Write/Bash events automatically.'),
178
+ dimText('Session summaries are stored when conversations end.'),
180
179
  ].join('\n'), 'Enable Hooks'))
181
180
  } else {
182
181
  console.log(box([
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Init Command
3
+ * Phase 22: Scans a repository and stores initial project context.
4
+ *
5
+ * Usage: claude-brain init [path] [--force]
6
+ */
7
+
8
+ import path from 'path'
9
+ import {
10
+ renderLogo, theme, heading, successText, warningText, dimText,
11
+ box, withSpinner,
12
+ } from '@/cli/ui/index.js'
13
+ import { scanRepo } from '@/automation/repo-scanner'
14
+
15
+ export async function runInit() {
16
+ console.log()
17
+ console.log(renderLogo())
18
+ console.log()
19
+ console.log(heading('Initialize Project'))
20
+ console.log()
21
+
22
+ // Parse arguments
23
+ const args = process.argv.slice(3)
24
+ const force = args.includes('--force')
25
+ const repoPath = path.resolve(
26
+ args.find(a => !a.startsWith('--')) || process.cwd()
27
+ )
28
+
29
+ console.log(` ${theme.bold('Path:')} ${dimText(repoPath)}`)
30
+ if (force) {
31
+ console.log(` ${theme.bold('Mode:')} ${dimText('Force re-scan')}`)
32
+ }
33
+ console.log()
34
+
35
+ // Scan the repo
36
+ const context = await withSpinner('Scanning repository', () => scanRepo(repoPath))
37
+
38
+ if (!context.name) {
39
+ console.log(warningText(' Could not detect project name. Specify a path to a project root.'))
40
+ return
41
+ }
42
+
43
+ // Display results
44
+ console.log(successText(` Project: ${context.name}`))
45
+
46
+ if (context.techStack.length > 0) {
47
+ console.log(` ${theme.bold('Tech stack:')} ${dimText(context.techStack.join(', '))}`)
48
+ }
49
+
50
+ if (context.description) {
51
+ const desc = context.description.split('\n')[0]?.slice(0, 100) || ''
52
+ console.log(` ${theme.bold('Description:')} ${dimText(desc)}`)
53
+ }
54
+
55
+ if (context.recentActivity) {
56
+ const commits = context.recentActivity.split('\n').length
57
+ console.log(` ${theme.bold('Recent commits:')} ${dimText(String(commits))}`)
58
+ }
59
+
60
+ if (context.projectInstructions) {
61
+ console.log(` ${theme.bold('CLAUDE.md:')} ${dimText('found')}`)
62
+ }
63
+
64
+ // Try to store context via brain
65
+ try {
66
+ const summary = [
67
+ `Project "${context.name}" initialized.`,
68
+ context.techStack.length > 0 ? `Tech stack: ${context.techStack.join(', ')}.` : '',
69
+ context.description ? `Description: ${context.description.split('\n')[0]?.slice(0, 200)}` : '',
70
+ context.structure ? `Structure:\n${context.structure.split('\n').slice(0, 15).join('\n')}` : '',
71
+ context.recentActivity ? `Recent git activity:\n${context.recentActivity}` : '',
72
+ context.projectInstructions ? `CLAUDE.md contents:\n${context.projectInstructions.slice(0, 500)}` : ''
73
+ ].filter(Boolean).join('\n\n')
74
+
75
+ // Write to a local init file so the MCP server can pick it up
76
+ const { existsSync, mkdirSync } = await import('fs')
77
+ const { writeFileSync } = await import('fs')
78
+ const dataDir = path.join(repoPath, '.claude-brain')
79
+ if (!existsSync(dataDir)) {
80
+ mkdirSync(dataDir, { recursive: true })
81
+ }
82
+ writeFileSync(path.join(dataDir, 'init-context.md'), summary)
83
+
84
+ console.log()
85
+ console.log(successText(` Initial context saved to .claude-brain/init-context.md`))
86
+ } catch (error) {
87
+ console.log(warningText(` Could not save context: ${error instanceof Error ? error.message : 'unknown error'}`))
88
+ }
89
+
90
+ console.log()
91
+ console.log(box([
92
+ heading('Project initialized!'),
93
+ '',
94
+ dimText('Brain will auto-load this context in future sessions.'),
95
+ dimText('Use brain tool to store additional decisions and patterns.'),
96
+ ].join('\n'), 'Done'))
97
+ console.log()
98
+ }
@@ -110,26 +110,13 @@ 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')
113
+ // Phase 21: Use session tracker from services (promoted from local creation)
114
+ {
115
+ const { getSessionTracker } = await import('@/server/services')
116
+ const sessionTracker = getSessionTracker()
117
+ if (sessionTracker) {
118
+ httpServer.setSessionTracker(sessionTracker)
119
+ mainLogger.info('Session tracker wired to HTTP server')
133
120
  }
134
121
  }
135
122
 
@@ -77,11 +77,11 @@ export async function runUpdate() {
77
77
  if (!existsSync(sourcePath)) {
78
78
  console.log(warningText(' CLAUDE.md asset not found in package, skipping'))
79
79
  } else if (!existsSync(destPath)) {
80
- await withSpinner('Installing CLAUDE.md', async () => {
80
+ await withSpinner('Installing CLAUDE.md (brain works automatically)', async () => {
81
81
  await fs.mkdir(claudeDir, { recursive: true })
82
82
  await fs.copyFile(sourcePath, destPath)
83
83
  })
84
- console.log(successText(' CLAUDE.md installed to ~/.claude/CLAUDE.md'))
84
+ console.log(successText(' CLAUDE.md installed to ~/.claude/CLAUDE.md (brain is now automatic)'))
85
85
  } else {
86
86
  const sourceContent = readFileSync(sourcePath, 'utf-8')
87
87
  const destContent = readFileSync(destPath, 'utf-8')
@@ -92,7 +92,7 @@ export async function runUpdate() {
92
92
  await withSpinner('Updating CLAUDE.md', async () => {
93
93
  await fs.copyFile(sourcePath, destPath)
94
94
  })
95
- console.log(successText(' CLAUDE.md updated to ~/.claude/CLAUDE.md'))
95
+ console.log(successText(' CLAUDE.md updated to ~/.claude/CLAUDE.md (brain is now automatic)'))
96
96
  }
97
97
  }
98
98
 
@@ -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.9.3',
6
+ serverVersion: '0.13.0',
7
7
  logLevel: 'info',
8
8
  logFilePath: './logs/claude-brain.log',
9
9
  dbPath: './data/memory.db',
@@ -43,5 +43,8 @@ export const defaultConfig: PartialConfig = {
43
43
  adaptiveThresholds: true,
44
44
  minFeedbackForAdaptation: 10
45
45
  }
46
+ },
47
+ intelligence: {
48
+ enabled: false
46
49
  }
47
50
  }
@@ -131,10 +131,10 @@ 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 */
134
+ /** Phase 17+20: Passive Learning via Hooks configuration */
135
135
  export const HooksConfigSchema = z.object({
136
- /** Master switch for hooks passive learning */
137
- enabled: z.boolean().default(false),
136
+ /** Master switch for hooks passive learning (Phase 20: enabled by default) */
137
+ enabled: z.boolean().default(true),
138
138
 
139
139
  /** What to capture from tool calls */
140
140
  capture: z.object({
@@ -150,8 +150,22 @@ export const HooksConfigSchema = z.object({
150
150
 
151
151
  /** Privacy filters */
152
152
  privacy: z.object({
153
- /** File paths to ignore (glob patterns) */
154
- ignorePaths: z.array(z.string()).default([]),
153
+ /** File paths to ignore (glob patterns) — Phase 20: sensible defaults */
154
+ ignorePaths: z.array(z.string()).default([
155
+ 'node_modules/**',
156
+ '.env*',
157
+ '*.key',
158
+ '*.pem',
159
+ '*.p12',
160
+ '*.pfx',
161
+ '.git/**',
162
+ 'dist/**',
163
+ 'build/**',
164
+ '*.secret',
165
+ 'credentials*',
166
+ '.aws/**',
167
+ '.ssh/**'
168
+ ]),
155
169
  /** Projects to ignore */
156
170
  ignoreProjects: z.array(z.string()).default([]),
157
171
  /** Minimum confidence to store captured knowledge */
@@ -270,7 +284,7 @@ export const ConfigSchema = z.object({
270
284
  serverName: z.string().default('claude-brain'),
271
285
 
272
286
  /** Server version in semver format */
273
- serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.9.3'),
287
+ serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.13.0'),
274
288
 
275
289
  /** Logging level */
276
290
  logLevel: LogLevelSchema.default('info'),
@@ -331,7 +345,13 @@ export const ConfigSchema = z.object({
331
345
  hooks: HooksConfigSchema.default({}),
332
346
 
333
347
  /** Phase 18: Knowledge packs configuration */
334
- packs: PacksConfigSchema.default({})
348
+ packs: PacksConfigSchema.default({}),
349
+
350
+ /** Phase 22: Intelligence features (opt-in) */
351
+ intelligence: z.object({
352
+ /** Enable consolidated intelligence features */
353
+ enabled: z.boolean().default(false)
354
+ }).default({})
335
355
  })
336
356
 
337
357
  export type Config = z.infer<typeof ConfigSchema>
@@ -14,7 +14,7 @@ import { BrainCapture } from './capture'
14
14
  import { appendToQueue } from './queue'
15
15
  import type { HookInput } from './types'
16
16
 
17
- async function main(): Promise<void> {
17
+ export async function main(): Promise<void> {
18
18
  // Parse --event arg
19
19
  const eventIdx = process.argv.indexOf('--event')
20
20
  const eventName = eventIdx >= 0 ? process.argv[eventIdx + 1] : undefined
@@ -46,9 +46,8 @@ async function main(): Promise<void> {
46
46
  input.hook_event_name = eventName as HookInput['hook_event_name']
47
47
  }
48
48
 
49
- // Load minimal config from env vars
50
- const enabled = process.env.CLAUDE_BRAIN_HOOKS_ENABLED !== 'false'
51
- if (!enabled) {
49
+ // Phase 20: Hooks enabled by default — only disabled by explicit env var
50
+ if (process.env.CLAUDE_BRAIN_HOOKS_ENABLED === 'false') {
52
51
  process.exit(0)
53
52
  return
54
53
  }
@@ -106,5 +105,8 @@ function readStdin(): Promise<string> {
106
105
  })
107
106
  }
108
107
 
109
- // Execute all errors caught silently
110
- main().catch(() => process.exit(0))
108
+ // Execute when run directly (not when imported by CLI)
109
+ const isDirectRun = process.argv[1]?.includes('brain-hook')
110
+ if (isDirectRun) {
111
+ main().catch(() => process.exit(0))
112
+ }
@@ -35,13 +35,20 @@ const TECH_ALIASES: Record<string, string> = {
35
35
  'gql': 'graphql', 'golang': 'go',
36
36
  }
37
37
 
38
+ /** Phase 20: Default privacy ignore paths for sensitive files */
39
+ const DEFAULT_IGNORE_PATHS = [
40
+ 'node_modules/**', '.env*', '*.key', '*.pem', '*.p12', '*.pfx',
41
+ '.git/**', 'dist/**', 'build/**', '*.secret', 'credentials*',
42
+ '.aws/**', '.ssh/**'
43
+ ]
44
+
38
45
  export class BrainCapture {
39
46
  private classifier: PassiveClassifier
40
47
  private config: HooksConfig
41
48
 
42
49
  constructor(config?: Partial<HooksConfig>) {
43
50
  this.config = {
44
- enabled: config?.enabled ?? false,
51
+ enabled: config?.enabled ?? true,
45
52
  capture: {
46
53
  toolUse: config?.capture?.toolUse ?? true,
47
54
  fileEdits: config?.capture?.fileEdits ?? true,
@@ -49,7 +56,7 @@ export class BrainCapture {
49
56
  userMessages: config?.capture?.userMessages ?? true,
50
57
  },
51
58
  privacy: {
52
- ignorePaths: config?.privacy?.ignorePaths ?? [],
59
+ ignorePaths: config?.privacy?.ignorePaths ?? DEFAULT_IGNORE_PATHS,
53
60
  ignoreProjects: config?.privacy?.ignoreProjects ?? [],
54
61
  minConfidence: config?.privacy?.minConfidence ?? 0.7,
55
62
  },
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Phase 21: Git Commit Capture
3
+ * Receives commit data from the post-commit hook and sends it to brain.
4
+ * Called as: claude-brain git-capture <project> <branch> <message> <files> <port>
5
+ */
6
+
7
+ import { appendToQueue } from './queue'
8
+ import type { CapturedKnowledge } from './types'
9
+
10
+ export async function handleGitCapture(): Promise<void> {
11
+ // Parse positional args: project, branch, message, files, port
12
+ const [, , , project, branch, message, filesStr, portStr] = process.argv
13
+
14
+ if (!project || !message) {
15
+ process.exit(0)
16
+ return
17
+ }
18
+
19
+ const port = parseInt(portStr || process.env.CLAUDE_BRAIN_PORT || '3000', 10)
20
+ const files = filesStr ? filesStr.split(',').filter(Boolean) : []
21
+
22
+ // Build captured knowledge
23
+ const knowledge: CapturedKnowledge = {
24
+ type: 'progress',
25
+ confidence: 0.9,
26
+ content: `Git commit on ${branch || 'unknown'}: ${message.trim()}${files.length > 0 ? ` (files: ${files.join(', ')})` : ''}`,
27
+ project: project || undefined,
28
+ technologies: detectTechnologies(files),
29
+ metadata: {
30
+ source: 'git-hook',
31
+ branch: branch || 'unknown',
32
+ commit_message: message.trim(),
33
+ files,
34
+ file_count: files.length,
35
+ },
36
+ source: 'hook-passive',
37
+ timestamp: new Date().toISOString(),
38
+ }
39
+
40
+ // POST to HTTP API server
41
+ try {
42
+ const res = await fetch(`http://localhost:${port}/api/hooks/ingest`, {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify({
46
+ knowledge: [knowledge],
47
+ sessionId: `git-${project}-${Date.now()}`,
48
+ }),
49
+ signal: AbortSignal.timeout(3000),
50
+ })
51
+
52
+ if (!res.ok) {
53
+ appendToQueue([knowledge])
54
+ }
55
+ } catch {
56
+ // Server unreachable — queue for later
57
+ appendToQueue([knowledge])
58
+ }
59
+
60
+ process.exit(0)
61
+ }
62
+
63
+ /**
64
+ * Detect technologies from file extensions.
65
+ */
66
+ function detectTechnologies(files: string[]): string[] {
67
+ const techMap: Record<string, string> = {
68
+ '.ts': 'typescript',
69
+ '.tsx': 'react',
70
+ '.js': 'javascript',
71
+ '.jsx': 'react',
72
+ '.py': 'python',
73
+ '.rs': 'rust',
74
+ '.go': 'go',
75
+ '.rb': 'ruby',
76
+ '.java': 'java',
77
+ '.swift': 'swift',
78
+ '.kt': 'kotlin',
79
+ '.vue': 'vue',
80
+ '.svelte': 'svelte',
81
+ '.css': 'css',
82
+ '.scss': 'scss',
83
+ '.sql': 'sql',
84
+ }
85
+
86
+ const techs = new Set<string>()
87
+ for (const file of files) {
88
+ const ext = '.' + file.split('.').pop()
89
+ if (techMap[ext]) {
90
+ techs.add(techMap[ext])
91
+ }
92
+ }
93
+ return [...techs]
94
+ }