claude-brain 0.13.1 → 0.14.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 CHANGED
@@ -1 +1 @@
1
- 0.13.1
1
+ 0.14.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
4
4
  "description": "Local development assistant bridging Obsidian vaults with Claude Code via MCP",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -32,6 +32,30 @@ export async function runInit() {
32
32
  }
33
33
  console.log()
34
34
 
35
+ // Phase 23b: Validate project directory
36
+ const { existsSync: dirExists } = await import('fs')
37
+ const homeDir = process.env.HOME || process.env.USERPROFILE || ''
38
+ if (repoPath === homeDir) {
39
+ console.log(warningText(' This is your home directory — not a project root.'))
40
+ console.log(dimText(' Run this inside a project directory, or pass a path: claude-brain init /path/to/project'))
41
+ console.log()
42
+ return
43
+ }
44
+
45
+ const hasGit = dirExists(path.join(repoPath, '.git'))
46
+ const hasPackageJson = dirExists(path.join(repoPath, 'package.json'))
47
+ const hasCargoToml = dirExists(path.join(repoPath, 'Cargo.toml'))
48
+ const hasPyproject = dirExists(path.join(repoPath, 'pyproject.toml'))
49
+ const hasGoMod = dirExists(path.join(repoPath, 'go.mod'))
50
+ const isProject = hasGit || hasPackageJson || hasCargoToml || hasPyproject || hasGoMod
51
+
52
+ if (!isProject && !force) {
53
+ console.log(warningText(' No project markers found (no .git, package.json, Cargo.toml, etc.)'))
54
+ console.log(dimText(' Use --force to scan anyway, or run inside a project directory.'))
55
+ console.log()
56
+ return
57
+ }
58
+
35
59
  // Scan the repo
36
60
  const context = await withSpinner('Scanning repository', () => scanRepo(repoPath))
37
61
 
@@ -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.13.1',
6
+ serverVersion: '0.14.1',
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.13.1'),
287
+ serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.14.1'),
288
288
 
289
289
  /** Logging level */
290
290
  logLevel: LogLevelSchema.default('info'),
@@ -9,13 +9,15 @@ import type { CapturedKnowledge } from './types'
9
9
 
10
10
  export async function handleGitCapture(): Promise<void> {
11
11
  // Parse positional args: project, branch, message, files, port
12
- const [, , , project, branch, message, filesStr, portStr] = process.argv
12
+ const [, , , rawProject, branch, message, filesStr, portStr] = process.argv
13
13
 
14
- if (!project || !message) {
14
+ if (!rawProject || !message) {
15
15
  process.exit(0)
16
16
  return
17
17
  }
18
18
 
19
+ // Phase 23b: Resolve project name — strip scoped package prefix, validate
20
+ const project = resolveProjectName(rawProject)
19
21
  const port = parseInt(portStr || process.env.CLAUDE_BRAIN_PORT || '3000', 10)
20
22
  const files = filesStr ? filesStr.split(',').filter(Boolean) : []
21
23
 
@@ -60,6 +62,19 @@ export async function handleGitCapture(): Promise<void> {
60
62
  process.exit(0)
61
63
  }
62
64
 
65
+ /**
66
+ * Phase 23b: Resolve project name from raw input.
67
+ * Strips npm scoped prefixes (@scope/name → name) and normalizes.
68
+ */
69
+ function resolveProjectName(raw: string): string {
70
+ let name = raw.trim()
71
+ // Strip npm scoped prefix: @scope/name → name
72
+ if (name.startsWith('@') && name.includes('/')) {
73
+ name = name.split('/').pop() || name
74
+ }
75
+ return name || raw
76
+ }
77
+
63
78
  /**
64
79
  * Detect technologies from file extensions.
65
80
  */
@@ -25,7 +25,17 @@ function buildHookScript(): string {
25
25
  ' BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)',
26
26
  ' MESSAGE=$(git log -1 --pretty=%B 2>/dev/null)',
27
27
  ' FILES=$(git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | tr "\\n" "," | sed "s/,$//")',
28
- ' PROJECT=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")',
28
+ ' REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)',
29
+ ' # Phase 23b: Smart project name detection (package.json > init-context > basename)',
30
+ ' if [ -f "$REPO_ROOT/package.json" ]; then',
31
+ ' PROJECT=$(grep -m1 \'"name"\' "$REPO_ROOT/package.json" 2>/dev/null | sed \'s/.*"name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/\' | sed \'s/@[^/]*\\///\')',
32
+ ' fi',
33
+ ' if [ -z "$PROJECT" ] && [ -f "$REPO_ROOT/.claude-brain/init-context.md" ]; then',
34
+ ' PROJECT=$(head -1 "$REPO_ROOT/.claude-brain/init-context.md" 2>/dev/null | sed \'s/^Project "\\([^"]*\\)".*/\\1/\')',
35
+ ' fi',
36
+ ' if [ -z "$PROJECT" ]; then',
37
+ ' PROJECT=$(basename "$REPO_ROOT")',
38
+ ' fi',
29
39
  ' PORT=${CLAUDE_BRAIN_PORT:-3000}',
30
40
  ' claude-brain git-capture "$PROJECT" "$BRANCH" "$MESSAGE" "$FILES" "$PORT" 2>/dev/null',
31
41
  ') &',
@@ -152,30 +152,40 @@ function isOurHookEntry(entry: any): boolean {
152
152
  )
153
153
  }
154
154
 
155
- /** Copy the hook script to the install location */
155
+ /** All files needed by brain-hook.ts at runtime */
156
+ const HOOK_FILES = [
157
+ 'brain-hook.ts',
158
+ 'capture.ts',
159
+ 'queue.ts',
160
+ 'types.ts',
161
+ 'passive-classifier.ts',
162
+ ]
163
+
164
+ /** Copy the hook script and all its dependencies to the install location */
156
165
  function copyHookScript(): void {
157
- const destPath = getHookScriptPath()
158
- const destDir = dirname(destPath)
166
+ const destDir = dirname(getHookScriptPath())
159
167
 
160
168
  if (!existsSync(destDir)) {
161
169
  mkdirSync(destDir, { recursive: true })
162
170
  }
163
171
 
164
- // Find source script relative to this file
165
- // In development: src/hooks/brain-hook.ts
166
- // In production: dist/hooks/brain-hook.js
167
- const srcPath = join(dirname(new URL(import.meta.url).pathname), 'brain-hook.ts')
168
-
169
- if (existsSync(srcPath)) {
170
- const content = readFileSync(srcPath, 'utf-8')
171
- writeFileSync(destPath, content, 'utf-8')
172
- } else {
173
- // Try .js extension for compiled version
174
- const jsSrcPath = srcPath.replace('.ts', '.js')
175
- if (existsSync(jsSrcPath)) {
176
- const content = readFileSync(jsSrcPath, 'utf-8')
172
+ const srcDir = dirname(new URL(import.meta.url).pathname)
173
+
174
+ for (const file of HOOK_FILES) {
175
+ const srcPath = join(srcDir, file)
176
+ const destPath = join(destDir, file)
177
+
178
+ if (existsSync(srcPath)) {
179
+ const content = readFileSync(srcPath, 'utf-8')
177
180
  writeFileSync(destPath, content, 'utf-8')
181
+ } else {
182
+ // Try .js extension for compiled version
183
+ const jsSrcPath = srcPath.replace('.ts', '.js')
184
+ const jsDestPath = destPath.replace('.ts', '.js')
185
+ if (existsSync(jsSrcPath)) {
186
+ const content = readFileSync(jsSrcPath, 'utf-8')
187
+ writeFileSync(jsDestPath, content, 'utf-8')
188
+ }
178
189
  }
179
- // If neither exists, the hook script will need to be installed separately
180
190
  }
181
191
  }
@@ -5,13 +5,20 @@
5
5
  */
6
6
 
7
7
  import { appendFileSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
8
- import { dirname, join } from 'node:path'
9
- import { getHomePaths } from '@/config/home'
8
+ import { dirname, join, resolve } from 'node:path'
9
+ import { homedir } from 'node:os'
10
10
  import type { CapturedKnowledge } from './types'
11
11
 
12
+ /** Inline home path resolution — avoids @/config/home dependency for <200ms hook context */
13
+ function getHookDataDir(): string {
14
+ const envHome = process.env.CLAUDE_BRAIN_HOME
15
+ const root = envHome ? resolve(envHome) : join(homedir(), '.claude-brain')
16
+ return join(root, 'data')
17
+ }
18
+
12
19
  /** Get the queue file path */
13
20
  export function getQueuePath(): string {
14
- return join(getHomePaths().data, 'hook-queue.jsonl')
21
+ return join(getHookDataDir(), 'hook-queue.jsonl')
15
22
  }
16
23
 
17
24
  /**
@@ -88,7 +88,12 @@ const PROGRESS_PHRASES = [
88
88
  'built', 'created', 'added', 'fixed', 'resolved',
89
89
  'shipped', 'deployed', 'merged', 'released',
90
90
  "i'm working on", 'currently working', 'making progress',
91
- 'just did', 'just finished'
91
+ 'just did', 'just finished',
92
+ // Phase 23b: present-tense statement patterns
93
+ "i'm implementing", "i'm building", "i'm adding",
94
+ "i'm fixing", "i'm refactoring", "i'm setting up",
95
+ "i'm migrating", "i'm updating", "i'm creating",
96
+ 'started implementing', 'started building', 'started adding'
92
97
  ]
93
98
 
94
99
  // Comparison indicators
@@ -156,10 +161,13 @@ const LIST_PHRASES = [
156
161
  // Update/correct existing memory
157
162
  const UPDATE_PHRASES = [
158
163
  'actually,', 'actually ', 'correction:', 'update that',
159
- 'i changed my mind', 'change that to', 'instead of what i said',
164
+ 'i changed my mind', 'changed my mind', 'change that to', 'instead of what i said',
160
165
  'supersedes', 'override', 'revise that', 'amend that',
161
166
  'that should be', 'update the decision', 'modify that',
162
- 'replace that with', 'no wait', 'scratch that, use'
167
+ 'replace that with', 'no wait', 'scratch that, use',
168
+ // Phase 23b: additional update triggers
169
+ 'switch to', 'instead use', 'use instead', 'no longer using',
170
+ 'moved to', 'migrated to', 'replaced with'
163
171
  ]
164
172
 
165
173
  // Delete/forget memory
@@ -23,6 +23,7 @@ import {
23
23
  getPhase12Service,
24
24
  getKnowledgeGraphService,
25
25
  getEpisodeService,
26
+ getSessionTracker,
26
27
  isServicesInitialized
27
28
  } from '@/server/services'
28
29
  import { timed } from '@/utils/timing'
@@ -926,6 +927,29 @@ export class BrainRouter {
926
927
  }
927
928
  }
928
929
 
930
+ // Phase 23b Fix 5: Session recall — "what have we discussed" queries session tracker
931
+ if (this.isSessionRecallQuery(message)) {
932
+ try {
933
+ const tracker = getSessionTracker()
934
+ if (tracker) {
935
+ const stats = tracker.getStats()
936
+ if (stats.totalItems > 0) {
937
+ tiers.push({
938
+ label: 'Current Session',
939
+ results: [{
940
+ content: `Active sessions: ${stats.activeSessions}, items tracked: ${stats.totalItems}`,
941
+ score: 0.95,
942
+ source: 'Session Tracker',
943
+ metadata: {}
944
+ }]
945
+ })
946
+ }
947
+ }
948
+ } catch {
949
+ // Session tracker not available
950
+ }
951
+ }
952
+
929
953
  // Register with episode
930
954
  this.registerEpisodeMessage(message, effectiveProject, 'question')
931
955
 
@@ -1203,6 +1227,30 @@ export class BrainRouter {
1203
1227
  }
1204
1228
  }
1205
1229
 
1230
+ /**
1231
+ * Phase 23b: Detect session recall queries ("what have we discussed", "this session", etc.)
1232
+ */
1233
+ private isSessionRecallQuery(message: string): boolean {
1234
+ const lower = message.toLowerCase()
1235
+ const SESSION_RECALL_PHRASES = [
1236
+ 'what have we discussed',
1237
+ 'what did we discuss',
1238
+ 'what have we talked about',
1239
+ 'what did we talk about',
1240
+ 'this session',
1241
+ 'current session',
1242
+ 'session so far',
1243
+ 'what have we done',
1244
+ 'what did we do',
1245
+ 'session summary',
1246
+ 'summarize this session',
1247
+ 'recap this session',
1248
+ 'what happened this session',
1249
+ 'what have we covered'
1250
+ ]
1251
+ return SESSION_RECALL_PHRASES.some(p => lower.includes(p))
1252
+ }
1253
+
1206
1254
  /**
1207
1255
  * C7: Detect complex multi-part questions
1208
1256
  */
@@ -7,6 +7,7 @@ import { Hono } from 'hono'
7
7
  import type { Logger } from 'pino'
8
8
  import type { Config } from '@/config'
9
9
  import { getMemoryService, getVaultService, isServicesInitialized } from '@/server/services'
10
+ import { ResourceProvider } from '@/server/providers/resources'
10
11
  import type { MemoryManager } from '@/memory'
11
12
  import type { CapturedKnowledge, HookStats } from '@/hooks/types'
12
13
  import { SmartDeduplicator } from '@/hooks/deduplicator'
@@ -81,6 +82,9 @@ export class HttpApiServer {
81
82
  this.app.post('/api/hooks/ingest', (c) => this.handleHookIngest(c))
82
83
  this.app.post('/api/hooks/session-end', (c) => this.handleHookSessionEnd(c))
83
84
  this.app.get('/api/hooks/status', () => this.handleHookStatus())
85
+
86
+ // Phase 23b: Expose brain://context/auto via HTTP for testability
87
+ this.app.get('/api/context/auto', () => this.handleContextAuto())
84
88
  }
85
89
 
86
90
  private async handleListProjects(): Promise<Response> {
@@ -458,6 +462,23 @@ export class HttpApiServer {
458
462
  }
459
463
  }
460
464
 
465
+ // ─── Phase 23b: Context Auto Endpoint ────────────────────
466
+
467
+ private async handleContextAuto(): Promise<Response> {
468
+ try {
469
+ const resourceProvider = new ResourceProvider(this.logger)
470
+ const result = await resourceProvider.readResource('brain://context/auto')
471
+ const text = result?.contents?.[0]?.text || ''
472
+ return Response.json({ success: true, data: { content: text } })
473
+ } catch (error) {
474
+ this.logger.error({ error }, 'Failed to get auto context')
475
+ return Response.json(
476
+ { success: false, error: 'Failed to get auto context' },
477
+ { status: 500 }
478
+ )
479
+ }
480
+ }
481
+
461
482
  // ─── Phase 17: Hook Endpoints ────────────────────────────
462
483
 
463
484
  private async handleHookIngest(c: any): Promise<Response> {
@@ -564,7 +585,7 @@ export class HttpApiServer {
564
585
  ...this.hookStats,
565
586
  activeSessions: sessionStats?.activeSessions ?? 0,
566
587
  sessionItems: sessionStats?.totalItems ?? 0,
567
- hooksEnabled: this.config.hooks?.enabled ?? false,
588
+ hooksEnabled: this.config.hooks?.enabled ?? true,
568
589
  },
569
590
  })
570
591
  }