claude-brain 0.14.4 → 0.15.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/assets/CLAUDE.md +1 -1
- package/package.json +3 -1
- package/scripts/postinstall.mjs +341 -0
- package/src/cli/auto-update.ts +157 -0
- package/src/cli/commands/serve.ts +6 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +1 -1
- package/src/hooks/installer.ts +4 -1
- package/src/hooks/passive-classifier.ts +56 -15
- package/src/memory/chroma/embeddings.ts +4 -2
- package/src/memory/chroma/index.ts +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.15.1
|
package/assets/CLAUDE.md
CHANGED
|
@@ -8,4 +8,4 @@ Use the `brain` tool ONLY when you want to:
|
|
|
8
8
|
- Update something: "Changed my mind, use Postgres instead"
|
|
9
9
|
- Delete something: "Remove the note about migrations"
|
|
10
10
|
|
|
11
|
-
Everything else (session tracking, file captures, git commits, context loading) happens automatically.
|
|
11
|
+
Everything else (session tracking, file captures, git commits, context loading) happens automatically via invisible PostToolUse and Stop hooks in ~/.claude/settings.json. These hooks silently capture tool events and store patterns, decisions, and corrections without any explicit brain() call.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -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')
|
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.15.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.15.1'),
|
|
288
288
|
|
|
289
289
|
/** Logging level */
|
|
290
290
|
logLevel: LogLevelSchema.default('info'),
|
package/src/hooks/installer.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs'
|
|
7
7
|
import { join, dirname } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
8
9
|
import { homedir } from 'node:os'
|
|
9
10
|
import { getClaudeBrainHome } from '@/config/home'
|
|
10
11
|
|
|
@@ -169,7 +170,9 @@ function copyHookScript(): void {
|
|
|
169
170
|
mkdirSync(destDir, { recursive: true })
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
|
|
173
|
+
// Use Bun's import.meta.dir if available, otherwise fileURLToPath for Windows compat
|
|
174
|
+
// (new URL(...).pathname returns "/C:/..." on Windows, breaking existsSync)
|
|
175
|
+
const srcDir = (import.meta as any).dir ?? dirname(fileURLToPath(import.meta.url))
|
|
173
176
|
|
|
174
177
|
for (const file of HOOK_FILES) {
|
|
175
178
|
const srcPath = join(srcDir, file)
|
|
@@ -111,6 +111,36 @@ export class PassiveClassifier {
|
|
|
111
111
|
|
|
112
112
|
// Check for new file creation (Write tool)
|
|
113
113
|
if (input.tool_name?.toLowerCase() === 'write') {
|
|
114
|
+
// Check file content for decision/correction language before defaulting to pattern
|
|
115
|
+
if (typeof content === 'string' && content.length > 50) {
|
|
116
|
+
const decisionInContent = this.detectDecisionLanguage(content)
|
|
117
|
+
if (decisionInContent) {
|
|
118
|
+
return {
|
|
119
|
+
type: 'decision',
|
|
120
|
+
confidence: 0.8,
|
|
121
|
+
content: decisionInContent,
|
|
122
|
+
project: this.extractProjectFromCwd(input.cwd),
|
|
123
|
+
technologies,
|
|
124
|
+
metadata: { filePath, role, action: 'create' },
|
|
125
|
+
source: 'hook-passive',
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const correctionInContent = this.detectCorrectionLanguage(content)
|
|
130
|
+
if (correctionInContent) {
|
|
131
|
+
return {
|
|
132
|
+
type: 'correction',
|
|
133
|
+
confidence: 0.75,
|
|
134
|
+
content: correctionInContent,
|
|
135
|
+
project: this.extractProjectFromCwd(input.cwd),
|
|
136
|
+
technologies,
|
|
137
|
+
metadata: { filePath, role, action: 'create' },
|
|
138
|
+
source: 'hook-passive',
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
114
144
|
return {
|
|
115
145
|
type: 'pattern',
|
|
116
146
|
confidence: 0.7,
|
|
@@ -161,10 +191,19 @@ export class PassiveClassifier {
|
|
|
161
191
|
}
|
|
162
192
|
|
|
163
193
|
private classifyBashCommand(input: HookInput): CapturedKnowledge | null {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
194
|
+
const rawCommand = (input.tool_input?.command || '') as string
|
|
195
|
+
if (!rawCommand || rawCommand.length < 3) return null
|
|
196
|
+
|
|
197
|
+
// Split compound commands (cd "..." && bun add react) into sub-commands
|
|
198
|
+
const subCommands = rawCommand.split(/\s*(?:&&|\|\||;)\s*/).map(s => s.trim()).filter(Boolean)
|
|
199
|
+
|
|
200
|
+
// Find the first meaningful sub-command (skip cd, export, etc.)
|
|
201
|
+
const command = subCommands.find(sub => {
|
|
202
|
+
const firstWord = sub.split(/\s+/)[0]?.toLowerCase()
|
|
203
|
+
return !firstWord || !SKIP_COMMANDS.has(firstWord)
|
|
204
|
+
}) || rawCommand
|
|
166
205
|
|
|
167
|
-
//
|
|
206
|
+
// If all sub-commands are skip-worthy, bail
|
|
168
207
|
const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase()
|
|
169
208
|
if (firstWord && SKIP_COMMANDS.has(firstWord)) return null
|
|
170
209
|
|
|
@@ -180,7 +219,7 @@ export class PassiveClassifier {
|
|
|
180
219
|
content: `Installed package(s): ${packages}`,
|
|
181
220
|
project: this.extractProjectFromCwd(input.cwd),
|
|
182
221
|
technologies: this.extractTechFromPackages(packages),
|
|
183
|
-
metadata: { command, action: 'install' },
|
|
222
|
+
metadata: { command: rawCommand, action: 'install' },
|
|
184
223
|
source: 'hook-passive',
|
|
185
224
|
timestamp: new Date().toISOString(),
|
|
186
225
|
}
|
|
@@ -198,16 +237,17 @@ export class PassiveClassifier {
|
|
|
198
237
|
content: `Git: ${command.trim().slice(0, 200)}`,
|
|
199
238
|
project: this.extractProjectFromCwd(input.cwd),
|
|
200
239
|
technologies: ['git'],
|
|
201
|
-
metadata: { command, action: 'git' },
|
|
240
|
+
metadata: { command: rawCommand, action: 'git' },
|
|
202
241
|
source: 'hook-passive',
|
|
203
242
|
timestamp: new Date().toISOString(),
|
|
204
243
|
}
|
|
205
244
|
}
|
|
206
245
|
}
|
|
207
246
|
|
|
208
|
-
// Test/build runs
|
|
247
|
+
// Test/build runs — check all sub-commands since test may follow cd
|
|
248
|
+
const buildCommand = subCommands.find(sub => BUILD_PATTERNS.some(p => p.test(sub))) || command
|
|
209
249
|
for (const pattern of BUILD_PATTERNS) {
|
|
210
|
-
if (pattern.test(
|
|
250
|
+
if (pattern.test(buildCommand)) {
|
|
211
251
|
const responseText = this.extractResponseText(input.tool_response)
|
|
212
252
|
const failed = responseText?.toLowerCase().includes('fail') ||
|
|
213
253
|
responseText?.toLowerCase().includes('error')
|
|
@@ -216,10 +256,10 @@ export class PassiveClassifier {
|
|
|
216
256
|
return {
|
|
217
257
|
type: 'correction',
|
|
218
258
|
confidence: 0.75,
|
|
219
|
-
content: `Build/test failure: ${
|
|
259
|
+
content: `Build/test failure: ${buildCommand.trim().slice(0, 100)}`,
|
|
220
260
|
project: this.extractProjectFromCwd(input.cwd),
|
|
221
261
|
technologies: [],
|
|
222
|
-
metadata: { command, action: 'build', failed: true },
|
|
262
|
+
metadata: { command: rawCommand, action: 'build', failed: true },
|
|
223
263
|
source: 'hook-passive',
|
|
224
264
|
timestamp: new Date().toISOString(),
|
|
225
265
|
}
|
|
@@ -228,10 +268,10 @@ export class PassiveClassifier {
|
|
|
228
268
|
return {
|
|
229
269
|
type: 'progress',
|
|
230
270
|
confidence: 0.7,
|
|
231
|
-
content: `Ran: ${
|
|
271
|
+
content: `Ran: ${buildCommand.trim().slice(0, 200)}`,
|
|
232
272
|
project: this.extractProjectFromCwd(input.cwd),
|
|
233
273
|
technologies: [],
|
|
234
|
-
metadata: { command, action: 'build', failed: false },
|
|
274
|
+
metadata: { command: rawCommand, action: 'build', failed: false },
|
|
235
275
|
source: 'hook-passive',
|
|
236
276
|
timestamp: new Date().toISOString(),
|
|
237
277
|
}
|
|
@@ -249,7 +289,7 @@ export class PassiveClassifier {
|
|
|
249
289
|
content: correction,
|
|
250
290
|
project: this.extractProjectFromCwd(input.cwd),
|
|
251
291
|
technologies: [],
|
|
252
|
-
metadata: { command, action: 'bash' },
|
|
292
|
+
metadata: { command: rawCommand, action: 'bash' },
|
|
253
293
|
source: 'hook-passive',
|
|
254
294
|
timestamp: new Date().toISOString(),
|
|
255
295
|
}
|
|
@@ -281,7 +321,7 @@ export class PassiveClassifier {
|
|
|
281
321
|
|
|
282
322
|
/** Extract file role from path segments */
|
|
283
323
|
private extractRoleFromPath(filePath: string): string | undefined {
|
|
284
|
-
const segments = filePath.toLowerCase().split(
|
|
324
|
+
const segments = filePath.toLowerCase().split(/[/\\]/)
|
|
285
325
|
for (const segment of segments) {
|
|
286
326
|
if (PATH_ROLE_MAP[segment]) return PATH_ROLE_MAP[segment]
|
|
287
327
|
}
|
|
@@ -331,7 +371,8 @@ export class PassiveClassifier {
|
|
|
331
371
|
/** Extract project name from cwd (last directory segment) */
|
|
332
372
|
private extractProjectFromCwd(cwd: string): string | undefined {
|
|
333
373
|
if (!cwd) return undefined
|
|
334
|
-
|
|
374
|
+
// Split on both / and \ for cross-platform support
|
|
375
|
+
const parts = cwd.split(/[/\\]/).filter(Boolean)
|
|
335
376
|
const last = parts.pop()
|
|
336
377
|
if (last && last.length > 1 && last.length < 50) {
|
|
337
378
|
return last.replace(/\s+/g, '-').toLowerCase()
|
|
@@ -341,7 +382,7 @@ export class PassiveClassifier {
|
|
|
341
382
|
|
|
342
383
|
/** Shorten a file path for display */
|
|
343
384
|
private shortenPath(filePath: string): string {
|
|
344
|
-
const parts = filePath.split(
|
|
385
|
+
const parts = filePath.split(/[/\\]/)
|
|
345
386
|
if (parts.length <= 3) return filePath
|
|
346
387
|
return `.../${parts.slice(-3).join('/')}`
|
|
347
388
|
}
|
|
@@ -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
|
-
|
|
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
|
|