claude-brain 0.25.0 → 0.25.2

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.25.0
1
+ 0.25.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -165,6 +165,7 @@ logLevel: warn
165
165
  const HOOK_FILES = [
166
166
  'brain-hook.ts',
167
167
  'context-hook.ts',
168
+ 'interceptor-hook.ts',
168
169
  'capture.ts',
169
170
  'queue.ts',
170
171
  'types.ts',
@@ -215,7 +216,8 @@ function installHooks() {
215
216
  hasOurHooks(settings.hooks.PostToolUse) &&
216
217
  hasOurHooks(settings.hooks.Stop) &&
217
218
  hasOurHooks(settings.hooks.UserPromptSubmit) &&
218
- hasOurHooks(settings.hooks.SessionStart)) {
219
+ hasOurHooks(settings.hooks.SessionStart) &&
220
+ hasOurHooks(settings.hooks.PreToolUse)) {
219
221
  log('Hooks already installed')
220
222
  return true
221
223
  }
@@ -223,8 +225,10 @@ function installHooks() {
223
225
  // Build hook command
224
226
  const brainScriptPath = join(HOME, 'hooks', 'brain-hook.ts')
225
227
  const contextScriptPath = join(HOME, 'hooks', 'context-hook.ts')
228
+ const interceptorScriptPath = join(HOME, 'hooks', 'interceptor-hook.ts')
229
+ const port = process.env.CLAUDE_BRAIN_PORT || process.env.PORT || '3000'
226
230
  function buildCmd(event, scriptPath) {
227
- return `bun "${scriptPath}" --event ${event} # ${HOOK_MARKER}`
231
+ return `bun "${scriptPath}" --event ${event} --port ${port} # ${HOOK_MARKER}`
228
232
  }
229
233
 
230
234
  if (!settings.hooks) settings.hooks = {}
@@ -265,6 +269,19 @@ function installHooks() {
265
269
  })
266
270
  }
267
271
 
272
+ // PreToolUse — code intelligence interceptor for Glob and Grep
273
+ if (!hasOurHooks(settings.hooks.PreToolUse)) {
274
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = []
275
+ settings.hooks.PreToolUse.push({
276
+ matcher: 'Glob',
277
+ hooks: [{ type: 'command', command: buildCmd('PreToolUse', interceptorScriptPath) }],
278
+ })
279
+ settings.hooks.PreToolUse.push({
280
+ matcher: 'Grep',
281
+ hooks: [{ type: 'command', command: buildCmd('PreToolUse', interceptorScriptPath) }],
282
+ })
283
+ }
284
+
268
285
  // Write atomically
269
286
  if (!existsSync(CLAUDE_DIR)) {
270
287
  mkdirSync(CLAUDE_DIR, { recursive: true })
@@ -83,6 +83,7 @@ export async function runReindex() {
83
83
 
84
84
  const parser = new CodeParser()
85
85
  await parser.initialize()
86
+ console.log(dimText(' Parser initialized'))
86
87
 
87
88
  const indexer = new CodeIndexer(codeDb, parser, logger)
88
89
  await indexer.initialize()
@@ -94,7 +95,19 @@ export async function runReindex() {
94
95
  }
95
96
 
96
97
  const result = await indexer.indexProject(projectPath, projectName)
97
- console.log(successText(` Reindex complete: ${result.filesIndexed ?? 0} files, ${result.symbolsFound ?? 0} symbols`))
98
+ const files = result.filesIndexed ?? 0
99
+ const symbols = result.symbolsFound ?? 0
100
+ const skipped = result.filesSkipped ?? 0
101
+
102
+ if (files === 0 && skipped === 0) {
103
+ console.log(warningText(` No supported files found in project.`))
104
+ console.log(dimText(` Supported: .ts .tsx .js .jsx .mjs .cjs .py .go .rs .vue .html .css .json .yaml .yml`))
105
+ if (result.errors && result.errors.length > 0) {
106
+ console.log(warningText(` Errors: ${result.errors.slice(0, 3).join('; ')}`))
107
+ }
108
+ } else {
109
+ console.log(successText(` Reindex complete: ${files} files, ${symbols} symbols` + (skipped > 0 ? ` (${skipped} unchanged)` : '')))
110
+ }
98
111
  codeDb.close()
99
112
  } catch (error) {
100
113
  console.log(box(
@@ -190,6 +190,10 @@ export class CodeIndexer {
190
190
  private async collectFiles(dirPath: string): Promise<string[]> {
191
191
  const files: string[] = []
192
192
  await this.walkDirectory(dirPath, files)
193
+ if (files.length === 0) {
194
+ this.logger.warn({ dirPath }, 'collectFiles returned 0 files — trying fallback without withFileTypes')
195
+ await this.walkDirectoryFallback(dirPath, files)
196
+ }
193
197
  return files
194
198
  }
195
199
 
@@ -197,8 +201,9 @@ export class CodeIndexer {
197
201
  let entries
198
202
  try {
199
203
  entries = await readdir(dirPath, { withFileTypes: true })
200
- } catch {
201
- return // Skip unreadable directories
204
+ } catch (error) {
205
+ this.logger.debug({ dirPath, error: error instanceof Error ? error.message : String(error) }, 'Cannot read directory')
206
+ return
202
207
  }
203
208
 
204
209
  for (const entry of entries) {
@@ -215,6 +220,38 @@ export class CodeIndexer {
215
220
  }
216
221
  }
217
222
 
223
+ /** Fallback walker that uses readdir (names only) + stat — works on all platforms */
224
+ private async walkDirectoryFallback(dirPath: string, files: string[]): Promise<void> {
225
+ let names: string[]
226
+ try {
227
+ names = await readdir(dirPath)
228
+ } catch (error) {
229
+ this.logger.debug({ dirPath, error: error instanceof Error ? error.message : String(error) }, 'Fallback: cannot read directory')
230
+ return
231
+ }
232
+
233
+ for (const name of names) {
234
+ const fullPath = join(dirPath, name)
235
+ let fileStat
236
+ try {
237
+ fileStat = await stat(fullPath)
238
+ } catch {
239
+ continue
240
+ }
241
+
242
+ if (fileStat.isDirectory()) {
243
+ if (!SKIP_DIRS.has(name)) {
244
+ await this.walkDirectoryFallback(fullPath, files)
245
+ }
246
+ } else if (fileStat.isFile()) {
247
+ if (SKIP_FILES.has(name)) continue
248
+ const ext = extname(name).toLowerCase()
249
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue
250
+ files.push(fullPath)
251
+ }
252
+ }
253
+ }
254
+
218
255
  private async indexSingleFile(
219
256
  filePath: string,
220
257
  projectPath: string,
@@ -109,7 +109,7 @@ async function main(): Promise<void> {
109
109
  }).catch(() => null)
110
110
 
111
111
  // Phase 29: Detect file paths in the user's prompt and fetch linked memories
112
- const projectName = input.cwd ? input.cwd.split('/').filter(Boolean).pop() : ''
112
+ const projectName = input.cwd ? input.cwd.split(/[/\\]/).filter(Boolean).pop() : ''
113
113
  const filePaths = extractFilePathsFromPrompt(input.prompt)
114
114
  const fileMemoryPromises = filePaths.slice(0, 3).map(fp =>
115
115
  fetch(`${baseUrl}/api/memory/for-file?file=${encodeURIComponent(fp)}&project=${encodeURIComponent(projectName || '')}`, {
@@ -135,7 +135,7 @@ async function main(): Promise<void> {
135
135
  if (input.cwd) params.set('cwd', input.cwd)
136
136
 
137
137
  // Extract project name from cwd for code map
138
- const projectName = input.cwd ? input.cwd.split('/').filter(Boolean).pop() : undefined
138
+ const projectName = input.cwd ? input.cwd.split(/[/\\]/).filter(Boolean).pop() : undefined
139
139
 
140
140
  // Fetch brain context AND code map in parallel
141
141
  const [contextRes, codeMapRes] = await Promise.all([
@@ -70,7 +70,7 @@ function resolveProjectName(raw: string): string {
70
70
  let name = raw.trim()
71
71
  // Strip npm scoped prefix: @scope/name → name
72
72
  if (name.startsWith('@') && name.includes('/')) {
73
- name = name.split('/').pop() || name
73
+ name = name.split(/[/\\]/).pop() || name
74
74
  }
75
75
  return name || raw
76
76
  }
@@ -230,7 +230,10 @@ export function isHooksInstalled(): boolean {
230
230
  const hasUserPromptSubmit = Array.isArray(settings.hooks.UserPromptSubmit) &&
231
231
  settings.hooks.UserPromptSubmit.some((entry: any) => isOurHookEntry(entry))
232
232
 
233
- return hasPostToolUse || hasStop || hasUserPromptSubmit
233
+ const hasPreToolUse = Array.isArray(settings.hooks.PreToolUse) &&
234
+ settings.hooks.PreToolUse.some((entry: any) => isOurHookEntry(entry))
235
+
236
+ return hasPostToolUse || hasStop || hasUserPromptSubmit || hasPreToolUse
234
237
  }
235
238
 
236
239
  /** Check if a hook entry belongs to us (by marker in command) */
@@ -133,7 +133,8 @@ function isComplexRegex(pattern: string): boolean {
133
133
 
134
134
  function detectProject(cwd?: string): string | null {
135
135
  if (!cwd) return null
136
- const segments = cwd.split('/').filter(Boolean)
136
+ // Split by both / and \ for cross-platform support (Windows uses backslashes)
137
+ const segments = cwd.split(/[/\\]/).filter(Boolean)
137
138
  return segments[segments.length - 1] || null
138
139
  }
139
140
 
@@ -305,7 +305,7 @@ export class PassiveClassifier {
305
305
  }
306
306
 
307
307
  // Check for Dockerfile without extension
308
- const basename = filePath.split('/').pop()?.toLowerCase() || ''
308
+ const basename = filePath.split(/[/\\]/).pop()?.toLowerCase() || ''
309
309
  if (basename === 'dockerfile' || basename.startsWith('dockerfile.')) {
310
310
  techs.push('docker')
311
311
  }