claude-brain 0.10.0 → 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.
- package/VERSION +1 -1
- package/assets/CLAUDE.md +9 -1
- package/package.json +1 -1
- package/src/automation/phase12-manager.ts +456 -0
- package/src/automation/project-detector.ts +13 -0
- package/src/automation/repo-scanner.ts +205 -0
- package/src/cli/bin.ts +30 -0
- package/src/cli/commands/git-hook.ts +189 -0
- package/src/cli/commands/hooks.ts +8 -9
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/serve.ts +7 -20
- package/src/cli/commands/update.ts +3 -3
- package/src/config/defaults.ts +4 -1
- package/src/config/schema.ts +27 -7
- package/src/hooks/brain-hook.ts +8 -6
- package/src/hooks/capture.ts +9 -2
- package/src/hooks/git-capture.ts +94 -0
- package/src/hooks/git-hook-installer.ts +197 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/session-tracker.ts +79 -3
- package/src/hooks/types.ts +1 -1
- package/src/intelligence/index.ts +24 -0
- package/src/knowledge/graph/builder.ts +26 -0
- package/src/memory/chroma/store.ts +12 -1
- package/src/memory/episodic/manager.ts +17 -0
- package/src/memory/index.ts +25 -6
- package/src/phase12/index.ts +3 -454
- package/src/routing/intent-classifier.ts +13 -3
- package/src/routing/router.ts +38 -14
- package/src/routing/search-engine.ts +26 -17
- package/src/server/handlers/tools/analyze-decision-evolution.ts +1 -1
- package/src/server/handlers/tools/detect-trends.ts +1 -1
- package/src/server/handlers/tools/find-cross-project-patterns.ts +1 -1
- package/src/server/handlers/tools/get-decision-timeline.ts +2 -2
- package/src/server/handlers/tools/get-recommendations.ts +1 -1
- package/src/server/handlers/tools/what-if-analysis.ts +1 -1
- package/src/server/providers/resources.ts +195 -0
- package/src/server/services.ts +53 -6
- package/src/tools/schemas.ts +1 -1
- package/src/utils/phase12-helper.ts +2 -2
- package/src/utils/timing.ts +47 -0
- package/src/vault/writer.ts +22 -2
- /package/src/{cross-project → intelligence/cross-project}/affinity.ts +0 -0
- /package/src/{cross-project → intelligence/cross-project}/generalizer.ts +0 -0
- /package/src/{cross-project → intelligence/cross-project}/index.ts +0 -0
- /package/src/{cross-project → intelligence/cross-project}/transfer.ts +0 -0
- /package/src/{optimization → intelligence/optimization}/index.ts +0 -0
- /package/src/{optimization → intelligence/optimization}/precompute.ts +0 -0
- /package/src/{optimization → intelligence/optimization}/semantic-cache.ts +0 -0
- /package/src/{prediction → intelligence/prediction}/context-anticipator.ts +0 -0
- /package/src/{prediction → intelligence/prediction}/decision-predictor.ts +0 -0
- /package/src/{prediction → intelligence/prediction}/index.ts +0 -0
- /package/src/{prediction → intelligence/prediction}/recommender.ts +0 -0
- /package/src/{reasoning → intelligence/reasoning}/chain-retrieval.ts +0 -0
- /package/src/{reasoning → intelligence/reasoning}/counterfactual.ts +0 -0
- /package/src/{reasoning → intelligence/reasoning}/index.ts +0 -0
- /package/src/{reasoning → intelligence/reasoning}/synthesizer.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/evolution.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/index.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/query-processor.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/timeline.ts +0 -0
- /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('
|
|
65
|
-
` ${theme.bold('CLAUDE_BRAIN_HOOKS_ENABLED=
|
|
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('
|
|
172
|
+
successText('Hooks are enabled by default since v0.10.0.'),
|
|
175
173
|
'',
|
|
176
|
-
|
|
174
|
+
dimText('If previously disabled, remove the env variable:'),
|
|
175
|
+
` ${theme.bold('unset CLAUDE_BRAIN_HOOKS_ENABLED')}`,
|
|
177
176
|
'',
|
|
178
|
-
dimText('
|
|
179
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
package/src/config/defaults.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -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(
|
|
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.
|
|
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>
|
package/src/hooks/brain-hook.ts
CHANGED
|
@@ -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
|
-
//
|
|
50
|
-
|
|
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
|
|
110
|
-
|
|
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
|
+
}
|
package/src/hooks/capture.ts
CHANGED
|
@@ -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 ??
|
|
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
|
+
}
|