claude-brain 0.14.4 → 0.15.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.14.4
1
+ 0.15.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-brain",
3
- "version": "0.14.4",
3
+ "version": "0.15.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",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "src/**/*.ts",
12
+ "scripts/postinstall.mjs",
12
13
  "packs/",
13
14
  "assets/",
14
15
  "package.json",
@@ -19,6 +20,7 @@
19
20
  "LICENSE"
20
21
  ],
21
22
  "scripts": {
23
+ "postinstall": "node scripts/postinstall.mjs",
22
24
  "dev": "CLAUDE_BRAIN_HOME=. bun --watch src/index.ts",
23
25
  "build": "bun build src/index.ts --outdir dist --target bun",
24
26
  "build:binary": "bun build src/cli/bin.ts --compile --outfile claude-brain",
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script for claude-brain.
5
+ * Runs automatically after `npm install -g claude-brain` or `bun install -g claude-brain`.
6
+ * Pure Node.js — no TypeScript, no path aliases, no Bun APIs.
7
+ * Always exits 0 so it never blocks the install.
8
+ */
9
+
10
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, renameSync } from 'node:fs'
11
+ import { join, dirname } from 'node:path'
12
+ import { homedir } from 'node:os'
13
+ import { execSync } from 'node:child_process'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ const __filename = fileURLToPath(import.meta.url)
17
+ const __dirname = dirname(__filename)
18
+
19
+ const PREFIX = '[claude-brain]'
20
+ const HOME = join(homedir(), '.claude-brain')
21
+ const CLAUDE_DIR = join(homedir(), '.claude')
22
+ const CLAUDE_SETTINGS = join(CLAUDE_DIR, 'settings.json')
23
+ const CLAUDE_MD_PATH = join(CLAUDE_DIR, 'CLAUDE.md')
24
+ const HOOK_MARKER = 'claude-brain-hook'
25
+
26
+ function log(msg) {
27
+ console.error(`${PREFIX} ${msg}`)
28
+ }
29
+
30
+ // ── Step 0: Skip if not a global install ─────────────────
31
+
32
+ function shouldSkip() {
33
+ // CI environments
34
+ if (process.env.CI === 'true' || process.env.CI === '1') {
35
+ return 'CI environment detected'
36
+ }
37
+
38
+ // If we're inside a node_modules directory, it's a local install
39
+ const scriptDir = dirname(__filename)
40
+ if (scriptDir.includes('node_modules') && !scriptDir.includes('global')) {
41
+ // Check if this is actually a global install by looking deeper
42
+ // bun global installs go to ~/.bun/install/global/node_modules
43
+ // npm global installs go to /usr/local/lib/node_modules or ~/.npm-global
44
+ const isGlobalPath =
45
+ scriptDir.includes('.bun/install/global') ||
46
+ scriptDir.includes('/usr/local/lib/node_modules') ||
47
+ scriptDir.includes('/usr/lib/node_modules') ||
48
+ scriptDir.includes('npm-global') ||
49
+ scriptDir.includes('AppData/Roaming/npm')
50
+
51
+ if (!isGlobalPath) {
52
+ return 'local install (not global)'
53
+ }
54
+ }
55
+
56
+ return null
57
+ }
58
+
59
+ // ── Step 1: Create ~/.claude-brain/ ──────────────────────
60
+
61
+ function setupHomeDirectory() {
62
+ if (existsSync(join(HOME, 'data'))) {
63
+ log('Home directory already initialized')
64
+ return true
65
+ }
66
+
67
+ log(`Setting up ${HOME}/...`)
68
+
69
+ const dirs = [
70
+ join(HOME, 'data'),
71
+ join(HOME, 'data', 'chroma'),
72
+ join(HOME, 'logs'),
73
+ join(HOME, 'vault'),
74
+ join(HOME, 'vault', 'Projects'),
75
+ join(HOME, 'vault', 'Global'),
76
+ join(HOME, 'hooks'),
77
+ ]
78
+
79
+ for (const dir of dirs) {
80
+ mkdirSync(dir, { recursive: true })
81
+ }
82
+
83
+ // Write default .env if not present
84
+ const envPath = join(HOME, '.env')
85
+ if (!existsSync(envPath)) {
86
+ writeFileSync(envPath, `# Claude Brain Configuration
87
+ # Generated by postinstall
88
+ VAULT_PATH=${join(HOME, 'vault')}
89
+ LOG_LEVEL=info
90
+ NODE_ENV=production
91
+
92
+ # ChromaDB Configuration
93
+ CHROMA_MODE=client-server
94
+ CHROMA_HOST=localhost
95
+ CHROMA_PORT=8000
96
+ CHROMA_EMBEDDING_PROVIDER=transformers
97
+ `, 'utf-8')
98
+ }
99
+
100
+ // Write default global standards
101
+ const standardsPath = join(HOME, 'vault', 'Global', 'standards.md')
102
+ if (!existsSync(standardsPath)) {
103
+ writeFileSync(standardsPath, `---
104
+ type: global-standards
105
+ last_updated: ${new Date().toISOString().split('T')[0]}
106
+ ---
107
+
108
+ # Global Coding Standards
109
+
110
+ ## General
111
+ - Write clear, readable code
112
+ - Prefer explicit over implicit
113
+ - Keep functions focused and small
114
+
115
+ ## TypeScript
116
+ - Use strict mode
117
+ - Prefer const over let
118
+ - Add JSDoc comments for public APIs
119
+ `, 'utf-8')
120
+ }
121
+
122
+ log('Home directory initialized')
123
+ return true
124
+ }
125
+
126
+ // ── Step 2: Install ChromaDB via pip ─────────────────────
127
+
128
+ function installChromaDB() {
129
+ // Check if already installed
130
+ try {
131
+ execSync('chroma --version', { stdio: 'pipe', timeout: 5000 })
132
+ log('ChromaDB already installed')
133
+ return true
134
+ } catch {}
135
+
136
+ // Check for Python
137
+ let pythonCmd = null
138
+ for (const cmd of ['python3', 'python']) {
139
+ try {
140
+ const ver = execSync(`${cmd} --version`, { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 })
141
+ if (ver.includes('3.')) {
142
+ pythonCmd = cmd
143
+ break
144
+ }
145
+ } catch {}
146
+ }
147
+
148
+ if (!pythonCmd) {
149
+ log('Python 3 not found — skipping ChromaDB install (SQLite fallback will be used)')
150
+ return false
151
+ }
152
+
153
+ log('Installing ChromaDB...')
154
+
155
+ const pipCommands = pythonCmd === 'python3'
156
+ ? ['pip3 install chromadb', 'python3 -m pip install chromadb']
157
+ : ['pip install chromadb', 'python -m pip install chromadb']
158
+
159
+ for (const cmd of pipCommands) {
160
+ try {
161
+ execSync(cmd, { stdio: 'pipe', timeout: 300_000 })
162
+ log('ChromaDB installed')
163
+ return true
164
+ } catch {}
165
+ }
166
+
167
+ log('ChromaDB install failed — SQLite fallback will be used')
168
+ return false
169
+ }
170
+
171
+ // ── Step 3: Register MCP server ──────────────────────────
172
+
173
+ function registerMcpServer() {
174
+ // Check if claude CLI exists
175
+ try {
176
+ execSync('claude --version', { stdio: 'pipe', timeout: 5000 })
177
+ } catch {
178
+ log('Claude CLI not found — skipping MCP registration')
179
+ log(' Run manually: claude mcp add claude-brain -- claude-brain serve')
180
+ return false
181
+ }
182
+
183
+ // Check if already configured
184
+ try {
185
+ const result = execSync('claude mcp list', { encoding: 'utf-8', stdio: 'pipe', timeout: 10000 })
186
+ if (result.includes('claude-brain')) {
187
+ log('MCP server already registered')
188
+ return true
189
+ }
190
+ } catch {}
191
+
192
+ // Register
193
+ try {
194
+ execSync('claude mcp add claude-brain -- claude-brain serve', {
195
+ stdio: 'pipe',
196
+ timeout: 15000,
197
+ })
198
+ log('Registered as MCP server in Claude Code')
199
+ return true
200
+ } catch (err) {
201
+ const msg = err instanceof Error ? err.message : String(err)
202
+ if (msg.includes('already') || msg.includes('exists')) {
203
+ log('MCP server already registered')
204
+ return true
205
+ }
206
+ log('MCP registration failed — run manually: claude mcp add claude-brain -- claude-brain serve')
207
+ return false
208
+ }
209
+ }
210
+
211
+ // ── Step 4: Install hooks ────────────────────────────────
212
+
213
+ function installHooks() {
214
+ // Read existing settings
215
+ let settings = {}
216
+ if (existsSync(CLAUDE_SETTINGS)) {
217
+ try {
218
+ settings = JSON.parse(readFileSync(CLAUDE_SETTINGS, 'utf-8'))
219
+ } catch {}
220
+ }
221
+
222
+ // Check if already installed
223
+ function hasOurHooks(entries) {
224
+ if (!Array.isArray(entries)) return false
225
+ return entries.some(entry =>
226
+ entry && Array.isArray(entry.hooks) &&
227
+ entry.hooks.some(h => typeof h.command === 'string' && h.command.includes(HOOK_MARKER))
228
+ )
229
+ }
230
+
231
+ if (settings.hooks && (hasOurHooks(settings.hooks.PostToolUse) || hasOurHooks(settings.hooks.Stop))) {
232
+ log('Hooks already installed')
233
+ return true
234
+ }
235
+
236
+ // Build hook command
237
+ const scriptPath = join(HOME, 'hooks', 'brain-hook.ts')
238
+ function buildCmd(event) {
239
+ return `bun "${scriptPath}" --event ${event} # ${HOOK_MARKER}`
240
+ }
241
+
242
+ if (!settings.hooks) settings.hooks = {}
243
+
244
+ // PostToolUse
245
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = []
246
+ settings.hooks.PostToolUse.push({
247
+ matcher: '',
248
+ hooks: [{ type: 'command', command: buildCmd('PostToolUse') }],
249
+ })
250
+
251
+ // Stop
252
+ if (!settings.hooks.Stop) settings.hooks.Stop = []
253
+ settings.hooks.Stop.push({
254
+ matcher: '',
255
+ hooks: [{ type: 'command', command: buildCmd('Stop') }],
256
+ })
257
+
258
+ // Write atomically
259
+ if (!existsSync(CLAUDE_DIR)) {
260
+ mkdirSync(CLAUDE_DIR, { recursive: true })
261
+ }
262
+ const tmpPath = CLAUDE_SETTINGS + '.tmp'
263
+ writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
264
+ renameSync(tmpPath, CLAUDE_SETTINGS)
265
+
266
+ log('Hooks installed')
267
+ return true
268
+ }
269
+
270
+ // ── Step 5: Install CLAUDE.md ────────────────────────────
271
+
272
+ function installClaudeMd() {
273
+ // Find the assets/CLAUDE.md relative to this script
274
+ // scripts/postinstall.mjs → assets/CLAUDE.md
275
+ const assetsPath = join(__dirname, '..', 'assets', 'CLAUDE.md')
276
+
277
+ if (!existsSync(assetsPath)) {
278
+ log('CLAUDE.md asset not found — skipping')
279
+ return false
280
+ }
281
+
282
+ // Only install if no CLAUDE.md exists yet (don't overwrite user customizations)
283
+ if (existsSync(CLAUDE_MD_PATH)) {
284
+ // Check if it already mentions claude-brain
285
+ const existing = readFileSync(CLAUDE_MD_PATH, 'utf-8')
286
+ if (existing.includes('brain') || existing.includes('Brain')) {
287
+ log('CLAUDE.md already configured')
288
+ return true
289
+ }
290
+ // Append our section
291
+ const addition = readFileSync(assetsPath, 'utf-8')
292
+ writeFileSync(CLAUDE_MD_PATH, existing.trimEnd() + '\n\n' + addition, 'utf-8')
293
+ log('Appended brain instructions to existing CLAUDE.md')
294
+ return true
295
+ }
296
+
297
+ // Create new
298
+ if (!existsSync(CLAUDE_DIR)) {
299
+ mkdirSync(CLAUDE_DIR, { recursive: true })
300
+ }
301
+ const content = readFileSync(assetsPath, 'utf-8')
302
+ writeFileSync(CLAUDE_MD_PATH, content, 'utf-8')
303
+ log('Installed CLAUDE.md')
304
+ return true
305
+ }
306
+
307
+ // ── Main ─────────────────────────────────────────────────
308
+
309
+ async function main() {
310
+ const skipReason = shouldSkip()
311
+ if (skipReason) {
312
+ log(`Skipping postinstall (${skipReason})`)
313
+ return
314
+ }
315
+
316
+ log('Running postinstall setup...')
317
+ console.error('')
318
+
319
+ const results = {
320
+ home: false,
321
+ chromadb: false,
322
+ mcp: false,
323
+ hooks: false,
324
+ claudemd: false,
325
+ }
326
+
327
+ try { results.home = setupHomeDirectory() } catch (e) { log(`Home setup failed: ${e.message}`) }
328
+ try { results.chromadb = installChromaDB() } catch (e) { log(`ChromaDB install failed: ${e.message}`) }
329
+ try { results.mcp = registerMcpServer() } catch (e) { log(`MCP registration failed: ${e.message}`) }
330
+ try { results.hooks = installHooks() } catch (e) { log(`Hook install failed: ${e.message}`) }
331
+ try { results.claudemd = installClaudeMd() } catch (e) { log(`CLAUDE.md install failed: ${e.message}`) }
332
+
333
+ console.error('')
334
+ log('Setup complete! Restart Claude Code to activate.')
335
+ }
336
+
337
+ main().catch(err => {
338
+ log(`Postinstall error: ${err.message}`)
339
+ }).finally(() => {
340
+ process.exit(0)
341
+ })
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Background auto-update check for claude-brain.
3
+ * Runs on every `serve` start, rate-limited to 1 check per hour.
4
+ * All output to stderr (stdout reserved for MCP JSON-RPC).
5
+ * Never throws — all errors silently caught.
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
9
+ import { join, dirname } from 'node:path'
10
+ import { execSync } from 'node:child_process'
11
+ import { getHomePaths } from '@/config/home'
12
+
13
+ const CHECK_INTERVAL_MS = 60 * 60 * 1000 // 1 hour
14
+ const TIMESTAMP_FILE = 'last-update-check'
15
+
16
+ function getTimestampPath(): string {
17
+ return join(getHomePaths().data, TIMESTAMP_FILE)
18
+ }
19
+
20
+ function shouldCheck(): boolean {
21
+ const tsPath = getTimestampPath()
22
+ if (!existsSync(tsPath)) return true
23
+
24
+ try {
25
+ const lastCheck = parseInt(readFileSync(tsPath, 'utf-8').trim(), 10)
26
+ return Date.now() - lastCheck > CHECK_INTERVAL_MS
27
+ } catch {
28
+ return true
29
+ }
30
+ }
31
+
32
+ function writeTimestamp(): void {
33
+ const tsPath = getTimestampPath()
34
+ const dir = dirname(tsPath)
35
+ if (!existsSync(dir)) {
36
+ mkdirSync(dir, { recursive: true })
37
+ }
38
+ writeFileSync(tsPath, String(Date.now()), 'utf-8')
39
+ }
40
+
41
+ function getCurrentVersion(): string | null {
42
+ try {
43
+ // Read from our own package.json
44
+ const pkgPath = join(dirname(new URL(import.meta.url).pathname), '..', '..', 'package.json')
45
+ if (existsSync(pkgPath)) {
46
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
47
+ return pkg.version || null
48
+ }
49
+ } catch {}
50
+
51
+ // Fallback: read VERSION file
52
+ try {
53
+ const versionPath = join(dirname(new URL(import.meta.url).pathname), '..', '..', 'VERSION')
54
+ if (existsSync(versionPath)) {
55
+ return readFileSync(versionPath, 'utf-8').trim()
56
+ }
57
+ } catch {}
58
+
59
+ return null
60
+ }
61
+
62
+ function getLatestVersion(): string | null {
63
+ try {
64
+ const result = execSync('npm view claude-brain version', {
65
+ encoding: 'utf-8',
66
+ stdio: ['pipe', 'pipe', 'pipe'],
67
+ timeout: 10_000,
68
+ })
69
+ return result.trim() || null
70
+ } catch {
71
+ return null
72
+ }
73
+ }
74
+
75
+ function isNewer(latest: string, current: string): boolean {
76
+ const parse = (v: string) => v.split('.').map(Number)
77
+ const [lMaj, lMin, lPat] = parse(latest)
78
+ const [cMaj, cMin, cPat] = parse(current)
79
+
80
+ if (lMaj !== cMaj) return lMaj > cMaj
81
+ if (lMin !== cMin) return lMin > cMin
82
+ return lPat > cPat
83
+ }
84
+
85
+ function performUpdate(): boolean {
86
+ // Try bun first, fallback to npm
87
+ const commands = [
88
+ 'bun update -g claude-brain',
89
+ 'npm update -g claude-brain',
90
+ ]
91
+
92
+ for (const cmd of commands) {
93
+ try {
94
+ execSync(cmd, {
95
+ stdio: ['pipe', 'pipe', 'pipe'],
96
+ timeout: 120_000,
97
+ })
98
+ return true
99
+ } catch {}
100
+ }
101
+
102
+ return false
103
+ }
104
+
105
+ function postUpdateTasks(): void {
106
+ // Re-install hooks (idempotent)
107
+ try {
108
+ const { installHooks } = require('@/hooks/installer')
109
+ installHooks()
110
+ } catch {}
111
+
112
+ // Re-install CLAUDE.md if asset exists
113
+ try {
114
+ const { existsSync: exists, readFileSync: read, writeFileSync: write } = require('node:fs')
115
+ const { join: pjoin } = require('node:path')
116
+ const { homedir } = require('node:os')
117
+
118
+ const assetsPath = pjoin(dirname(new URL(import.meta.url).pathname), '..', '..', 'assets', 'CLAUDE.md')
119
+ const claudeMdPath = pjoin(homedir(), '.claude', 'CLAUDE.md')
120
+
121
+ if (exists(assetsPath)) {
122
+ if (!exists(claudeMdPath)) {
123
+ write(claudeMdPath, read(assetsPath, 'utf-8'), 'utf-8')
124
+ }
125
+ }
126
+ } catch {}
127
+ }
128
+
129
+ async function checkAndUpdate(): Promise<void> {
130
+ if (!shouldCheck()) return
131
+
132
+ writeTimestamp()
133
+
134
+ const current = getCurrentVersion()
135
+ if (!current) return
136
+
137
+ const latest = getLatestVersion()
138
+ if (!latest) return
139
+
140
+ if (!isNewer(latest, current)) return
141
+
142
+ console.error(`[claude-brain] Update available: ${current} → ${latest}`)
143
+
144
+ const updated = performUpdate()
145
+ if (updated) {
146
+ console.error(`[claude-brain] Updated to ${latest}`)
147
+ postUpdateTasks()
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Fire-and-forget background update check.
153
+ * Call this from serve.ts — it never rejects.
154
+ */
155
+ export function checkForUpdateInBackground(): Promise<void> {
156
+ return checkAndUpdate().catch(() => {})
157
+ }
@@ -18,6 +18,12 @@ export async function runServe() {
18
18
  // Auto-initialize home directory on first run
19
19
  ensureHomeDirectory()
20
20
 
21
+ // Background auto-update (non-blocking)
22
+ try {
23
+ const { checkForUpdateInBackground } = await import('@/cli/auto-update')
24
+ checkForUpdateInBackground().catch(() => {})
25
+ } catch {}
26
+
21
27
  // Auto-install Claude Code hooks (idempotent, non-fatal)
22
28
  try {
23
29
  const { installHooks } = await import('@/hooks/installer')
@@ -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.14.4',
6
+ serverVersion: '0.15.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.14.4'),
287
+ serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.15.0'),
288
288
 
289
289
  /** Logging level */
290
290
  logLevel: LogLevelSchema.default('info'),
@@ -135,7 +135,7 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
135
135
  export function createEmbeddingProvider(
136
136
  logger: Logger,
137
137
  config: ChromaConfig
138
- ): EmbeddingProvider {
138
+ ): EmbeddingProvider | undefined {
139
139
  switch (config.embeddingProvider) {
140
140
  case 'openai':
141
141
  if (!config.openaiApiKey) {
@@ -148,6 +148,8 @@ export function createEmbeddingProvider(
148
148
 
149
149
  case 'default':
150
150
  default:
151
- return new DefaultEmbeddingProvider()
151
+ // Return undefined so store/search code falls through to queryTexts path
152
+ // (ChromaDB's built-in auto-embedding)
153
+ return undefined
152
154
  }
153
155
  }
@@ -23,7 +23,7 @@ export class ChromaManager {
23
23
  public collections: CollectionManager
24
24
  public store: ChromaMemoryStore
25
25
  public search: ChromaSearchEngine
26
- public embeddings: EmbeddingProvider
26
+ public embeddings: EmbeddingProvider | undefined
27
27
 
28
28
  private initialized: boolean = false
29
29