claude-brain 0.14.2 → 0.14.4
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/README.md +191 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +11 -11
- package/bunfig.toml +8 -8
- package/package.json +80 -80
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/src/automation/auto-context.ts +240 -240
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/index.ts +11 -11
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +205 -205
- package/src/cli/auto-setup.ts +82 -82
- package/src/cli/bin.ts +202 -202
- package/src/cli/commands/chroma.ts +573 -573
- package/src/cli/commands/git-hook.ts +189 -189
- package/src/cli/commands/hooks.ts +213 -213
- package/src/cli/commands/init.ts +122 -122
- package/src/cli/commands/install-mcp.ts +92 -92
- package/src/cli/commands/pack.ts +197 -197
- package/src/cli/commands/serve.ts +167 -167
- package/src/cli/commands/start.ts +42 -42
- package/src/cli/commands/uninstall-mcp.ts +41 -41
- package/src/cli/commands/update.ts +121 -121
- package/src/cli/diagnose.ts +4 -4
- package/src/cli/health-check.ts +4 -4
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/setup.ts +4 -4
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/config/defaults.ts +50 -50
- package/src/config/home.ts +55 -55
- package/src/config/index.ts +7 -7
- package/src/config/loader.ts +166 -166
- package/src/config/migration.ts +76 -76
- package/src/config/schema.ts +360 -360
- package/src/config/validator.ts +184 -184
- package/src/config/watcher.ts +86 -86
- package/src/context/assembler.ts +398 -398
- package/src/context/cache-manager.ts +101 -101
- package/src/context/formatter.ts +84 -84
- package/src/context/hierarchy.ts +85 -85
- package/src/context/index.ts +83 -83
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/types.ts +252 -252
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +123 -123
- package/src/health/index.ts +229 -229
- package/src/hooks/brain-hook.ts +112 -112
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +207 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +191 -194
- package/src/hooks/passive-classifier.ts +366 -366
- package/src/hooks/queue.ts +129 -129
- package/src/hooks/session-tracker.ts +275 -275
- package/src/hooks/types.ts +47 -47
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/affinity.ts +162 -162
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +13 -13
- package/src/intelligence/cross-project/transfer.ts +201 -201
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +207 -207
- package/src/intelligence/prediction/context-anticipator.ts +198 -198
- package/src/intelligence/prediction/decision-predictor.ts +184 -184
- package/src/intelligence/prediction/index.ts +13 -13
- package/src/intelligence/prediction/recommender.ts +268 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +247 -247
- package/src/intelligence/reasoning/counterfactual.ts +248 -248
- package/src/intelligence/reasoning/index.ts +13 -13
- package/src/intelligence/reasoning/synthesizer.ts +169 -169
- package/src/intelligence/temporal/evolution.ts +197 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +259 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/knowledge/entity-extractor.ts +416 -416
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +168 -168
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +174 -174
- package/src/memory/chroma/collection-manager.ts +94 -94
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +153 -153
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +315 -315
- package/src/memory/chroma/store.ts +741 -741
- package/src/memory/consolidation/archiver.ts +164 -164
- package/src/memory/consolidation/merger.ts +186 -186
- package/src/memory/consolidation/scorer.ts +138 -138
- package/src/memory/context-builder.ts +236 -236
- package/src/memory/database.ts +169 -169
- package/src/memory/embedding-utils.ts +156 -156
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +351 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/index.ts +582 -582
- package/src/memory/knowledge-extractor.ts +455 -455
- package/src/memory/learning.ts +378 -378
- package/src/memory/patterns.ts +396 -396
- package/src/memory/schema.ts +88 -88
- package/src/memory/search.ts +309 -309
- package/src/memory/store.ts +787 -787
- package/src/memory/types.ts +121 -121
- package/src/orchestrator/coordinator.ts +272 -272
- package/src/orchestrator/decision-logger.ts +228 -228
- package/src/orchestrator/event-emitter.ts +198 -198
- package/src/orchestrator/event-queue.ts +184 -184
- package/src/orchestrator/handlers/base-handler.ts +70 -70
- package/src/orchestrator/handlers/context-handler.ts +73 -73
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/orchestrator/handlers/index.ts +10 -10
- package/src/orchestrator/handlers/status-handler.ts +131 -131
- package/src/orchestrator/handlers/task-handler.ts +171 -171
- package/src/orchestrator/index.ts +275 -275
- package/src/orchestrator/task-parser.ts +284 -284
- package/src/orchestrator/types.ts +98 -98
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -300
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +223 -223
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +223 -223
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +163 -163
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +198 -198
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +236 -236
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +188 -188
- package/src/retrieval/reranker/model.ts +95 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +428 -428
- package/src/routing/intent-classifier.ts +436 -436
- package/src/routing/response-filter.ts +258 -254
- package/src/routing/router.ts +1322 -1314
- package/src/routing/search-engine.ts +475 -475
- package/src/routing/types.ts +94 -84
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/handlers/call-tool.ts +156 -156
- package/src/server/handlers/index.ts +9 -9
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/analyze-decision-evolution.ts +151 -151
- package/src/server/handlers/tools/auto-remember.ts +200 -200
- package/src/server/handlers/tools/brain.ts +85 -85
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/detect-trends.ts +144 -144
- package/src/server/handlers/tools/find-cross-project-patterns.ts +168 -168
- package/src/server/handlers/tools/get-activity-log.ts +194 -194
- package/src/server/handlers/tools/get-code-standards.ts +124 -124
- package/src/server/handlers/tools/get-corrections.ts +154 -154
- package/src/server/handlers/tools/get-decision-timeline.ts +172 -172
- package/src/server/handlers/tools/get-episode.ts +103 -103
- package/src/server/handlers/tools/get-patterns.ts +158 -158
- package/src/server/handlers/tools/get-phase12-status.ts +63 -63
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/get-recommendations.ts +145 -145
- package/src/server/handlers/tools/index.ts +31 -31
- package/src/server/handlers/tools/init-project.ts +757 -757
- package/src/server/handlers/tools/list-episodes.ts +90 -90
- package/src/server/handlers/tools/list-projects.ts +125 -125
- package/src/server/handlers/tools/rate-memory.ts +101 -101
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +126 -126
- package/src/server/handlers/tools/record-correction.ts +125 -125
- package/src/server/handlers/tools/remember-decision.ts +153 -153
- package/src/server/handlers/tools/schemas.ts +253 -253
- package/src/server/handlers/tools/search-knowledge-graph.ts +102 -102
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/handlers/tools/what-if-analysis.ts +135 -135
- package/src/server/http-api.ts +693 -693
- package/src/server/index.ts +40 -40
- package/src/server/mcp-server.ts +283 -283
- package/src/server/providers/index.ts +7 -7
- package/src/server/providers/prompts.ts +327 -327
- package/src/server/providers/resources.ts +622 -622
- package/src/server/services.ts +468 -468
- package/src/server/types.ts +39 -39
- package/src/server/utils/error-handler.ts +155 -155
- package/src/server/utils/index.ts +13 -13
- package/src/server/utils/memory-indicator.ts +83 -83
- package/src/server/utils/request-context.ts +122 -122
- package/src/server/utils/response-formatter.ts +129 -124
- package/src/server/utils/validators.ts +210 -210
- package/src/setup/index.ts +48 -48
- package/src/setup/wizard.ts +461 -461
- package/src/tools/index.ts +24 -24
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.test.ts +30 -30
- package/src/tools/schemas.ts +617 -617
- package/src/tools/types.ts +412 -412
- package/src/utils/circuit-breaker.ts +130 -130
- package/src/utils/cleanup.ts +34 -34
- package/src/utils/error-handler.ts +132 -132
- package/src/utils/error-messages.ts +60 -60
- package/src/utils/fallback.ts +45 -45
- package/src/utils/index.ts +54 -54
- package/src/utils/logger-utils.ts +80 -80
- package/src/utils/logger.ts +88 -88
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/retry.ts +94 -94
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/frontmatter.ts +264 -264
- package/src/vault/index.ts +318 -318
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +422 -422
- package/src/vault/reader.ts +264 -264
- package/src/vault/templates.ts +186 -186
- package/src/vault/types.ts +73 -73
- package/src/vault/watcher.ts +277 -277
- package/src/vault/writer.ts +413 -413
- package/tsconfig.json +30 -30
package/src/vault/reader.ts
CHANGED
|
@@ -1,264 +1,264 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vault File Reader
|
|
3
|
-
* Safe, efficient file reading utilities with caching and frontmatter parsing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from 'fs/promises'
|
|
7
|
-
import path from 'path'
|
|
8
|
-
import matter from 'gray-matter'
|
|
9
|
-
import type { Logger } from 'pino'
|
|
10
|
-
import type { MarkdownFile, Frontmatter } from './types'
|
|
11
|
-
|
|
12
|
-
export interface CacheStats {
|
|
13
|
-
size: number
|
|
14
|
-
files: string[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class VaultReader {
|
|
18
|
-
private cache: Map<string, MarkdownFile>
|
|
19
|
-
private cacheTimestamps: Map<string, number>
|
|
20
|
-
private logger: Logger
|
|
21
|
-
private maxCacheSize: number
|
|
22
|
-
private cacheMaxAge: number // milliseconds
|
|
23
|
-
|
|
24
|
-
constructor(
|
|
25
|
-
logger: Logger,
|
|
26
|
-
options: { maxCacheSize?: number; cacheMaxAge?: number } = {}
|
|
27
|
-
) {
|
|
28
|
-
this.logger = logger.child({ component: 'vault-reader' })
|
|
29
|
-
this.cache = new Map()
|
|
30
|
-
this.cacheTimestamps = new Map()
|
|
31
|
-
this.maxCacheSize = options.maxCacheSize ?? 100
|
|
32
|
-
this.cacheMaxAge = options.cacheMaxAge ?? 5 * 60 * 1000 // 5 minutes default
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Read a markdown file with frontmatter
|
|
37
|
-
*/
|
|
38
|
-
async readMarkdownFile(
|
|
39
|
-
filePath: string,
|
|
40
|
-
useCache: boolean = true
|
|
41
|
-
): Promise<MarkdownFile> {
|
|
42
|
-
// Check cache first
|
|
43
|
-
if (useCache && this.isCacheValid(filePath)) {
|
|
44
|
-
this.logger.debug({ filePath }, 'Cache hit')
|
|
45
|
-
return this.cache.get(filePath)!
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
// Check if file exists
|
|
50
|
-
await fs.access(filePath)
|
|
51
|
-
|
|
52
|
-
// Read file content
|
|
53
|
-
const rawContent = await fs.readFile(filePath, 'utf-8')
|
|
54
|
-
|
|
55
|
-
// Parse frontmatter
|
|
56
|
-
const { data, content } = matter(rawContent)
|
|
57
|
-
|
|
58
|
-
const result: MarkdownFile = {
|
|
59
|
-
path: filePath,
|
|
60
|
-
frontmatter: data as Frontmatter,
|
|
61
|
-
content: content.trim(),
|
|
62
|
-
rawContent
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Cache the result
|
|
66
|
-
this.addToCache(filePath, result)
|
|
67
|
-
|
|
68
|
-
this.logger.debug({ filePath }, 'File read successfully')
|
|
69
|
-
return result
|
|
70
|
-
} catch (error) {
|
|
71
|
-
this.logger.error({ error, filePath }, 'Failed to read file')
|
|
72
|
-
throw new Error(`Failed to read file ${filePath}: ${error}`)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Read multiple files
|
|
78
|
-
*/
|
|
79
|
-
async readMarkdownFiles(
|
|
80
|
-
filePaths: string[],
|
|
81
|
-
useCache: boolean = true
|
|
82
|
-
): Promise<MarkdownFile[]> {
|
|
83
|
-
return Promise.all(
|
|
84
|
-
filePaths.map(fp => this.readMarkdownFile(fp, useCache))
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Check if file exists
|
|
90
|
-
*/
|
|
91
|
-
async fileExists(filePath: string): Promise<boolean> {
|
|
92
|
-
try {
|
|
93
|
-
await fs.access(filePath)
|
|
94
|
-
return true
|
|
95
|
-
} catch {
|
|
96
|
-
return false
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Read directory contents
|
|
102
|
-
*/
|
|
103
|
-
async readDirectory(
|
|
104
|
-
dirPath: string,
|
|
105
|
-
recursive: boolean = false
|
|
106
|
-
): Promise<string[]> {
|
|
107
|
-
try {
|
|
108
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
109
|
-
let files: string[] = []
|
|
110
|
-
|
|
111
|
-
for (const entry of entries) {
|
|
112
|
-
const fullPath = path.join(dirPath, entry.name)
|
|
113
|
-
|
|
114
|
-
if (entry.isDirectory() && recursive) {
|
|
115
|
-
const subFiles = await this.readDirectory(fullPath, true)
|
|
116
|
-
files = files.concat(subFiles)
|
|
117
|
-
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
118
|
-
files.push(fullPath)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return files
|
|
123
|
-
} catch (error) {
|
|
124
|
-
this.logger.error({ error, dirPath }, 'Failed to read directory')
|
|
125
|
-
return []
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Get all project directories
|
|
131
|
-
*/
|
|
132
|
-
async getProjectDirectories(projectsPath: string): Promise<string[]> {
|
|
133
|
-
try {
|
|
134
|
-
const entries = await fs.readdir(projectsPath, { withFileTypes: true })
|
|
135
|
-
return entries
|
|
136
|
-
.filter(entry => entry.isDirectory())
|
|
137
|
-
.map(entry => entry.name)
|
|
138
|
-
} catch (error) {
|
|
139
|
-
this.logger.error({ error, projectsPath }, 'Failed to get projects')
|
|
140
|
-
return []
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Get file stats
|
|
146
|
-
*/
|
|
147
|
-
async getFileStats(filePath: string): Promise<{
|
|
148
|
-
size: number
|
|
149
|
-
created: Date
|
|
150
|
-
modified: Date
|
|
151
|
-
} | null> {
|
|
152
|
-
try {
|
|
153
|
-
const stats = await fs.stat(filePath)
|
|
154
|
-
return {
|
|
155
|
-
size: stats.size,
|
|
156
|
-
created: stats.birthtime,
|
|
157
|
-
modified: stats.mtime
|
|
158
|
-
}
|
|
159
|
-
} catch {
|
|
160
|
-
return null
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Check if cache entry is valid
|
|
166
|
-
*/
|
|
167
|
-
private isCacheValid(filePath: string): boolean {
|
|
168
|
-
if (!this.cache.has(filePath)) {
|
|
169
|
-
return false
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const timestamp = this.cacheTimestamps.get(filePath)
|
|
173
|
-
if (!timestamp) {
|
|
174
|
-
return false
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return Date.now() - timestamp < this.cacheMaxAge
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Add item to cache with LRU eviction
|
|
182
|
-
*/
|
|
183
|
-
private addToCache(filePath: string, file: MarkdownFile): void {
|
|
184
|
-
// Evict oldest entries if cache is full
|
|
185
|
-
if (this.cache.size >= this.maxCacheSize) {
|
|
186
|
-
const oldestKey = this.findOldestCacheEntry()
|
|
187
|
-
if (oldestKey) {
|
|
188
|
-
this.cache.delete(oldestKey)
|
|
189
|
-
this.cacheTimestamps.delete(oldestKey)
|
|
190
|
-
this.logger.debug({ filePath: oldestKey }, 'Cache entry evicted')
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
this.cache.set(filePath, file)
|
|
195
|
-
this.cacheTimestamps.set(filePath, Date.now())
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Find the oldest cache entry
|
|
200
|
-
*/
|
|
201
|
-
private findOldestCacheEntry(): string | null {
|
|
202
|
-
let oldestKey: string | null = null
|
|
203
|
-
let oldestTime = Infinity
|
|
204
|
-
|
|
205
|
-
for (const [key, timestamp] of this.cacheTimestamps) {
|
|
206
|
-
if (timestamp < oldestTime) {
|
|
207
|
-
oldestTime = timestamp
|
|
208
|
-
oldestKey = key
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return oldestKey
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Invalidate cache for a file
|
|
217
|
-
*/
|
|
218
|
-
invalidateCache(filePath: string): void {
|
|
219
|
-
this.cache.delete(filePath)
|
|
220
|
-
this.cacheTimestamps.delete(filePath)
|
|
221
|
-
this.logger.debug({ filePath }, 'Cache invalidated')
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Clear entire cache
|
|
226
|
-
*/
|
|
227
|
-
clearCache(): void {
|
|
228
|
-
this.cache.clear()
|
|
229
|
-
this.cacheTimestamps.clear()
|
|
230
|
-
this.logger.debug('Cache cleared')
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Get cache statistics
|
|
235
|
-
*/
|
|
236
|
-
getCacheStats(): CacheStats {
|
|
237
|
-
return {
|
|
238
|
-
size: this.cache.size,
|
|
239
|
-
files: Array.from(this.cache.keys())
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Clean expired cache entries
|
|
245
|
-
*/
|
|
246
|
-
cleanExpiredCache(): number {
|
|
247
|
-
let cleaned = 0
|
|
248
|
-
const now = Date.now()
|
|
249
|
-
|
|
250
|
-
for (const [key, timestamp] of this.cacheTimestamps) {
|
|
251
|
-
if (now - timestamp >= this.cacheMaxAge) {
|
|
252
|
-
this.cache.delete(key)
|
|
253
|
-
this.cacheTimestamps.delete(key)
|
|
254
|
-
cleaned++
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (cleaned > 0) {
|
|
259
|
-
this.logger.debug({ cleaned }, 'Expired cache entries cleaned')
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return cleaned
|
|
263
|
-
}
|
|
264
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Vault File Reader
|
|
3
|
+
* Safe, efficient file reading utilities with caching and frontmatter parsing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
import matter from 'gray-matter'
|
|
9
|
+
import type { Logger } from 'pino'
|
|
10
|
+
import type { MarkdownFile, Frontmatter } from './types'
|
|
11
|
+
|
|
12
|
+
export interface CacheStats {
|
|
13
|
+
size: number
|
|
14
|
+
files: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class VaultReader {
|
|
18
|
+
private cache: Map<string, MarkdownFile>
|
|
19
|
+
private cacheTimestamps: Map<string, number>
|
|
20
|
+
private logger: Logger
|
|
21
|
+
private maxCacheSize: number
|
|
22
|
+
private cacheMaxAge: number // milliseconds
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
logger: Logger,
|
|
26
|
+
options: { maxCacheSize?: number; cacheMaxAge?: number } = {}
|
|
27
|
+
) {
|
|
28
|
+
this.logger = logger.child({ component: 'vault-reader' })
|
|
29
|
+
this.cache = new Map()
|
|
30
|
+
this.cacheTimestamps = new Map()
|
|
31
|
+
this.maxCacheSize = options.maxCacheSize ?? 100
|
|
32
|
+
this.cacheMaxAge = options.cacheMaxAge ?? 5 * 60 * 1000 // 5 minutes default
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read a markdown file with frontmatter
|
|
37
|
+
*/
|
|
38
|
+
async readMarkdownFile(
|
|
39
|
+
filePath: string,
|
|
40
|
+
useCache: boolean = true
|
|
41
|
+
): Promise<MarkdownFile> {
|
|
42
|
+
// Check cache first
|
|
43
|
+
if (useCache && this.isCacheValid(filePath)) {
|
|
44
|
+
this.logger.debug({ filePath }, 'Cache hit')
|
|
45
|
+
return this.cache.get(filePath)!
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Check if file exists
|
|
50
|
+
await fs.access(filePath)
|
|
51
|
+
|
|
52
|
+
// Read file content
|
|
53
|
+
const rawContent = await fs.readFile(filePath, 'utf-8')
|
|
54
|
+
|
|
55
|
+
// Parse frontmatter
|
|
56
|
+
const { data, content } = matter(rawContent)
|
|
57
|
+
|
|
58
|
+
const result: MarkdownFile = {
|
|
59
|
+
path: filePath,
|
|
60
|
+
frontmatter: data as Frontmatter,
|
|
61
|
+
content: content.trim(),
|
|
62
|
+
rawContent
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Cache the result
|
|
66
|
+
this.addToCache(filePath, result)
|
|
67
|
+
|
|
68
|
+
this.logger.debug({ filePath }, 'File read successfully')
|
|
69
|
+
return result
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.logger.error({ error, filePath }, 'Failed to read file')
|
|
72
|
+
throw new Error(`Failed to read file ${filePath}: ${error}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read multiple files
|
|
78
|
+
*/
|
|
79
|
+
async readMarkdownFiles(
|
|
80
|
+
filePaths: string[],
|
|
81
|
+
useCache: boolean = true
|
|
82
|
+
): Promise<MarkdownFile[]> {
|
|
83
|
+
return Promise.all(
|
|
84
|
+
filePaths.map(fp => this.readMarkdownFile(fp, useCache))
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if file exists
|
|
90
|
+
*/
|
|
91
|
+
async fileExists(filePath: string): Promise<boolean> {
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(filePath)
|
|
94
|
+
return true
|
|
95
|
+
} catch {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read directory contents
|
|
102
|
+
*/
|
|
103
|
+
async readDirectory(
|
|
104
|
+
dirPath: string,
|
|
105
|
+
recursive: boolean = false
|
|
106
|
+
): Promise<string[]> {
|
|
107
|
+
try {
|
|
108
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
109
|
+
let files: string[] = []
|
|
110
|
+
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const fullPath = path.join(dirPath, entry.name)
|
|
113
|
+
|
|
114
|
+
if (entry.isDirectory() && recursive) {
|
|
115
|
+
const subFiles = await this.readDirectory(fullPath, true)
|
|
116
|
+
files = files.concat(subFiles)
|
|
117
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
118
|
+
files.push(fullPath)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return files
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.logger.error({ error, dirPath }, 'Failed to read directory')
|
|
125
|
+
return []
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get all project directories
|
|
131
|
+
*/
|
|
132
|
+
async getProjectDirectories(projectsPath: string): Promise<string[]> {
|
|
133
|
+
try {
|
|
134
|
+
const entries = await fs.readdir(projectsPath, { withFileTypes: true })
|
|
135
|
+
return entries
|
|
136
|
+
.filter(entry => entry.isDirectory())
|
|
137
|
+
.map(entry => entry.name)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.logger.error({ error, projectsPath }, 'Failed to get projects')
|
|
140
|
+
return []
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get file stats
|
|
146
|
+
*/
|
|
147
|
+
async getFileStats(filePath: string): Promise<{
|
|
148
|
+
size: number
|
|
149
|
+
created: Date
|
|
150
|
+
modified: Date
|
|
151
|
+
} | null> {
|
|
152
|
+
try {
|
|
153
|
+
const stats = await fs.stat(filePath)
|
|
154
|
+
return {
|
|
155
|
+
size: stats.size,
|
|
156
|
+
created: stats.birthtime,
|
|
157
|
+
modified: stats.mtime
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if cache entry is valid
|
|
166
|
+
*/
|
|
167
|
+
private isCacheValid(filePath: string): boolean {
|
|
168
|
+
if (!this.cache.has(filePath)) {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const timestamp = this.cacheTimestamps.get(filePath)
|
|
173
|
+
if (!timestamp) {
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Date.now() - timestamp < this.cacheMaxAge
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Add item to cache with LRU eviction
|
|
182
|
+
*/
|
|
183
|
+
private addToCache(filePath: string, file: MarkdownFile): void {
|
|
184
|
+
// Evict oldest entries if cache is full
|
|
185
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
186
|
+
const oldestKey = this.findOldestCacheEntry()
|
|
187
|
+
if (oldestKey) {
|
|
188
|
+
this.cache.delete(oldestKey)
|
|
189
|
+
this.cacheTimestamps.delete(oldestKey)
|
|
190
|
+
this.logger.debug({ filePath: oldestKey }, 'Cache entry evicted')
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.cache.set(filePath, file)
|
|
195
|
+
this.cacheTimestamps.set(filePath, Date.now())
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Find the oldest cache entry
|
|
200
|
+
*/
|
|
201
|
+
private findOldestCacheEntry(): string | null {
|
|
202
|
+
let oldestKey: string | null = null
|
|
203
|
+
let oldestTime = Infinity
|
|
204
|
+
|
|
205
|
+
for (const [key, timestamp] of this.cacheTimestamps) {
|
|
206
|
+
if (timestamp < oldestTime) {
|
|
207
|
+
oldestTime = timestamp
|
|
208
|
+
oldestKey = key
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return oldestKey
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Invalidate cache for a file
|
|
217
|
+
*/
|
|
218
|
+
invalidateCache(filePath: string): void {
|
|
219
|
+
this.cache.delete(filePath)
|
|
220
|
+
this.cacheTimestamps.delete(filePath)
|
|
221
|
+
this.logger.debug({ filePath }, 'Cache invalidated')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Clear entire cache
|
|
226
|
+
*/
|
|
227
|
+
clearCache(): void {
|
|
228
|
+
this.cache.clear()
|
|
229
|
+
this.cacheTimestamps.clear()
|
|
230
|
+
this.logger.debug('Cache cleared')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get cache statistics
|
|
235
|
+
*/
|
|
236
|
+
getCacheStats(): CacheStats {
|
|
237
|
+
return {
|
|
238
|
+
size: this.cache.size,
|
|
239
|
+
files: Array.from(this.cache.keys())
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Clean expired cache entries
|
|
245
|
+
*/
|
|
246
|
+
cleanExpiredCache(): number {
|
|
247
|
+
let cleaned = 0
|
|
248
|
+
const now = Date.now()
|
|
249
|
+
|
|
250
|
+
for (const [key, timestamp] of this.cacheTimestamps) {
|
|
251
|
+
if (now - timestamp >= this.cacheMaxAge) {
|
|
252
|
+
this.cache.delete(key)
|
|
253
|
+
this.cacheTimestamps.delete(key)
|
|
254
|
+
cleaned++
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (cleaned > 0) {
|
|
259
|
+
this.logger.debug({ cleaned }, 'Expired cache entries cleaned')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return cleaned
|
|
263
|
+
}
|
|
264
|
+
}
|