claude-brain 0.5.0 → 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/chroma.ts +53 -17
- 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/client.ts +1 -1
- package/src/memory/chroma/index.ts +1 -1
- package/src/memory/chroma/store.ts +29 -9
- 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,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
|
+
}
|
|
@@ -6,6 +6,25 @@ import type { DecisionMetadata, MemoryMetadata } from './schemas'
|
|
|
6
6
|
import type { EmbeddingProvider } from './embeddings'
|
|
7
7
|
import type { SearchResult } from './search'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize metadata for ChromaDB v3.x compatibility.
|
|
11
|
+
* Strips undefined/null values (ChromaDB only accepts string, number, boolean).
|
|
12
|
+
*/
|
|
13
|
+
function sanitizeMetadata(metadata: Record<string, any>): Record<string, string | number | boolean> {
|
|
14
|
+
const clean: Record<string, string | number | boolean> = {}
|
|
15
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
16
|
+
if (value === undefined || value === null) continue
|
|
17
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
18
|
+
clean[key] = value
|
|
19
|
+
} else if (Array.isArray(value)) {
|
|
20
|
+
clean[key] = JSON.stringify(value)
|
|
21
|
+
} else {
|
|
22
|
+
clean[key] = String(value)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return clean
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
export interface StoreDecisionInput {
|
|
10
29
|
project: string
|
|
11
30
|
context: string
|
|
@@ -145,7 +164,7 @@ export class ChromaMemoryStore {
|
|
|
145
164
|
await collection.add({
|
|
146
165
|
ids: [id],
|
|
147
166
|
documents: [input.decision],
|
|
148
|
-
metadatas: [metadata as Record<string, any>],
|
|
167
|
+
metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
|
|
149
168
|
...(embeddings ? { embeddings } : {})
|
|
150
169
|
})
|
|
151
170
|
|
|
@@ -166,7 +185,7 @@ export class ChromaMemoryStore {
|
|
|
166
185
|
await memoriesCollection.add({
|
|
167
186
|
ids: [id], // Use same ID for cross-reference
|
|
168
187
|
documents: [memoryContent],
|
|
169
|
-
metadatas: [memoryMetadata],
|
|
188
|
+
metadatas: [sanitizeMetadata(memoryMetadata)],
|
|
170
189
|
...(embeddings ? { embeddings } : {})
|
|
171
190
|
})
|
|
172
191
|
|
|
@@ -204,6 +223,7 @@ export class ChromaMemoryStore {
|
|
|
204
223
|
example?: string
|
|
205
224
|
confidence: number
|
|
206
225
|
context?: string
|
|
226
|
+
source?: string
|
|
207
227
|
}): Promise<string> {
|
|
208
228
|
const id = randomUUID()
|
|
209
229
|
const now = new Date().toISOString()
|
|
@@ -217,7 +237,7 @@ export class ChromaMemoryStore {
|
|
|
217
237
|
context: input.context || '',
|
|
218
238
|
created_at: now,
|
|
219
239
|
updated_at: now,
|
|
220
|
-
source: 'manual'
|
|
240
|
+
source: input.source || 'manual'
|
|
221
241
|
}
|
|
222
242
|
|
|
223
243
|
try {
|
|
@@ -230,7 +250,7 @@ export class ChromaMemoryStore {
|
|
|
230
250
|
await collection.add({
|
|
231
251
|
ids: [id],
|
|
232
252
|
documents: [input.description],
|
|
233
|
-
metadatas: [metadata],
|
|
253
|
+
metadatas: [sanitizeMetadata(metadata)],
|
|
234
254
|
...(embeddings ? { embeddings } : {})
|
|
235
255
|
})
|
|
236
256
|
|
|
@@ -252,7 +272,7 @@ export class ChromaMemoryStore {
|
|
|
252
272
|
await memoriesCollection.add({
|
|
253
273
|
ids: [id],
|
|
254
274
|
documents: [memoryContent],
|
|
255
|
-
metadatas: [memoryMetadata],
|
|
275
|
+
metadatas: [sanitizeMetadata(memoryMetadata)],
|
|
256
276
|
...(embeddings ? { embeddings } : {})
|
|
257
277
|
})
|
|
258
278
|
|
|
@@ -304,7 +324,7 @@ export class ChromaMemoryStore {
|
|
|
304
324
|
await collection.add({
|
|
305
325
|
ids: [id],
|
|
306
326
|
documents: [input.correction],
|
|
307
|
-
metadatas: [metadata],
|
|
327
|
+
metadatas: [sanitizeMetadata(metadata)],
|
|
308
328
|
...(embeddings ? { embeddings } : {})
|
|
309
329
|
})
|
|
310
330
|
|
|
@@ -325,7 +345,7 @@ export class ChromaMemoryStore {
|
|
|
325
345
|
await memoriesCollection.add({
|
|
326
346
|
ids: [id],
|
|
327
347
|
documents: [memoryContent],
|
|
328
|
-
metadatas: [memoryMetadata],
|
|
348
|
+
metadatas: [sanitizeMetadata(memoryMetadata)],
|
|
329
349
|
...(embeddings ? { embeddings } : {})
|
|
330
350
|
})
|
|
331
351
|
|
|
@@ -367,7 +387,7 @@ export class ChromaMemoryStore {
|
|
|
367
387
|
await collection.add({
|
|
368
388
|
ids: [id],
|
|
369
389
|
documents: [input.content],
|
|
370
|
-
metadatas: [metadata as Record<string, any>],
|
|
390
|
+
metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
|
|
371
391
|
...(embeddings ? { embeddings } : {})
|
|
372
392
|
})
|
|
373
393
|
|
|
@@ -409,7 +429,7 @@ export class ChromaMemoryStore {
|
|
|
409
429
|
await collection.upsert({
|
|
410
430
|
ids: [id],
|
|
411
431
|
documents: [input.decision],
|
|
412
|
-
metadatas: [metadata as Record<string, any>],
|
|
432
|
+
metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
|
|
413
433
|
...(embeddings ? { embeddings } : {})
|
|
414
434
|
})
|
|
415
435
|
|
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
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18: Pack Manager
|
|
3
|
+
* Handles discovery, loading, and manifest tracking for knowledge packs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
import type { Logger } from 'pino'
|
|
9
|
+
import type { PacksConfig } from '@/config/schema'
|
|
10
|
+
import { KnowledgePackSchema, type KnowledgePack, type PackManifest } from './types'
|
|
11
|
+
|
|
12
|
+
/** Maps tech stack names to relevant pack IDs */
|
|
13
|
+
const STACK_MAP: Record<string, string[]> = {
|
|
14
|
+
// Languages
|
|
15
|
+
typescript: ['core/typescript'],
|
|
16
|
+
javascript: ['core/javascript'],
|
|
17
|
+
|
|
18
|
+
// Frontend frameworks
|
|
19
|
+
react: ['frontend/react'],
|
|
20
|
+
vue: ['frontend/react'], // reuse performance patterns; will add vue pack later
|
|
21
|
+
angular: ['frontend/react'],
|
|
22
|
+
svelte: ['frontend/react'],
|
|
23
|
+
'next.js': ['frontend/react'],
|
|
24
|
+
nuxt: ['frontend/react'],
|
|
25
|
+
remix: ['frontend/react'],
|
|
26
|
+
gatsby: ['frontend/react'],
|
|
27
|
+
|
|
28
|
+
// Backend
|
|
29
|
+
node: ['backend/node'],
|
|
30
|
+
express: ['backend/node'],
|
|
31
|
+
fastify: ['backend/node'],
|
|
32
|
+
hono: ['backend/node'],
|
|
33
|
+
elysia: ['backend/node'],
|
|
34
|
+
nestjs: ['backend/node'],
|
|
35
|
+
bun: ['backend/node'],
|
|
36
|
+
|
|
37
|
+
// Meta
|
|
38
|
+
jest: ['meta/testing'],
|
|
39
|
+
vitest: ['meta/testing'],
|
|
40
|
+
mocha: ['meta/testing'],
|
|
41
|
+
'bun:test': ['meta/testing'],
|
|
42
|
+
testing: ['meta/testing']
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class PackManager {
|
|
46
|
+
private logger: Logger
|
|
47
|
+
private config: PacksConfig
|
|
48
|
+
private packageRoot: string
|
|
49
|
+
private dataDir: string
|
|
50
|
+
|
|
51
|
+
constructor(logger: Logger, config: PacksConfig, packageRoot: string, dataDir: string) {
|
|
52
|
+
this.logger = logger.child({ component: 'pack-manager' })
|
|
53
|
+
this.config = config
|
|
54
|
+
this.packageRoot = packageRoot
|
|
55
|
+
this.dataDir = dataDir
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Find pack IDs relevant to a given tech stack */
|
|
59
|
+
findRelevantPacks(techStack: string[]): string[] {
|
|
60
|
+
const packIds = new Set<string>()
|
|
61
|
+
|
|
62
|
+
// Always include core and meta packs when configured
|
|
63
|
+
if (this.config.alwaysLoadCore) {
|
|
64
|
+
packIds.add('core/typescript')
|
|
65
|
+
packIds.add('core/javascript')
|
|
66
|
+
}
|
|
67
|
+
if (this.config.alwaysLoadMeta) {
|
|
68
|
+
packIds.add('meta/testing')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add tech-stack specific packs
|
|
72
|
+
for (const tech of techStack) {
|
|
73
|
+
const normalizedTech = tech.toLowerCase()
|
|
74
|
+
const mapped = STACK_MAP[normalizedTech]
|
|
75
|
+
if (mapped) {
|
|
76
|
+
for (const packId of mapped) {
|
|
77
|
+
packIds.add(packId)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Array.from(packIds)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Load and validate a pack from the packs directory */
|
|
86
|
+
async loadPack(packId: string): Promise<KnowledgePack> {
|
|
87
|
+
const packPath = path.join(this.packageRoot, this.config.packsDir, `${packId}.json`)
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const content = await fs.readFile(packPath, 'utf-8')
|
|
91
|
+
const raw = JSON.parse(content)
|
|
92
|
+
const pack = KnowledgePackSchema.parse(raw)
|
|
93
|
+
this.logger.debug({ packId, entries: pack.entries.length }, 'Pack loaded and validated')
|
|
94
|
+
return pack
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logger.error({ error, packId, packPath }, 'Failed to load pack')
|
|
97
|
+
throw new Error(`Failed to load pack "${packId}": ${error instanceof Error ? error.message : String(error)}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Get the manifest for a project (tracks which packs are loaded) */
|
|
102
|
+
async getManifest(project: string): Promise<PackManifest> {
|
|
103
|
+
const manifestPath = this.getManifestPath(project)
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const content = await fs.readFile(manifestPath, 'utf-8')
|
|
107
|
+
return JSON.parse(content) as PackManifest
|
|
108
|
+
} catch {
|
|
109
|
+
// No manifest yet — return empty
|
|
110
|
+
return {
|
|
111
|
+
project,
|
|
112
|
+
packs: [],
|
|
113
|
+
lastUpdated: new Date().toISOString()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Save manifest after loading packs */
|
|
119
|
+
async saveManifest(manifest: PackManifest): Promise<void> {
|
|
120
|
+
const manifestPath = this.getManifestPath(manifest.project)
|
|
121
|
+
const manifestDir = path.dirname(manifestPath)
|
|
122
|
+
|
|
123
|
+
await fs.mkdir(manifestDir, { recursive: true })
|
|
124
|
+
manifest.lastUpdated = new Date().toISOString()
|
|
125
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8')
|
|
126
|
+
|
|
127
|
+
this.logger.debug({ project: manifest.project, packs: manifest.packs.length }, 'Manifest saved')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Delete manifest for a project (used by reload) */
|
|
131
|
+
async deleteManifest(project: string): Promise<void> {
|
|
132
|
+
const manifestPath = this.getManifestPath(project)
|
|
133
|
+
try {
|
|
134
|
+
await fs.unlink(manifestPath)
|
|
135
|
+
} catch {
|
|
136
|
+
// File didn't exist, that's fine
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Check if a pack version is already loaded */
|
|
141
|
+
isPackLoaded(manifest: PackManifest, packId: string, version: string): boolean {
|
|
142
|
+
return manifest.packs.some(p => p.packId === packId && p.version === version)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** List all available packs in the packs directory */
|
|
146
|
+
async listAvailablePacks(): Promise<Array<{ id: string; name: string; description: string; entries: number; version: string }>> {
|
|
147
|
+
const packsDir = path.join(this.packageRoot, this.config.packsDir)
|
|
148
|
+
const packs: Array<{ id: string; name: string; description: string; entries: number; version: string }> = []
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await this.scanPacksDir(packsDir, '', packs)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.logger.warn({ error, packsDir }, 'Failed to scan packs directory')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return packs
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async scanPacksDir(
|
|
160
|
+
dir: string,
|
|
161
|
+
prefix: string,
|
|
162
|
+
result: Array<{ id: string; name: string; description: string; entries: number; version: string }>
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
let entries: import('fs').Dirent[]
|
|
165
|
+
try {
|
|
166
|
+
entries = await fs.readdir(dir, { withFileTypes: true })
|
|
167
|
+
} catch {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
await this.scanPacksDir(
|
|
174
|
+
path.join(dir, entry.name),
|
|
175
|
+
prefix ? `${prefix}/${entry.name}` : entry.name,
|
|
176
|
+
result
|
|
177
|
+
)
|
|
178
|
+
} else if (entry.name.endsWith('.json')) {
|
|
179
|
+
const packId = prefix
|
|
180
|
+
? `${prefix}/${entry.name.replace('.json', '')}`
|
|
181
|
+
: entry.name.replace('.json', '')
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const content = await fs.readFile(path.join(dir, entry.name), 'utf-8')
|
|
185
|
+
const raw = JSON.parse(content)
|
|
186
|
+
const pack = KnowledgePackSchema.parse(raw)
|
|
187
|
+
result.push({
|
|
188
|
+
id: packId,
|
|
189
|
+
name: pack.name,
|
|
190
|
+
description: pack.description,
|
|
191
|
+
entries: pack.entries.length,
|
|
192
|
+
version: pack.version
|
|
193
|
+
})
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.logger.warn({ error, packId }, 'Skipping invalid pack file')
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private getManifestPath(project: string): string {
|
|
202
|
+
return path.join(this.dataDir, 'pack-manifests', `${project}.json`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18: Knowledge Ranker
|
|
3
|
+
* Post-processing layer for search results that boosts personal entries
|
|
4
|
+
* over community (pack) entries
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PacksConfig } from '@/config/schema'
|
|
8
|
+
|
|
9
|
+
export interface RankedResult {
|
|
10
|
+
id: string
|
|
11
|
+
content: string
|
|
12
|
+
metadata: Record<string, any>
|
|
13
|
+
similarity: number
|
|
14
|
+
adjustedScore: number
|
|
15
|
+
badge: 'personal' | 'community'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class KnowledgeRanker {
|
|
19
|
+
private personalBoost: number
|
|
20
|
+
private projectBoost: number
|
|
21
|
+
|
|
22
|
+
constructor(config: PacksConfig) {
|
|
23
|
+
this.personalBoost = config.personalBoost
|
|
24
|
+
this.projectBoost = config.projectBoost
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Adjust similarity scores based on entry source */
|
|
28
|
+
rank(
|
|
29
|
+
results: Array<{ id: string; content: string; metadata: Record<string, any>; similarity: number }>,
|
|
30
|
+
currentProject?: string
|
|
31
|
+
): RankedResult[] {
|
|
32
|
+
const ranked = results.map(result => {
|
|
33
|
+
const isCommunity = this.isCommunityEntry(result.metadata)
|
|
34
|
+
let adjustedScore = result.similarity
|
|
35
|
+
|
|
36
|
+
if (!isCommunity) {
|
|
37
|
+
// Personal entries get a boost
|
|
38
|
+
adjustedScore *= this.personalBoost
|
|
39
|
+
|
|
40
|
+
// Project-specific entries get an additional boost
|
|
41
|
+
if (currentProject && result.metadata.project === currentProject) {
|
|
42
|
+
adjustedScore *= this.projectBoost
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Community entries: no boost (effectively ranked lower)
|
|
46
|
+
|
|
47
|
+
// Cap at 1.0
|
|
48
|
+
adjustedScore = Math.min(adjustedScore, 1.0)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: result.id,
|
|
52
|
+
content: result.content,
|
|
53
|
+
metadata: result.metadata,
|
|
54
|
+
similarity: result.similarity,
|
|
55
|
+
adjustedScore,
|
|
56
|
+
badge: isCommunity ? 'community' as const : 'personal' as const
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Sort by adjusted score descending
|
|
61
|
+
return ranked.sort((a, b) => b.adjustedScore - a.adjustedScore)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Detect if a search result is from a community pack */
|
|
65
|
+
isCommunityEntry(metadata: Record<string, any>): boolean {
|
|
66
|
+
// Check source field
|
|
67
|
+
if (typeof metadata.source === 'string' && metadata.source.startsWith('pack:')) {
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check context field for community marker
|
|
72
|
+
if (typeof metadata.context === 'string' && metadata.context.includes('[community:pack:')) {
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
}
|