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.
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -0
- package/package.json +2 -1
- package/packs/backend/node.json +173 -0
- package/packs/core/javascript.json +176 -0
- package/packs/core/typescript.json +222 -0
- package/packs/frontend/react.json +254 -0
- package/packs/meta/testing.json +172 -0
- package/src/cli/bin.ts +14 -0
- package/src/cli/commands/chroma.ts +53 -17
- package/src/cli/commands/hooks.ts +214 -0
- package/src/cli/commands/pack.ts +197 -0
- package/src/cli/commands/serve.ts +34 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +85 -2
- package/src/hooks/brain-hook.ts +110 -0
- package/src/hooks/capture.ts +161 -0
- package/src/hooks/deduplicator.ts +72 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/installer.ts +181 -0
- package/src/hooks/passive-classifier.ts +366 -0
- package/src/hooks/queue.ts +122 -0
- package/src/hooks/session-tracker.ts +199 -0
- package/src/hooks/types.ts +47 -0
- package/src/memory/chroma/client.ts +1 -1
- package/src/memory/chroma/index.ts +1 -1
- package/src/memory/chroma/store.ts +29 -9
- package/src/memory/index.ts +1 -0
- package/src/memory/store.ts +1 -0
- package/src/packs/index.ts +9 -0
- package/src/packs/loader.ts +134 -0
- package/src/packs/manager.ts +204 -0
- package/src/packs/ranker.ts +78 -0
- package/src/packs/types.ts +81 -0
- package/src/routing/entity-extractor.ts +410 -0
- package/src/routing/intent-classifier.ts +229 -0
- package/src/routing/response-filter.ts +221 -0
- package/src/routing/router.ts +671 -0
- package/src/server/handlers/call-tool.ts +7 -0
- package/src/server/handlers/list-tools.ts +22 -5
- package/src/server/handlers/tools/brain.ts +85 -0
- package/src/server/handlers/tools/init-project.ts +47 -0
- package/src/server/handlers/tools/schemas.ts +12 -0
- package/src/server/http-api.ts +188 -0
- package/src/tools/registry.ts +9 -0
- package/src/tools/schemas.ts +33 -1
|
@@ -0,0 +1,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
|
}
|
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.8.0',
|
|
7
7
|
logLevel: 'info',
|
|
8
8
|
logFilePath: './logs/claude-brain.log',
|
|
9
9
|
dbPath: './data/memory.db',
|
package/src/config/schema.ts
CHANGED
|
@@ -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.
|
|
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))
|