claude-brain 0.5.1 → 0.8.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/assets/CLAUDE-unified.md +11 -0
- package/package.json +2 -1
- package/packs/backend/node.json +173 -0
- package/packs/core/javascript.json +176 -0
- package/packs/core/typescript.json +222 -0
- package/packs/frontend/react.json +254 -0
- package/packs/meta/testing.json +172 -0
- package/src/cli/bin.ts +14 -0
- package/src/cli/commands/hooks.ts +214 -0
- package/src/cli/commands/pack.ts +197 -0
- package/src/cli/commands/serve.ts +34 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +85 -2
- package/src/hooks/brain-hook.ts +110 -0
- package/src/hooks/capture.ts +161 -0
- package/src/hooks/deduplicator.ts +72 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/installer.ts +181 -0
- package/src/hooks/passive-classifier.ts +366 -0
- package/src/hooks/queue.ts +122 -0
- package/src/hooks/session-tracker.ts +199 -0
- package/src/hooks/types.ts +47 -0
- package/src/memory/chroma/store.ts +2 -1
- package/src/memory/index.ts +1 -0
- package/src/memory/store.ts +1 -0
- package/src/packs/index.ts +9 -0
- package/src/packs/loader.ts +134 -0
- package/src/packs/manager.ts +204 -0
- package/src/packs/ranker.ts +78 -0
- package/src/packs/types.ts +81 -0
- package/src/routing/entity-extractor.ts +410 -0
- package/src/routing/intent-classifier.ts +229 -0
- package/src/routing/response-filter.ts +221 -0
- package/src/routing/router.ts +671 -0
- package/src/server/handlers/call-tool.ts +7 -0
- package/src/server/handlers/list-tools.ts +22 -5
- package/src/server/handlers/tools/brain.ts +85 -0
- package/src/server/handlers/tools/init-project.ts +47 -0
- package/src/server/handlers/tools/schemas.ts +12 -0
- package/src/server/http-api.ts +188 -0
- package/src/tools/registry.ts +9 -0
- package/src/tools/schemas.ts +33 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 17: File Queue
|
|
3
|
+
* JSONL-based offline fallback when HTTP API server is unreachable.
|
|
4
|
+
* Queue file: ~/.claude-brain/data/hook-queue.jsonl
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { appendFileSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
import { getHomePaths } from '@/config/home'
|
|
10
|
+
import type { CapturedKnowledge } from './types'
|
|
11
|
+
|
|
12
|
+
/** Get the queue file path */
|
|
13
|
+
export function getQueuePath(): string {
|
|
14
|
+
return join(getHomePaths().data, 'hook-queue.jsonl')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Append captured knowledge to the offline queue.
|
|
19
|
+
* Creates the file and parent directory if needed.
|
|
20
|
+
*/
|
|
21
|
+
export function appendToQueue(items: CapturedKnowledge[]): void {
|
|
22
|
+
if (items.length === 0) return
|
|
23
|
+
|
|
24
|
+
const queuePath = getQueuePath()
|
|
25
|
+
const dir = dirname(queuePath)
|
|
26
|
+
|
|
27
|
+
if (!existsSync(dir)) {
|
|
28
|
+
mkdirSync(dir, { recursive: true })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const lines = items.map(item => JSON.stringify(item)).join('\n') + '\n'
|
|
32
|
+
appendFileSync(queuePath, lines, 'utf-8')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read all items from the queue file.
|
|
37
|
+
* Returns empty array if file doesn't exist.
|
|
38
|
+
*/
|
|
39
|
+
export function readQueue(): CapturedKnowledge[] {
|
|
40
|
+
const queuePath = getQueuePath()
|
|
41
|
+
|
|
42
|
+
if (!existsSync(queuePath)) return []
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(queuePath, 'utf-8').trim()
|
|
46
|
+
if (!content) return []
|
|
47
|
+
|
|
48
|
+
return content
|
|
49
|
+
.split('\n')
|
|
50
|
+
.filter(line => line.trim())
|
|
51
|
+
.map(line => {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(line) as CapturedKnowledge
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.filter((item): item is CapturedKnowledge => item !== null)
|
|
59
|
+
} catch {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clear the queue file after successful processing.
|
|
66
|
+
*/
|
|
67
|
+
export function clearQueue(): void {
|
|
68
|
+
const queuePath = getQueuePath()
|
|
69
|
+
if (existsSync(queuePath)) {
|
|
70
|
+
writeFileSync(queuePath, '', 'utf-8')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Drain the queue by POSTing items to the ingest endpoint in batches.
|
|
76
|
+
* Called on server startup.
|
|
77
|
+
*
|
|
78
|
+
* @param port The HTTP API port
|
|
79
|
+
* @param batchSize Number of items per POST
|
|
80
|
+
* @returns Number of items drained
|
|
81
|
+
*/
|
|
82
|
+
export async function drainQueue(port: number = 3000, batchSize: number = 10): Promise<number> {
|
|
83
|
+
const items = readQueue()
|
|
84
|
+
if (items.length === 0) return 0
|
|
85
|
+
|
|
86
|
+
let drained = 0
|
|
87
|
+
const url = `http://localhost:${port}/api/hooks/ingest`
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
90
|
+
const batch = items.slice(i, i + batchSize)
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(url, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({ knowledge: batch }),
|
|
96
|
+
signal: AbortSignal.timeout(5000),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
drained += batch.length
|
|
101
|
+
} else {
|
|
102
|
+
// Stop draining on first failure
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Server not ready yet, stop
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (drained === items.length) {
|
|
112
|
+
clearQueue()
|
|
113
|
+
} else if (drained > 0) {
|
|
114
|
+
// Partially drained — rewrite queue with remaining items
|
|
115
|
+
const remaining = items.slice(drained)
|
|
116
|
+
const queuePath = getQueuePath()
|
|
117
|
+
const lines = remaining.map(item => JSON.stringify(item)).join('\n') + '\n'
|
|
118
|
+
writeFileSync(queuePath, lines, 'utf-8')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return drained
|
|
122
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 17: Session Tracker
|
|
3
|
+
* Server-side session tracking that maintains state across hook invocations.
|
|
4
|
+
* Buffers CapturedKnowledge items and generates session summaries on idle/stop.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Logger } from 'pino'
|
|
8
|
+
import type { EpisodeManager } from '@/memory/episodic/manager'
|
|
9
|
+
import { ExtractiveSummarizer } from '@/memory/episodic/summarizer'
|
|
10
|
+
import type { CapturedKnowledge } from './types'
|
|
11
|
+
import type { HooksConfig } from '@/config/schema'
|
|
12
|
+
|
|
13
|
+
interface SessionState {
|
|
14
|
+
sessionId: string
|
|
15
|
+
project?: string
|
|
16
|
+
items: CapturedKnowledge[]
|
|
17
|
+
startedAt: string
|
|
18
|
+
lastActivityAt: string
|
|
19
|
+
episodeId?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class HookSessionTracker {
|
|
23
|
+
private logger: Logger
|
|
24
|
+
private episodeManager: EpisodeManager | null
|
|
25
|
+
private summarizer: ExtractiveSummarizer
|
|
26
|
+
private sessions: Map<string, SessionState> = new Map()
|
|
27
|
+
private idleTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
28
|
+
private idleTimeoutMs: number
|
|
29
|
+
private minEventsForSummary: number
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
logger: Logger,
|
|
33
|
+
episodeManager: EpisodeManager | null,
|
|
34
|
+
config?: HooksConfig['sessions']
|
|
35
|
+
) {
|
|
36
|
+
this.logger = logger.child({ component: 'hook-session-tracker' })
|
|
37
|
+
this.episodeManager = episodeManager
|
|
38
|
+
this.summarizer = new ExtractiveSummarizer()
|
|
39
|
+
this.idleTimeoutMs = (config?.idleTimeoutMinutes ?? 30) * 60 * 1000
|
|
40
|
+
this.minEventsForSummary = config?.minEventsForSummary ?? 3
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Track a captured knowledge item within a session.
|
|
45
|
+
*/
|
|
46
|
+
async track(sessionId: string, knowledge: CapturedKnowledge): Promise<void> {
|
|
47
|
+
let session = this.sessions.get(sessionId)
|
|
48
|
+
|
|
49
|
+
if (!session) {
|
|
50
|
+
session = {
|
|
51
|
+
sessionId,
|
|
52
|
+
project: knowledge.project,
|
|
53
|
+
items: [],
|
|
54
|
+
startedAt: new Date().toISOString(),
|
|
55
|
+
lastActivityAt: new Date().toISOString(),
|
|
56
|
+
}
|
|
57
|
+
this.sessions.set(sessionId, session)
|
|
58
|
+
|
|
59
|
+
// Start episode if manager available
|
|
60
|
+
if (this.episodeManager) {
|
|
61
|
+
try {
|
|
62
|
+
const episode = await this.episodeManager.startEpisode(knowledge.project)
|
|
63
|
+
session.episodeId = episode.id
|
|
64
|
+
} catch (err) {
|
|
65
|
+
this.logger.warn({ err }, 'Failed to start episode for session')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
session.items.push(knowledge)
|
|
71
|
+
session.lastActivityAt = new Date().toISOString()
|
|
72
|
+
|
|
73
|
+
// Reset idle timer
|
|
74
|
+
this.resetIdleTimer(sessionId)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* End a session immediately (triggered by Stop hook event).
|
|
79
|
+
*/
|
|
80
|
+
async endSession(sessionId: string): Promise<void> {
|
|
81
|
+
const session = this.sessions.get(sessionId)
|
|
82
|
+
if (!session) return
|
|
83
|
+
|
|
84
|
+
this.clearIdleTimer(sessionId)
|
|
85
|
+
await this.summarizeAndPersist(session)
|
|
86
|
+
this.sessions.delete(sessionId)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* End all active sessions (for server shutdown).
|
|
91
|
+
*/
|
|
92
|
+
async endAllSessions(): Promise<void> {
|
|
93
|
+
const sessionIds = Array.from(this.sessions.keys())
|
|
94
|
+
for (const id of sessionIds) {
|
|
95
|
+
await this.endSession(id)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get stats about tracked sessions.
|
|
101
|
+
*/
|
|
102
|
+
getStats(): { activeSessions: number; totalItems: number } {
|
|
103
|
+
let totalItems = 0
|
|
104
|
+
for (const session of this.sessions.values()) {
|
|
105
|
+
totalItems += session.items.length
|
|
106
|
+
}
|
|
107
|
+
return { activeSessions: this.sessions.size, totalItems }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private resetIdleTimer(sessionId: string): void {
|
|
111
|
+
this.clearIdleTimer(sessionId)
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
this.onIdle(sessionId)
|
|
114
|
+
}, this.idleTimeoutMs)
|
|
115
|
+
this.idleTimers.set(sessionId, timer)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private clearIdleTimer(sessionId: string): void {
|
|
119
|
+
const existing = this.idleTimers.get(sessionId)
|
|
120
|
+
if (existing) {
|
|
121
|
+
clearTimeout(existing)
|
|
122
|
+
this.idleTimers.delete(sessionId)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async onIdle(sessionId: string): Promise<void> {
|
|
127
|
+
this.logger.debug({ sessionId }, 'Session idle timeout, summarizing')
|
|
128
|
+
await this.endSession(sessionId)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async summarizeAndPersist(session: SessionState): Promise<void> {
|
|
132
|
+
if (session.items.length < this.minEventsForSummary) {
|
|
133
|
+
this.logger.debug(
|
|
134
|
+
{ sessionId: session.sessionId, items: session.items.length },
|
|
135
|
+
'Too few items for summary, skipping'
|
|
136
|
+
)
|
|
137
|
+
// Still end the episode if started
|
|
138
|
+
if (session.episodeId && this.episodeManager) {
|
|
139
|
+
try {
|
|
140
|
+
await this.episodeManager.endEpisode(session.episodeId)
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.logger.info(
|
|
147
|
+
{ sessionId: session.sessionId, items: session.items.length, project: session.project },
|
|
148
|
+
'Summarizing session'
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Build a synthetic episode for the summarizer
|
|
152
|
+
const messages = session.items.map((item) => ({
|
|
153
|
+
role: 'assistant' as const,
|
|
154
|
+
content: `[${item.type}] ${item.content}`,
|
|
155
|
+
timestamp: item.timestamp,
|
|
156
|
+
token_estimate: Math.ceil(item.content.length / 4),
|
|
157
|
+
}))
|
|
158
|
+
|
|
159
|
+
const syntheticEpisode = {
|
|
160
|
+
id: session.sessionId,
|
|
161
|
+
project: session.project,
|
|
162
|
+
status: 'completed' as const,
|
|
163
|
+
started_at: session.startedAt,
|
|
164
|
+
ended_at: new Date().toISOString(),
|
|
165
|
+
messages,
|
|
166
|
+
related_decisions: session.items
|
|
167
|
+
.filter(i => i.type === 'decision')
|
|
168
|
+
.map((_, idx) => `hook-decision-${idx}`),
|
|
169
|
+
related_patterns: session.items
|
|
170
|
+
.filter(i => i.type === 'pattern')
|
|
171
|
+
.map((_, idx) => `hook-pattern-${idx}`),
|
|
172
|
+
related_corrections: session.items
|
|
173
|
+
.filter(i => i.type === 'correction')
|
|
174
|
+
.map((_, idx) => `hook-correction-${idx}`),
|
|
175
|
+
token_count: messages.reduce((sum, m) => sum + (m.token_estimate || 0), 0),
|
|
176
|
+
message_count: messages.length,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const summary = this.summarizer.summarize(syntheticEpisode)
|
|
180
|
+
|
|
181
|
+
// End the episode with summary attached
|
|
182
|
+
if (session.episodeId && this.episodeManager) {
|
|
183
|
+
try {
|
|
184
|
+
// Add messages to episode before ending
|
|
185
|
+
for (const msg of messages) {
|
|
186
|
+
await this.episodeManager.addMessage(session.episodeId, msg)
|
|
187
|
+
}
|
|
188
|
+
await this.episodeManager.endEpisode(session.episodeId)
|
|
189
|
+
} catch (err) {
|
|
190
|
+
this.logger.warn({ err, sessionId: session.sessionId }, 'Failed to end episode')
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.logger.info(
|
|
195
|
+
{ sessionId: session.sessionId, summary: summary.brief },
|
|
196
|
+
'Session summarized'
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 17: Passive Learning via Hooks — Shared Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Claude Code hook stdin JSON format */
|
|
6
|
+
export interface HookInput {
|
|
7
|
+
session_id: string
|
|
8
|
+
hook_event_name: 'PostToolUse' | 'Stop' | 'PreToolUse'
|
|
9
|
+
cwd: string
|
|
10
|
+
tool_name?: string
|
|
11
|
+
tool_input?: Record<string, any>
|
|
12
|
+
tool_response?: {
|
|
13
|
+
content?: string | Array<{ type: string; text?: string }>
|
|
14
|
+
[key: string]: any
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Knowledge type classifications */
|
|
19
|
+
export type KnowledgeType = 'decision' | 'pattern' | 'correction' | 'progress'
|
|
20
|
+
|
|
21
|
+
/** A piece of knowledge captured from a hook event */
|
|
22
|
+
export interface CapturedKnowledge {
|
|
23
|
+
type: KnowledgeType
|
|
24
|
+
confidence: number
|
|
25
|
+
content: string
|
|
26
|
+
project?: string
|
|
27
|
+
technologies: string[]
|
|
28
|
+
metadata: Record<string, any>
|
|
29
|
+
source: 'hook-passive'
|
|
30
|
+
timestamp: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What to do with captured knowledge before storage */
|
|
34
|
+
export type StoreAction =
|
|
35
|
+
| { action: 'store_new' }
|
|
36
|
+
| { action: 'merge'; existingId: string; mergedContent: string }
|
|
37
|
+
| { action: 'skip'; reason: string }
|
|
38
|
+
|
|
39
|
+
/** Hook event stats for status reporting */
|
|
40
|
+
export interface HookStats {
|
|
41
|
+
totalCaptured: number
|
|
42
|
+
totalSkipped: number
|
|
43
|
+
totalMerged: number
|
|
44
|
+
sessionsTracked: number
|
|
45
|
+
lastCaptureAt?: string
|
|
46
|
+
queueSize: number
|
|
47
|
+
}
|
|
@@ -223,6 +223,7 @@ export class ChromaMemoryStore {
|
|
|
223
223
|
example?: string
|
|
224
224
|
confidence: number
|
|
225
225
|
context?: string
|
|
226
|
+
source?: string
|
|
226
227
|
}): Promise<string> {
|
|
227
228
|
const id = randomUUID()
|
|
228
229
|
const now = new Date().toISOString()
|
|
@@ -236,7 +237,7 @@ export class ChromaMemoryStore {
|
|
|
236
237
|
context: input.context || '',
|
|
237
238
|
created_at: now,
|
|
238
239
|
updated_at: now,
|
|
239
|
-
source: 'manual'
|
|
240
|
+
source: input.source || 'manual'
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
try {
|
package/src/memory/index.ts
CHANGED
package/src/memory/store.ts
CHANGED
|
@@ -336,6 +336,7 @@ export class MemoryStore {
|
|
|
336
336
|
example?: string
|
|
337
337
|
confidence: number
|
|
338
338
|
context?: string
|
|
339
|
+
source?: string
|
|
339
340
|
}): Promise<string> {
|
|
340
341
|
try {
|
|
341
342
|
const content = `Pattern (${input.pattern_type}): ${input.description}${input.context ? `\nContext: ${input.context}` : ''}${input.example ? `\nExample: ${input.example}` : ''}`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18: Knowledge Packs
|
|
3
|
+
* Pre-seeded knowledge for zero cold-start experience
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './types'
|
|
7
|
+
export { PackManager } from './manager'
|
|
8
|
+
export { PackLoader } from './loader'
|
|
9
|
+
export { KnowledgeRanker, type RankedResult } from './ranker'
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18: Pack Loader
|
|
3
|
+
* Orchestrates loading pack entries into the memory system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from 'pino'
|
|
7
|
+
import type { MemoryManager } from '@/memory/index'
|
|
8
|
+
import type { PackManager } from './manager'
|
|
9
|
+
import type { PacksConfig } from '@/config/schema'
|
|
10
|
+
import { ENTRY_TYPE_TO_PATTERN_TYPE, type PackEntry, type PackLoadResult } from './types'
|
|
11
|
+
|
|
12
|
+
export class PackLoader {
|
|
13
|
+
private logger: Logger
|
|
14
|
+
private memory: MemoryManager
|
|
15
|
+
private packManager: PackManager
|
|
16
|
+
private config: PacksConfig
|
|
17
|
+
|
|
18
|
+
constructor(logger: Logger, memory: MemoryManager, packManager: PackManager, config: PacksConfig) {
|
|
19
|
+
this.logger = logger.child({ component: 'pack-loader' })
|
|
20
|
+
this.memory = memory
|
|
21
|
+
this.packManager = packManager
|
|
22
|
+
this.config = config
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Main entry point: load all relevant packs for a project */
|
|
26
|
+
async loadPacksForProject(project: string, techStack: string[]): Promise<PackLoadResult> {
|
|
27
|
+
const result: PackLoadResult = {
|
|
28
|
+
packsLoaded: 0,
|
|
29
|
+
entriesLoaded: 0,
|
|
30
|
+
packDetails: [],
|
|
31
|
+
skipped: []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const relevantPackIds = this.packManager.findRelevantPacks(techStack)
|
|
35
|
+
this.logger.info({ project, techStack, relevantPackIds }, 'Loading packs for project')
|
|
36
|
+
|
|
37
|
+
const manifest = await this.packManager.getManifest(project)
|
|
38
|
+
|
|
39
|
+
for (const packId of relevantPackIds) {
|
|
40
|
+
try {
|
|
41
|
+
const pack = await this.packManager.loadPack(packId)
|
|
42
|
+
|
|
43
|
+
// Idempotency check
|
|
44
|
+
if (this.packManager.isPackLoaded(manifest, packId, pack.version)) {
|
|
45
|
+
result.skipped.push({ packId, reason: `Already loaded (v${pack.version})` })
|
|
46
|
+
this.logger.debug({ packId, version: pack.version }, 'Pack already loaded, skipping')
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Load each entry
|
|
51
|
+
let entriesLoaded = 0
|
|
52
|
+
for (const entry of pack.entries) {
|
|
53
|
+
try {
|
|
54
|
+
await this.storeEntry(project, packId, entry)
|
|
55
|
+
entriesLoaded++
|
|
56
|
+
} catch (error) {
|
|
57
|
+
this.logger.warn({ error, packId, entry: entry.title }, 'Failed to store pack entry')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Update manifest
|
|
62
|
+
manifest.packs.push({
|
|
63
|
+
packId,
|
|
64
|
+
version: pack.version,
|
|
65
|
+
entriesLoaded,
|
|
66
|
+
loadedAt: new Date().toISOString()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
result.packsLoaded++
|
|
70
|
+
result.entriesLoaded += entriesLoaded
|
|
71
|
+
result.packDetails.push({
|
|
72
|
+
packId,
|
|
73
|
+
name: pack.name,
|
|
74
|
+
entriesLoaded
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
this.logger.info({ packId, entriesLoaded }, 'Pack loaded successfully')
|
|
78
|
+
} catch (error) {
|
|
79
|
+
result.skipped.push({
|
|
80
|
+
packId,
|
|
81
|
+
reason: `Load failed: ${error instanceof Error ? error.message : String(error)}`
|
|
82
|
+
})
|
|
83
|
+
this.logger.warn({ error, packId }, 'Failed to load pack')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Save updated manifest
|
|
88
|
+
await this.packManager.saveManifest(manifest)
|
|
89
|
+
|
|
90
|
+
this.logger.info({
|
|
91
|
+
project,
|
|
92
|
+
packsLoaded: result.packsLoaded,
|
|
93
|
+
entriesLoaded: result.entriesLoaded,
|
|
94
|
+
skipped: result.skipped.length
|
|
95
|
+
}, 'Pack loading complete')
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Route a single pack entry to the appropriate storage method */
|
|
101
|
+
private async storeEntry(project: string, packId: string, entry: PackEntry): Promise<void> {
|
|
102
|
+
const dampenedConfidence = entry.confidence * this.config.communityConfidenceMultiplier
|
|
103
|
+
|
|
104
|
+
if (entry.type === 'decision-template') {
|
|
105
|
+
// Decision templates go to rememberDecision
|
|
106
|
+
await this.memory.rememberDecision(
|
|
107
|
+
project,
|
|
108
|
+
`[community:pack:${packId}] ${entry.category}`,
|
|
109
|
+
`${entry.title}: ${entry.content}`,
|
|
110
|
+
`Pre-seeded from knowledge pack: ${packId}`,
|
|
111
|
+
{
|
|
112
|
+
tags: ['pack', `pack:${packId}`, ...entry.tags]
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
} else {
|
|
116
|
+
// All other types go to storePattern
|
|
117
|
+
const patternType = ENTRY_TYPE_TO_PATTERN_TYPE[entry.type]
|
|
118
|
+
if (!patternType) {
|
|
119
|
+
this.logger.warn({ type: entry.type, packId }, 'Unknown entry type, skipping')
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await this.memory.storePattern({
|
|
124
|
+
project,
|
|
125
|
+
pattern_type: patternType,
|
|
126
|
+
description: `${entry.title}: ${entry.content}`,
|
|
127
|
+
example: entry.example,
|
|
128
|
+
confidence: dampenedConfidence,
|
|
129
|
+
context: `[community:pack:${packId}] ${entry.category}`,
|
|
130
|
+
source: `pack:${packId}`
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|