claude-brain 0.17.13 → 0.22.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 +1 -1
- package/package.json +3 -1
- package/scripts/postinstall.mjs +80 -104
- package/src/cli/auto-setup.ts +1 -9
- package/src/cli/bin.ts +23 -2
- package/src/cli/commands/export.ts +130 -0
- package/src/cli/commands/reindex.ts +107 -0
- package/src/cli/commands/serve.ts +54 -0
- package/src/cli/commands/status.ts +158 -0
- package/src/code-intelligence/indexer.ts +315 -0
- package/src/code-intelligence/linker.ts +178 -0
- package/src/code-intelligence/parser.ts +484 -0
- package/src/code-intelligence/query.ts +291 -0
- package/src/code-intelligence/schema.ts +83 -0
- package/src/code-intelligence/types.ts +95 -0
- package/src/config/defaults.ts +3 -3
- package/src/config/loader.ts +6 -0
- package/src/config/schema.ts +28 -2
- package/src/health/index.ts +5 -2
- package/src/hooks/brain-hook.ts +4 -1
- package/src/hooks/context-hook.ts +69 -10
- package/src/hooks/installer.ts +4 -7
- package/src/intelligence/cross-project/index.ts +1 -7
- package/src/intelligence/prediction/index.ts +1 -7
- package/src/intelligence/reasoning/index.ts +1 -7
- package/src/memory/compression.ts +105 -0
- package/src/memory/fts5-search.ts +456 -0
- package/src/memory/index.ts +342 -38
- package/src/memory/migrations/add-fts5.ts +98 -0
- package/src/memory/pruning.ts +60 -0
- package/src/routing/intent-classifier.ts +58 -1
- package/src/routing/response-filter.ts +128 -0
- package/src/routing/router.ts +457 -54
- package/src/server/http-api.ts +319 -1
- package/src/server/providers/resources.ts +1 -42
- package/src/server/services.ts +113 -12
- package/src/server/web-viewer.ts +1115 -0
- package/src/setup/index.ts +12 -22
- package/src/tools/schemas.ts +1 -1
- package/src/intelligence/cross-project/affinity.ts +0 -159
- package/src/intelligence/cross-project/transfer.ts +0 -201
- package/src/intelligence/prediction/context-anticipator.ts +0 -198
- package/src/intelligence/prediction/decision-predictor.ts +0 -184
- package/src/intelligence/reasoning/counterfactual.ts +0 -248
- package/src/intelligence/reasoning/synthesizer.ts +0 -167
- package/src/setup/wizard.ts +0 -459
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Log Pruning — Phase 30
|
|
3
|
+
* Removes old activity_log entries to prevent database bloat.
|
|
4
|
+
* Runs on server startup and can be scheduled for periodic cleanup.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from 'bun:sqlite'
|
|
8
|
+
import type { Logger } from 'pino'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Prune activity log entries older than maxAgeDays.
|
|
12
|
+
* Returns the number of rows deleted.
|
|
13
|
+
*/
|
|
14
|
+
export function pruneActivityLog(db: Database, maxAgeDays: number = 30): number {
|
|
15
|
+
const cutoff = new Date(Date.now() - maxAgeDays * 86400000).toISOString()
|
|
16
|
+
|
|
17
|
+
// Count before delete since bun:sqlite db.run doesn't return changes easily
|
|
18
|
+
const countBefore = (db.prepare('SELECT COUNT(*) as cnt FROM activity_log WHERE created_at < ?').get(cutoff) as any)?.cnt ?? 0
|
|
19
|
+
if (countBefore === 0) return 0
|
|
20
|
+
|
|
21
|
+
db.prepare('DELETE FROM activity_log WHERE created_at < ?').run(cutoff)
|
|
22
|
+
return countBefore
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Start periodic pruning on a timer.
|
|
27
|
+
* Returns a cleanup function to stop the timer.
|
|
28
|
+
*/
|
|
29
|
+
export function startPeriodicPruning(
|
|
30
|
+
db: Database,
|
|
31
|
+
logger: Logger,
|
|
32
|
+
maxAgeDays: number = 30,
|
|
33
|
+
intervalMs: number = 24 * 60 * 60 * 1000 // 24 hours
|
|
34
|
+
): () => void {
|
|
35
|
+
const pruneLogger = logger.child({ component: 'pruning' })
|
|
36
|
+
|
|
37
|
+
// Run immediately on startup
|
|
38
|
+
try {
|
|
39
|
+
const deleted = pruneActivityLog(db, maxAgeDays)
|
|
40
|
+
if (deleted > 0) {
|
|
41
|
+
pruneLogger.info({ deleted, maxAgeDays }, 'Activity log pruned on startup')
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
pruneLogger.warn({ error }, 'Failed to prune activity log on startup')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Schedule periodic cleanup
|
|
48
|
+
const timer = setInterval(() => {
|
|
49
|
+
try {
|
|
50
|
+
const deleted = pruneActivityLog(db, maxAgeDays)
|
|
51
|
+
if (deleted > 0) {
|
|
52
|
+
pruneLogger.info({ deleted, maxAgeDays }, 'Periodic activity log prune complete')
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
pruneLogger.warn({ error }, 'Periodic activity log prune failed')
|
|
56
|
+
}
|
|
57
|
+
}, intervalMs)
|
|
58
|
+
|
|
59
|
+
return () => clearInterval(timer)
|
|
60
|
+
}
|
|
@@ -25,6 +25,8 @@ export type Intent =
|
|
|
25
25
|
| 'list_all'
|
|
26
26
|
| 'update_memory'
|
|
27
27
|
| 'delete_memory'
|
|
28
|
+
| 'detail_request'
|
|
29
|
+
| 'timeline'
|
|
28
30
|
| 'no_action'
|
|
29
31
|
|
|
30
32
|
export interface ClassificationResult {
|
|
@@ -201,6 +203,28 @@ const TEMPORAL_PHRASES = [
|
|
|
201
203
|
'earlier this', 'earlier today', 'over the past'
|
|
202
204
|
]
|
|
203
205
|
|
|
206
|
+
// Phase 27: Detail request patterns — "details obs_abc123", "show me obs_abc123"
|
|
207
|
+
const DETAIL_PATTERNS = [
|
|
208
|
+
/^details?\s+(\S+)/i, // "details obs_abc123"
|
|
209
|
+
/^show\s+(?:me\s+)?(\S+)/i, // "show me obs_abc123" (only when followed by an ID-like string)
|
|
210
|
+
/^get\s+(?:details?\s+)?(\S+)/i, // "get details obs_abc123"
|
|
211
|
+
/^expand\s+(\S+)/i, // "expand obs_abc123"
|
|
212
|
+
/^more\s+(?:about\s+)?(\S+)/i, // "more about obs_abc123"
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
// Phase 27: Timeline patterns
|
|
216
|
+
const TIMELINE_PATTERNS = [
|
|
217
|
+
/timeline\s+(?:for\s+)?(.+)/i, // "timeline for expense-tracker"
|
|
218
|
+
/what\s+did\s+I\s+do\s+(.+)/i, // "what did I do yesterday"
|
|
219
|
+
/show\s+(?:me\s+)?history/i, // "show me history"
|
|
220
|
+
/recent\s+(?:activity|changes)/i, // "recent activity"
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
/** Check if a string looks like an observation ID (contains underscores or 8+ alphanumeric chars) */
|
|
224
|
+
function isIdLike(s: string): boolean {
|
|
225
|
+
return /[_-]/.test(s) || /^[a-f0-9]{8,}$/i.test(s)
|
|
226
|
+
}
|
|
227
|
+
|
|
204
228
|
export class IntentClassifier {
|
|
205
229
|
/**
|
|
206
230
|
* Classify the intent of a message
|
|
@@ -275,7 +299,18 @@ export class IntentClassifier {
|
|
|
275
299
|
return { primary: 'session_start', confidence: 0.90, secondary }
|
|
276
300
|
}
|
|
277
301
|
|
|
278
|
-
// 12.
|
|
302
|
+
// 12. detail_request: "details obs_abc123", "show me <id>" (before exploration to avoid misclassification)
|
|
303
|
+
if (this.isDetailRequest(lower)) {
|
|
304
|
+
return { primary: 'detail_request', confidence: 0.90, secondary }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 12b. timeline: "timeline for project", "what did I do yesterday", "recent activity" (before exploration)
|
|
308
|
+
if (this.isTimeline(lower)) {
|
|
309
|
+
if (hasTemporal) secondary.push('exploration')
|
|
310
|
+
return { primary: 'timeline', confidence: 0.85, secondary }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 12c. exploration: trends, graph, evolution, history (general exploration that isn't a specific timeline)
|
|
279
314
|
if (this.isExploration(lower)) {
|
|
280
315
|
if (this.isQuestion(lower, message)) secondary.push('question')
|
|
281
316
|
if (hasTemporal) secondary.push('exploration')
|
|
@@ -430,6 +465,28 @@ export class IntentClassifier {
|
|
|
430
465
|
return SESSION_START_PHRASES.some(p => lower.includes(p))
|
|
431
466
|
}
|
|
432
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Phase 27: Detect detail requests — "details <id>", "show me <id>", "expand <id>"
|
|
470
|
+
* Only matches when the argument looks like an ID (contains underscores, hyphens, or 8+ hex chars).
|
|
471
|
+
*/
|
|
472
|
+
private isDetailRequest(lower: string): boolean {
|
|
473
|
+
for (const pattern of DETAIL_PATTERNS) {
|
|
474
|
+
const match = lower.match(pattern)
|
|
475
|
+
if (match && match[1] && isIdLike(match[1])) {
|
|
476
|
+
return true
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return false
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Phase 27: Detect timeline requests — "timeline for X", "recent activity", "what did I do yesterday"
|
|
484
|
+
* Distinct from exploration because it specifically asks for chronological activity view.
|
|
485
|
+
*/
|
|
486
|
+
private isTimeline(lower: string): boolean {
|
|
487
|
+
return TIMELINE_PATTERNS.some(p => p.test(lower))
|
|
488
|
+
}
|
|
489
|
+
|
|
433
490
|
/**
|
|
434
491
|
* Phase 19 B3: Detect temporal signals in the message.
|
|
435
492
|
* Used to add 'temporal' context to secondary intents.
|
|
@@ -259,3 +259,131 @@ export class ResponseFilter {
|
|
|
259
259
|
return `Low relevance match (${score}%)`
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
+
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// Phase 27: Progressive Disclosure — Compact Response Formatters
|
|
265
|
+
// =============================================================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Truncate text to maxLen chars, appending "..." if truncated
|
|
269
|
+
*/
|
|
270
|
+
function truncate(text: string, maxLen: number): string {
|
|
271
|
+
if (!text) return ''
|
|
272
|
+
if (text.length <= maxLen) return text
|
|
273
|
+
return text.substring(0, maxLen).trimEnd() + '...'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Format a date as a human-readable relative time string
|
|
278
|
+
*/
|
|
279
|
+
function formatRelativeTime(dateStr: string | Date | undefined): string {
|
|
280
|
+
if (!dateStr) return 'unknown'
|
|
281
|
+
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr
|
|
282
|
+
if (isNaN(date.getTime())) return 'unknown'
|
|
283
|
+
const now = new Date()
|
|
284
|
+
const diffMs = now.getTime() - date.getTime()
|
|
285
|
+
const diffMins = Math.floor(diffMs / 60000)
|
|
286
|
+
const diffHours = Math.floor(diffMs / 3600000)
|
|
287
|
+
const diffDays = Math.floor(diffMs / 86400000)
|
|
288
|
+
|
|
289
|
+
if (diffMins < 1) return 'just now'
|
|
290
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`
|
|
291
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`
|
|
292
|
+
if (diffDays === 1) return 'yesterday'
|
|
293
|
+
if (diffDays < 7) return `${diffDays} days ago`
|
|
294
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) === 1 ? '' : 's'} ago`
|
|
295
|
+
return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) === 1 ? '' : 's'} ago`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Layer 1: Format a single result as a compact one-liner with metadata
|
|
300
|
+
*/
|
|
301
|
+
export function formatCompactResult(result: any, index: number): string {
|
|
302
|
+
const meta = result.metadata || {}
|
|
303
|
+
const category = result.category || result.type || meta.category || result.source || 'memory'
|
|
304
|
+
const summary = truncate(result.content || result.text || result.decision || '', 80)
|
|
305
|
+
const project = result.project || meta.project || 'general'
|
|
306
|
+
const timeAgo = formatRelativeTime(
|
|
307
|
+
result.createdAt || result.created_at || result.timestamp ||
|
|
308
|
+
result.date || meta.created_at || meta.createdAt || meta.date
|
|
309
|
+
)
|
|
310
|
+
const id = result.id || meta.id || meta.decision_id || 'unknown'
|
|
311
|
+
|
|
312
|
+
return `${index}. [${category}] ${summary}\n Project: ${project} | ${timeAgo}\n ID: ${id}`
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Layer 1: Full compact response with header, compact items, and footer
|
|
317
|
+
*/
|
|
318
|
+
export function formatCompactResponse(results: any[], query: string): string {
|
|
319
|
+
if (results.length === 0) {
|
|
320
|
+
return `No memories found for "${query}".`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const header = `Found ${results.length} relevant ${results.length === 1 ? 'memory' : 'memories'} for "${query}":`
|
|
324
|
+
const items = results.map((r, i) => formatCompactResult(r, i + 1)).join('\n\n')
|
|
325
|
+
const footer = `\nUse brain("details {ID}") for full context.`
|
|
326
|
+
|
|
327
|
+
return header + '\n\n' + items + footer
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Layer 2: Full detail view for a single observation/memory
|
|
332
|
+
*/
|
|
333
|
+
export function formatDetailResponse(observation: any): string {
|
|
334
|
+
const category = observation.category || observation.type || 'Memory'
|
|
335
|
+
const content = observation.content || observation.text || observation.decision || ''
|
|
336
|
+
const context = observation.context || observation.metadata?.context || ''
|
|
337
|
+
const reasoning = observation.reasoning || observation.metadata?.reasoning || ''
|
|
338
|
+
const tags = observation.tags || observation.metadata?.tags || []
|
|
339
|
+
const created = observation.createdAt || observation.created_at || observation.timestamp || ''
|
|
340
|
+
|
|
341
|
+
const lines: string[] = []
|
|
342
|
+
lines.push(`${category.charAt(0).toUpperCase() + category.slice(1)}: ${content}`)
|
|
343
|
+
if (context) lines.push(`\nContext: ${context}`)
|
|
344
|
+
if (reasoning) lines.push(`Reasoning: ${reasoning}`)
|
|
345
|
+
if (tags.length > 0) lines.push(`Tags: ${Array.isArray(tags) ? tags.join(', ') : tags}`)
|
|
346
|
+
if (created) lines.push(`Created: ${created}`)
|
|
347
|
+
|
|
348
|
+
return lines.join('\n')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Layer 3: Group observations by day for timeline view
|
|
353
|
+
*/
|
|
354
|
+
export function groupByDay(observations: any[]): Map<string, any[]> {
|
|
355
|
+
const groups = new Map<string, any[]>()
|
|
356
|
+
for (const obs of observations) {
|
|
357
|
+
const date = new Date(obs.createdAt || obs.created_at || obs.timestamp || Date.now())
|
|
358
|
+
const dayKey = isNaN(date.getTime())
|
|
359
|
+
? 'Unknown'
|
|
360
|
+
: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
361
|
+
if (!groups.has(dayKey)) groups.set(dayKey, [])
|
|
362
|
+
groups.get(dayKey)!.push(obs)
|
|
363
|
+
}
|
|
364
|
+
return groups
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Layer 3: Format a timeline from day-grouped observations
|
|
369
|
+
*/
|
|
370
|
+
export function formatTimeline(grouped: Map<string, any[]>, project?: string): string {
|
|
371
|
+
if (grouped.size === 0) {
|
|
372
|
+
return project ? `No activity found for "${project}".` : 'No recent activity found.'
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const header = project ? `Timeline for ${project}:` : 'Recent timeline:'
|
|
376
|
+
const sections: string[] = [header, '']
|
|
377
|
+
|
|
378
|
+
for (const [day, observations] of grouped) {
|
|
379
|
+
sections.push(`${day}:`)
|
|
380
|
+
for (const obs of observations) {
|
|
381
|
+
const category = obs.category || obs.type || 'memory'
|
|
382
|
+
const summary = truncate(obs.content || obs.text || obs.decision || '', 60)
|
|
383
|
+
sections.push(` - [${category}] ${summary}`)
|
|
384
|
+
}
|
|
385
|
+
sections.push('')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return sections.join('\n').trim()
|
|
389
|
+
}
|