claude-brain 0.16.0 → 0.17.1
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/package.json +1 -1
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +1 -1
- package/src/hooks/context-hook.ts +137 -0
- package/src/hooks/installer.ts +57 -7
- package/src/hooks/types.ts +6 -1
- package/src/server/http-api.ts +68 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.17.1
|
package/package.json
CHANGED
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.17.1',
|
|
7
7
|
logLevel: 'info',
|
|
8
8
|
logFilePath: './logs/claude-brain.log',
|
|
9
9
|
dbPath: './data/memory.db',
|
package/src/config/schema.ts
CHANGED
|
@@ -284,7 +284,7 @@ export const ConfigSchema = z.object({
|
|
|
284
284
|
serverName: z.string().default('claude-brain'),
|
|
285
285
|
|
|
286
286
|
/** Server version in semver format */
|
|
287
|
-
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.17.1'),
|
|
288
288
|
|
|
289
289
|
/** Logging level */
|
|
290
290
|
logLevel: LogLevelSchema.default('info'),
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Phase 26: Context Injection Hook
|
|
4
|
+
* Runs on UserPromptSubmit and SessionStart to inject relevant memories
|
|
5
|
+
* into Claude's context via additionalContext.
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL CONSTRAINTS:
|
|
8
|
+
* - Must complete in <3s (blocks user prompt processing)
|
|
9
|
+
* - No heavy imports — just fetch + JSON parse
|
|
10
|
+
* - All errors silently caught with process.exit(0)
|
|
11
|
+
* - Outputs JSON to stdout: { hookSpecificOutput: { additionalContext: "..." } }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface HookStdin {
|
|
15
|
+
session_id: string
|
|
16
|
+
hook_event_name: string
|
|
17
|
+
prompt?: string
|
|
18
|
+
cwd?: string
|
|
19
|
+
source?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main(): Promise<void> {
|
|
23
|
+
// Parse --event arg
|
|
24
|
+
const eventIdx = process.argv.indexOf('--event')
|
|
25
|
+
const eventName = eventIdx >= 0 ? process.argv[eventIdx + 1] : undefined
|
|
26
|
+
|
|
27
|
+
// Read stdin JSON from Claude Code
|
|
28
|
+
let rawInput: string
|
|
29
|
+
try {
|
|
30
|
+
rawInput = await readStdin()
|
|
31
|
+
} catch {
|
|
32
|
+
process.exit(0)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!rawInput.trim()) {
|
|
37
|
+
process.exit(0)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let input: HookStdin
|
|
42
|
+
try {
|
|
43
|
+
input = JSON.parse(rawInput)
|
|
44
|
+
} catch {
|
|
45
|
+
process.exit(0)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const event = eventName || input.hook_event_name
|
|
50
|
+
|
|
51
|
+
// Skip if hooks explicitly disabled
|
|
52
|
+
if (process.env.CLAUDE_BRAIN_HOOKS_ENABLED === 'false') {
|
|
53
|
+
process.exit(0)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const port = parseInt(process.env.CLAUDE_BRAIN_PORT || '3000', 10)
|
|
58
|
+
const baseUrl = `http://localhost:${port}`
|
|
59
|
+
|
|
60
|
+
let context = ''
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (event === 'UserPromptSubmit' && input.prompt) {
|
|
64
|
+
// Query for memories relevant to the user's prompt
|
|
65
|
+
const params = new URLSearchParams({
|
|
66
|
+
query: input.prompt.slice(0, 500), // Limit query length
|
|
67
|
+
limit: '5',
|
|
68
|
+
})
|
|
69
|
+
if (input.cwd) params.set('cwd', input.cwd)
|
|
70
|
+
|
|
71
|
+
const res = await fetch(`${baseUrl}/api/hooks/context-query?${params}`, {
|
|
72
|
+
signal: AbortSignal.timeout(2000),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if (res.ok) {
|
|
76
|
+
const data = await res.json() as { success: boolean; context: string }
|
|
77
|
+
context = data.context || ''
|
|
78
|
+
}
|
|
79
|
+
} else if (event === 'SessionStart') {
|
|
80
|
+
// Load broader project context on session start
|
|
81
|
+
const params = new URLSearchParams({ type: 'session-start' })
|
|
82
|
+
if (input.cwd) params.set('cwd', input.cwd)
|
|
83
|
+
|
|
84
|
+
const res = await fetch(`${baseUrl}/api/hooks/context-query?${params}`, {
|
|
85
|
+
signal: AbortSignal.timeout(2000),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (res.ok) {
|
|
89
|
+
const data = await res.json() as { success: boolean; context: string }
|
|
90
|
+
context = data.context || ''
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Server unreachable or timeout — exit silently
|
|
95
|
+
process.exit(0)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Only output if we have context to inject
|
|
100
|
+
if (context.trim()) {
|
|
101
|
+
const output = {
|
|
102
|
+
hookSpecificOutput: {
|
|
103
|
+
hookEventName: event,
|
|
104
|
+
additionalContext: `[Brain Memory]\n${context}`,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write(JSON.stringify(output))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
process.exit(0)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Read all of stdin as a string */
|
|
114
|
+
function readStdin(): Promise<string> {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const chunks: Buffer[] = []
|
|
117
|
+
const stdin = process.stdin
|
|
118
|
+
|
|
119
|
+
stdin.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
120
|
+
stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
|
|
121
|
+
stdin.on('error', reject)
|
|
122
|
+
|
|
123
|
+
// Timeout after 2 seconds
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
stdin.destroy()
|
|
126
|
+
resolve(Buffer.concat(chunks).toString('utf-8'))
|
|
127
|
+
}, 2000)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Execute when run directly
|
|
132
|
+
const isDirectRun = process.argv[1]?.includes('context-hook')
|
|
133
|
+
if (isDirectRun) {
|
|
134
|
+
main().catch(() => process.exit(0))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { main }
|
package/src/hooks/installer.ts
CHANGED
|
@@ -12,11 +12,16 @@ import { getClaudeBrainHome } from '@/config/home'
|
|
|
12
12
|
const CLAUDE_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json')
|
|
13
13
|
const HOOK_MARKER = 'claude-brain-hook'
|
|
14
14
|
|
|
15
|
-
/** Get the path where the hook script should be installed */
|
|
15
|
+
/** Get the path where the brain-hook script should be installed */
|
|
16
16
|
export function getHookScriptPath(): string {
|
|
17
17
|
return join(getClaudeBrainHome(), 'hooks', 'brain-hook.ts')
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Get the path where the context-hook script should be installed */
|
|
21
|
+
export function getContextHookScriptPath(): string {
|
|
22
|
+
return join(getClaudeBrainHome(), 'hooks', 'context-hook.ts')
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
/** Read Claude Code settings.json, creating if needed */
|
|
21
26
|
function readSettings(): Record<string, any> {
|
|
22
27
|
if (!existsSync(CLAUDE_SETTINGS_PATH)) return {}
|
|
@@ -39,10 +44,11 @@ function writeSettings(settings: Record<string, any>): void {
|
|
|
39
44
|
renameSync(tmpPath, CLAUDE_SETTINGS_PATH)
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
/** Build the hook command string */
|
|
43
|
-
function buildHookCommand(event: string): string {
|
|
44
|
-
const scriptPath = getHookScriptPath()
|
|
45
|
-
|
|
47
|
+
/** Build the hook command string, embedding the port so hooks work regardless of env */
|
|
48
|
+
function buildHookCommand(event: string, script: 'brain-hook' | 'context-hook' = 'brain-hook'): string {
|
|
49
|
+
const scriptPath = script === 'context-hook' ? getContextHookScriptPath() : getHookScriptPath()
|
|
50
|
+
const port = process.env.PORT || process.env.CLAUDE_BRAIN_PORT || '3000'
|
|
51
|
+
return `CLAUDE_BRAIN_PORT=${port} bun "${scriptPath}" --event ${event} # ${HOOK_MARKER}`
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
/**
|
|
@@ -79,6 +85,26 @@ export function installHooks(): { installed: boolean; message: string } {
|
|
|
79
85
|
}],
|
|
80
86
|
})
|
|
81
87
|
|
|
88
|
+
// UserPromptSubmit hook — injects relevant memories into every prompt
|
|
89
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = []
|
|
90
|
+
settings.hooks.UserPromptSubmit.push({
|
|
91
|
+
matcher: '',
|
|
92
|
+
hooks: [{
|
|
93
|
+
type: 'command',
|
|
94
|
+
command: buildHookCommand('UserPromptSubmit', 'context-hook'),
|
|
95
|
+
}],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// SessionStart hook — injects project context on session start/resume
|
|
99
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = []
|
|
100
|
+
settings.hooks.SessionStart.push({
|
|
101
|
+
matcher: 'startup,resume,compact',
|
|
102
|
+
hooks: [{
|
|
103
|
+
type: 'command',
|
|
104
|
+
command: buildHookCommand('SessionStart', 'context-hook'),
|
|
105
|
+
}],
|
|
106
|
+
})
|
|
107
|
+
|
|
82
108
|
writeSettings(settings)
|
|
83
109
|
|
|
84
110
|
// Copy hook script to install location
|
|
@@ -118,6 +144,26 @@ export function uninstallHooks(): { uninstalled: boolean; message: string } {
|
|
|
118
144
|
}
|
|
119
145
|
}
|
|
120
146
|
|
|
147
|
+
// Remove our entries from UserPromptSubmit
|
|
148
|
+
if (Array.isArray(settings.hooks.UserPromptSubmit)) {
|
|
149
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
|
|
150
|
+
(entry: any) => !isOurHookEntry(entry)
|
|
151
|
+
)
|
|
152
|
+
if (settings.hooks.UserPromptSubmit.length === 0) {
|
|
153
|
+
delete settings.hooks.UserPromptSubmit
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Remove our entries from SessionStart
|
|
158
|
+
if (Array.isArray(settings.hooks.SessionStart)) {
|
|
159
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
160
|
+
(entry: any) => !isOurHookEntry(entry)
|
|
161
|
+
)
|
|
162
|
+
if (settings.hooks.SessionStart.length === 0) {
|
|
163
|
+
delete settings.hooks.SessionStart
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
121
167
|
// Clean up empty hooks object
|
|
122
168
|
if (Object.keys(settings.hooks).length === 0) {
|
|
123
169
|
delete settings.hooks
|
|
@@ -142,7 +188,10 @@ export function isHooksInstalled(): boolean {
|
|
|
142
188
|
const hasStop = Array.isArray(settings.hooks.Stop) &&
|
|
143
189
|
settings.hooks.Stop.some((entry: any) => isOurHookEntry(entry))
|
|
144
190
|
|
|
145
|
-
|
|
191
|
+
const hasUserPromptSubmit = Array.isArray(settings.hooks.UserPromptSubmit) &&
|
|
192
|
+
settings.hooks.UserPromptSubmit.some((entry: any) => isOurHookEntry(entry))
|
|
193
|
+
|
|
194
|
+
return hasPostToolUse || hasStop || hasUserPromptSubmit
|
|
146
195
|
}
|
|
147
196
|
|
|
148
197
|
/** Check if a hook entry belongs to us (by marker in command) */
|
|
@@ -153,9 +202,10 @@ function isOurHookEntry(entry: any): boolean {
|
|
|
153
202
|
)
|
|
154
203
|
}
|
|
155
204
|
|
|
156
|
-
/** All files needed by brain-hook.ts at runtime */
|
|
205
|
+
/** All files needed by brain-hook.ts and context-hook.ts at runtime */
|
|
157
206
|
const HOOK_FILES = [
|
|
158
207
|
'brain-hook.ts',
|
|
208
|
+
'context-hook.ts',
|
|
159
209
|
'capture.ts',
|
|
160
210
|
'queue.ts',
|
|
161
211
|
'types.ts',
|
package/src/hooks/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/** Claude Code hook stdin JSON format */
|
|
6
6
|
export interface HookInput {
|
|
7
7
|
session_id: string
|
|
8
|
-
hook_event_name: 'PostToolUse' | 'Stop' | 'PreToolUse' | 'GitCommit'
|
|
8
|
+
hook_event_name: 'PostToolUse' | 'Stop' | 'PreToolUse' | 'GitCommit' | 'UserPromptSubmit' | 'SessionStart'
|
|
9
9
|
cwd: string
|
|
10
10
|
tool_name?: string
|
|
11
11
|
tool_input?: Record<string, any>
|
|
@@ -13,6 +13,11 @@ export interface HookInput {
|
|
|
13
13
|
content?: string | Array<{ type: string; text?: string }>
|
|
14
14
|
[key: string]: any
|
|
15
15
|
}
|
|
16
|
+
// UserPromptSubmit fields
|
|
17
|
+
prompt?: string
|
|
18
|
+
// SessionStart fields
|
|
19
|
+
source?: string
|
|
20
|
+
model?: string
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/** Knowledge type classifications */
|
package/src/server/http-api.ts
CHANGED
|
@@ -83,6 +83,9 @@ export class HttpApiServer {
|
|
|
83
83
|
this.app.post('/api/hooks/session-end', (c) => this.handleHookSessionEnd(c))
|
|
84
84
|
this.app.get('/api/hooks/status', () => this.handleHookStatus())
|
|
85
85
|
|
|
86
|
+
// Phase 26: Context injection for UserPromptSubmit/SessionStart hooks
|
|
87
|
+
this.app.get('/api/hooks/context-query', (c) => this.handleContextQuery(c))
|
|
88
|
+
|
|
86
89
|
// Phase 23b: Expose brain://context/auto via HTTP for testability
|
|
87
90
|
this.app.get('/api/context/auto', () => this.handleContextAuto())
|
|
88
91
|
}
|
|
@@ -462,6 +465,71 @@ export class HttpApiServer {
|
|
|
462
465
|
}
|
|
463
466
|
}
|
|
464
467
|
|
|
468
|
+
// ─── Phase 26: Context Query for Hook Injection ──────────
|
|
469
|
+
|
|
470
|
+
private async handleContextQuery(c: any): Promise<Response> {
|
|
471
|
+
try {
|
|
472
|
+
const query = c.req.query('query') || ''
|
|
473
|
+
const type = c.req.query('type') || ''
|
|
474
|
+
const cwd = c.req.query('cwd') || ''
|
|
475
|
+
const limit = parseInt(c.req.query('limit') || '5', 10)
|
|
476
|
+
|
|
477
|
+
const memoryService = getMemoryService()
|
|
478
|
+
if (!memoryService || !memoryService.isInitialized()) {
|
|
479
|
+
return Response.json({ success: true, context: '' })
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Extract project name from cwd (last path segment)
|
|
483
|
+
const projectName = cwd ? cwd.split('/').filter(Boolean).pop() : undefined
|
|
484
|
+
|
|
485
|
+
let contextParts: string[] = []
|
|
486
|
+
|
|
487
|
+
if (type === 'session-start') {
|
|
488
|
+
// Broader context for session start: recent decisions + patterns
|
|
489
|
+
const [recallText, patterns] = await Promise.all([
|
|
490
|
+
memoryService.recallSimilar(projectName || 'recent work', {
|
|
491
|
+
project: projectName,
|
|
492
|
+
limit: 5,
|
|
493
|
+
minSimilarity: 0.2,
|
|
494
|
+
}),
|
|
495
|
+
memoryService.getPatterns(projectName, { limit: 5 }),
|
|
496
|
+
])
|
|
497
|
+
|
|
498
|
+
if (recallText && recallText.trim()) {
|
|
499
|
+
contextParts.push(recallText)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (patterns && patterns.length > 0) {
|
|
503
|
+
const patternLines = patterns
|
|
504
|
+
.slice(0, 3)
|
|
505
|
+
.map((p: any) => `- Pattern: ${p.description}`)
|
|
506
|
+
.join('\n')
|
|
507
|
+
if (patternLines) {
|
|
508
|
+
contextParts.push(patternLines)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} else if (query) {
|
|
512
|
+
// Query-specific recall
|
|
513
|
+
const recallText = await memoryService.recallSimilar(query, {
|
|
514
|
+
project: projectName,
|
|
515
|
+
limit,
|
|
516
|
+
minSimilarity: 0.3,
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
if (recallText && recallText.trim()) {
|
|
520
|
+
contextParts.push(recallText)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const context = contextParts.join('\n')
|
|
525
|
+
|
|
526
|
+
return Response.json({ success: true, context })
|
|
527
|
+
} catch (error) {
|
|
528
|
+
this.logger.error({ error }, 'Failed to get context for hook')
|
|
529
|
+
return Response.json({ success: true, context: '' })
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
465
533
|
// ─── Phase 23b: Context Auto Endpoint ────────────────────
|
|
466
534
|
|
|
467
535
|
private async handleContextAuto(): Promise<Response> {
|