claude-brain 0.16.0 → 0.17.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 CHANGED
@@ -1 +1 @@
1
- 0.16.0
1
+ 0.17.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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.16.0',
6
+ serverVersion: '0.17.0',
7
7
  logLevel: 'info',
8
8
  logFilePath: './logs/claude-brain.log',
9
9
  dbPath: './data/memory.db',
@@ -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.16.0'),
287
+ serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.17.0'),
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 }
@@ -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
- return `bun "${scriptPath}" --event ${event} # ${HOOK_MARKER}`
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
- return hasPostToolUse || hasStop
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',
@@ -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 */
@@ -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> {