claude-brain 0.15.0 → 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 +1 -1
- 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/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.15.
|
|
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
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.15.
|
|
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.15.
|
|
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
|
}
|