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/watcher.ts
CHANGED
|
@@ -1,277 +1,277 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vault File Watcher
|
|
3
|
-
* Monitor vault for changes and emit events for real-time synchronization
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { watch, type FSWatcher } from 'fs'
|
|
7
|
-
import fs from 'fs/promises'
|
|
8
|
-
import path from 'path'
|
|
9
|
-
import { EventEmitter } from 'events'
|
|
10
|
-
import type { Logger } from 'pino'
|
|
11
|
-
|
|
12
|
-
export type FileChangeType = 'created' | 'modified' | 'deleted'
|
|
13
|
-
|
|
14
|
-
export interface FileChangeEvent {
|
|
15
|
-
type: FileChangeType
|
|
16
|
-
path: string
|
|
17
|
-
timestamp: Date
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface WatcherStatus {
|
|
21
|
-
watching: string[]
|
|
22
|
-
activeDebounces: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface VaultWatcherEvents {
|
|
26
|
-
change: (event: FileChangeEvent) => void
|
|
27
|
-
created: (event: FileChangeEvent) => void
|
|
28
|
-
modified: (event: FileChangeEvent) => void
|
|
29
|
-
deleted: (event: FileChangeEvent) => void
|
|
30
|
-
error: (error: Error, path: string) => void
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class VaultWatcher extends EventEmitter {
|
|
34
|
-
private watchers: Map<string, FSWatcher>
|
|
35
|
-
private logger: Logger
|
|
36
|
-
private debounceTimers: Map<string, NodeJS.Timeout>
|
|
37
|
-
private debounceMs: number
|
|
38
|
-
private fileStates: Map<string, boolean> // Track if file exists
|
|
39
|
-
|
|
40
|
-
constructor(logger: Logger, debounceMs: number = 500) {
|
|
41
|
-
super()
|
|
42
|
-
this.logger = logger.child({ component: 'vault-watcher' })
|
|
43
|
-
this.watchers = new Map()
|
|
44
|
-
this.debounceTimers = new Map()
|
|
45
|
-
this.debounceMs = debounceMs
|
|
46
|
-
this.fileStates = new Map()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Type-safe event listener
|
|
51
|
-
*/
|
|
52
|
-
on<K extends keyof VaultWatcherEvents>(
|
|
53
|
-
event: K,
|
|
54
|
-
listener: VaultWatcherEvents[K]
|
|
55
|
-
): this {
|
|
56
|
-
return super.on(event, listener)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Type-safe emit
|
|
61
|
-
*/
|
|
62
|
-
emit<K extends keyof VaultWatcherEvents>(
|
|
63
|
-
event: K,
|
|
64
|
-
...args: Parameters<VaultWatcherEvents[K]>
|
|
65
|
-
): boolean {
|
|
66
|
-
return super.emit(event, ...args)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Start watching a directory
|
|
71
|
-
*/
|
|
72
|
-
watch(dirPath: string, recursive: boolean = true): void {
|
|
73
|
-
if (this.watchers.has(dirPath)) {
|
|
74
|
-
this.logger.warn({ dirPath }, 'Already watching directory')
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const watcher = watch(
|
|
80
|
-
dirPath,
|
|
81
|
-
{ recursive },
|
|
82
|
-
(eventType, filename) => {
|
|
83
|
-
if (!filename || !filename.endsWith('.md')) {
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const fullPath = path.join(dirPath, filename)
|
|
88
|
-
this.handleFileChange(eventType, fullPath)
|
|
89
|
-
}
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
watcher.on('error', (error) => {
|
|
93
|
-
this.logger.error({ error, dirPath }, 'Watcher error')
|
|
94
|
-
this.emit('error', error, dirPath)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
this.watchers.set(dirPath, watcher)
|
|
98
|
-
this.logger.info({ dirPath, recursive }, 'Started watching directory')
|
|
99
|
-
} catch (error) {
|
|
100
|
-
this.logger.error({ error, dirPath }, 'Failed to start watching')
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Stop watching a directory
|
|
106
|
-
*/
|
|
107
|
-
unwatch(dirPath: string): void {
|
|
108
|
-
const watcher = this.watchers.get(dirPath)
|
|
109
|
-
if (watcher) {
|
|
110
|
-
watcher.close()
|
|
111
|
-
this.watchers.delete(dirPath)
|
|
112
|
-
this.logger.info({ dirPath }, 'Stopped watching directory')
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Stop all watchers
|
|
118
|
-
*/
|
|
119
|
-
unwatchAll(): void {
|
|
120
|
-
for (const [dirPath, watcher] of this.watchers) {
|
|
121
|
-
watcher.close()
|
|
122
|
-
this.logger.debug({ dirPath }, 'Watcher closed')
|
|
123
|
-
}
|
|
124
|
-
this.watchers.clear()
|
|
125
|
-
|
|
126
|
-
// Clear all debounce timers
|
|
127
|
-
for (const timer of this.debounceTimers.values()) {
|
|
128
|
-
clearTimeout(timer)
|
|
129
|
-
}
|
|
130
|
-
this.debounceTimers.clear()
|
|
131
|
-
|
|
132
|
-
// Clear file states
|
|
133
|
-
this.fileStates.clear()
|
|
134
|
-
|
|
135
|
-
this.logger.info('All watchers stopped')
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Handle file change with debouncing
|
|
140
|
-
*/
|
|
141
|
-
private handleFileChange(eventType: string, filePath: string): void {
|
|
142
|
-
// Clear existing timer for this file
|
|
143
|
-
const existingTimer = this.debounceTimers.get(filePath)
|
|
144
|
-
if (existingTimer) {
|
|
145
|
-
clearTimeout(existingTimer)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Set new timer
|
|
149
|
-
const timer = setTimeout(async () => {
|
|
150
|
-
await this.emitFileChange(eventType, filePath)
|
|
151
|
-
this.debounceTimers.delete(filePath)
|
|
152
|
-
}, this.debounceMs)
|
|
153
|
-
|
|
154
|
-
this.debounceTimers.set(filePath, timer)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Emit file change event after determining change type
|
|
159
|
-
*/
|
|
160
|
-
private async emitFileChange(eventType: string, filePath: string): Promise<void> {
|
|
161
|
-
// Determine change type by checking file existence
|
|
162
|
-
let changeType: FileChangeType
|
|
163
|
-
const previouslyExisted = this.fileStates.get(filePath) ?? false
|
|
164
|
-
let currentlyExists = false
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
await fs.access(filePath)
|
|
168
|
-
currentlyExists = true
|
|
169
|
-
} catch {
|
|
170
|
-
currentlyExists = false
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Update file state
|
|
174
|
-
this.fileStates.set(filePath, currentlyExists)
|
|
175
|
-
|
|
176
|
-
// Determine the type of change
|
|
177
|
-
if (eventType === 'rename') {
|
|
178
|
-
if (currentlyExists && !previouslyExisted) {
|
|
179
|
-
changeType = 'created'
|
|
180
|
-
} else if (!currentlyExists && previouslyExisted) {
|
|
181
|
-
changeType = 'deleted'
|
|
182
|
-
} else if (currentlyExists) {
|
|
183
|
-
// File was renamed to this path from somewhere else
|
|
184
|
-
changeType = 'created'
|
|
185
|
-
} else {
|
|
186
|
-
// File was renamed away from this path
|
|
187
|
-
changeType = 'deleted'
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
// eventType === 'change'
|
|
191
|
-
changeType = 'modified'
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const event: FileChangeEvent = {
|
|
195
|
-
type: changeType,
|
|
196
|
-
path: filePath,
|
|
197
|
-
timestamp: new Date()
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
this.logger.debug({ event }, 'File change detected')
|
|
201
|
-
this.emit('change', event)
|
|
202
|
-
this.emit(changeType, event)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Force refresh of file states for a directory
|
|
207
|
-
*/
|
|
208
|
-
async refreshFileStates(dirPath: string): Promise<void> {
|
|
209
|
-
try {
|
|
210
|
-
const files = await this.scanDirectory(dirPath)
|
|
211
|
-
for (const file of files) {
|
|
212
|
-
this.fileStates.set(file, true)
|
|
213
|
-
}
|
|
214
|
-
this.logger.debug({ dirPath, fileCount: files.length }, 'File states refreshed')
|
|
215
|
-
} catch (error) {
|
|
216
|
-
this.logger.error({ error, dirPath }, 'Failed to refresh file states')
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Scan directory for markdown files
|
|
222
|
-
*/
|
|
223
|
-
private async scanDirectory(dirPath: string): Promise<string[]> {
|
|
224
|
-
const files: string[] = []
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
228
|
-
|
|
229
|
-
for (const entry of entries) {
|
|
230
|
-
const fullPath = path.join(dirPath, entry.name)
|
|
231
|
-
|
|
232
|
-
if (entry.isDirectory()) {
|
|
233
|
-
const subFiles = await this.scanDirectory(fullPath)
|
|
234
|
-
files.push(...subFiles)
|
|
235
|
-
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
236
|
-
files.push(fullPath)
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
} catch {
|
|
240
|
-
// Directory might not exist yet
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return files
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Get watcher status
|
|
248
|
-
*/
|
|
249
|
-
getStatus(): WatcherStatus {
|
|
250
|
-
return {
|
|
251
|
-
watching: Array.from(this.watchers.keys()),
|
|
252
|
-
activeDebounces: this.debounceTimers.size
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Check if a path is being watched
|
|
258
|
-
*/
|
|
259
|
-
isWatching(dirPath: string): boolean {
|
|
260
|
-
return this.watchers.has(dirPath)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Get the number of active watchers
|
|
265
|
-
*/
|
|
266
|
-
getWatcherCount(): number {
|
|
267
|
-
return this.watchers.size
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Update debounce delay
|
|
272
|
-
*/
|
|
273
|
-
setDebounceMs(ms: number): void {
|
|
274
|
-
this.debounceMs = ms
|
|
275
|
-
this.logger.info({ debounceMs: ms }, 'Debounce delay updated')
|
|
276
|
-
}
|
|
277
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Vault File Watcher
|
|
3
|
+
* Monitor vault for changes and emit events for real-time synchronization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { watch, type FSWatcher } from 'fs'
|
|
7
|
+
import fs from 'fs/promises'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import { EventEmitter } from 'events'
|
|
10
|
+
import type { Logger } from 'pino'
|
|
11
|
+
|
|
12
|
+
export type FileChangeType = 'created' | 'modified' | 'deleted'
|
|
13
|
+
|
|
14
|
+
export interface FileChangeEvent {
|
|
15
|
+
type: FileChangeType
|
|
16
|
+
path: string
|
|
17
|
+
timestamp: Date
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WatcherStatus {
|
|
21
|
+
watching: string[]
|
|
22
|
+
activeDebounces: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VaultWatcherEvents {
|
|
26
|
+
change: (event: FileChangeEvent) => void
|
|
27
|
+
created: (event: FileChangeEvent) => void
|
|
28
|
+
modified: (event: FileChangeEvent) => void
|
|
29
|
+
deleted: (event: FileChangeEvent) => void
|
|
30
|
+
error: (error: Error, path: string) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class VaultWatcher extends EventEmitter {
|
|
34
|
+
private watchers: Map<string, FSWatcher>
|
|
35
|
+
private logger: Logger
|
|
36
|
+
private debounceTimers: Map<string, NodeJS.Timeout>
|
|
37
|
+
private debounceMs: number
|
|
38
|
+
private fileStates: Map<string, boolean> // Track if file exists
|
|
39
|
+
|
|
40
|
+
constructor(logger: Logger, debounceMs: number = 500) {
|
|
41
|
+
super()
|
|
42
|
+
this.logger = logger.child({ component: 'vault-watcher' })
|
|
43
|
+
this.watchers = new Map()
|
|
44
|
+
this.debounceTimers = new Map()
|
|
45
|
+
this.debounceMs = debounceMs
|
|
46
|
+
this.fileStates = new Map()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type-safe event listener
|
|
51
|
+
*/
|
|
52
|
+
on<K extends keyof VaultWatcherEvents>(
|
|
53
|
+
event: K,
|
|
54
|
+
listener: VaultWatcherEvents[K]
|
|
55
|
+
): this {
|
|
56
|
+
return super.on(event, listener)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Type-safe emit
|
|
61
|
+
*/
|
|
62
|
+
emit<K extends keyof VaultWatcherEvents>(
|
|
63
|
+
event: K,
|
|
64
|
+
...args: Parameters<VaultWatcherEvents[K]>
|
|
65
|
+
): boolean {
|
|
66
|
+
return super.emit(event, ...args)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Start watching a directory
|
|
71
|
+
*/
|
|
72
|
+
watch(dirPath: string, recursive: boolean = true): void {
|
|
73
|
+
if (this.watchers.has(dirPath)) {
|
|
74
|
+
this.logger.warn({ dirPath }, 'Already watching directory')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const watcher = watch(
|
|
80
|
+
dirPath,
|
|
81
|
+
{ recursive },
|
|
82
|
+
(eventType, filename) => {
|
|
83
|
+
if (!filename || !filename.endsWith('.md')) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fullPath = path.join(dirPath, filename)
|
|
88
|
+
this.handleFileChange(eventType, fullPath)
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
watcher.on('error', (error) => {
|
|
93
|
+
this.logger.error({ error, dirPath }, 'Watcher error')
|
|
94
|
+
this.emit('error', error, dirPath)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
this.watchers.set(dirPath, watcher)
|
|
98
|
+
this.logger.info({ dirPath, recursive }, 'Started watching directory')
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.logger.error({ error, dirPath }, 'Failed to start watching')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stop watching a directory
|
|
106
|
+
*/
|
|
107
|
+
unwatch(dirPath: string): void {
|
|
108
|
+
const watcher = this.watchers.get(dirPath)
|
|
109
|
+
if (watcher) {
|
|
110
|
+
watcher.close()
|
|
111
|
+
this.watchers.delete(dirPath)
|
|
112
|
+
this.logger.info({ dirPath }, 'Stopped watching directory')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Stop all watchers
|
|
118
|
+
*/
|
|
119
|
+
unwatchAll(): void {
|
|
120
|
+
for (const [dirPath, watcher] of this.watchers) {
|
|
121
|
+
watcher.close()
|
|
122
|
+
this.logger.debug({ dirPath }, 'Watcher closed')
|
|
123
|
+
}
|
|
124
|
+
this.watchers.clear()
|
|
125
|
+
|
|
126
|
+
// Clear all debounce timers
|
|
127
|
+
for (const timer of this.debounceTimers.values()) {
|
|
128
|
+
clearTimeout(timer)
|
|
129
|
+
}
|
|
130
|
+
this.debounceTimers.clear()
|
|
131
|
+
|
|
132
|
+
// Clear file states
|
|
133
|
+
this.fileStates.clear()
|
|
134
|
+
|
|
135
|
+
this.logger.info('All watchers stopped')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Handle file change with debouncing
|
|
140
|
+
*/
|
|
141
|
+
private handleFileChange(eventType: string, filePath: string): void {
|
|
142
|
+
// Clear existing timer for this file
|
|
143
|
+
const existingTimer = this.debounceTimers.get(filePath)
|
|
144
|
+
if (existingTimer) {
|
|
145
|
+
clearTimeout(existingTimer)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Set new timer
|
|
149
|
+
const timer = setTimeout(async () => {
|
|
150
|
+
await this.emitFileChange(eventType, filePath)
|
|
151
|
+
this.debounceTimers.delete(filePath)
|
|
152
|
+
}, this.debounceMs)
|
|
153
|
+
|
|
154
|
+
this.debounceTimers.set(filePath, timer)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Emit file change event after determining change type
|
|
159
|
+
*/
|
|
160
|
+
private async emitFileChange(eventType: string, filePath: string): Promise<void> {
|
|
161
|
+
// Determine change type by checking file existence
|
|
162
|
+
let changeType: FileChangeType
|
|
163
|
+
const previouslyExisted = this.fileStates.get(filePath) ?? false
|
|
164
|
+
let currentlyExists = false
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await fs.access(filePath)
|
|
168
|
+
currentlyExists = true
|
|
169
|
+
} catch {
|
|
170
|
+
currentlyExists = false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Update file state
|
|
174
|
+
this.fileStates.set(filePath, currentlyExists)
|
|
175
|
+
|
|
176
|
+
// Determine the type of change
|
|
177
|
+
if (eventType === 'rename') {
|
|
178
|
+
if (currentlyExists && !previouslyExisted) {
|
|
179
|
+
changeType = 'created'
|
|
180
|
+
} else if (!currentlyExists && previouslyExisted) {
|
|
181
|
+
changeType = 'deleted'
|
|
182
|
+
} else if (currentlyExists) {
|
|
183
|
+
// File was renamed to this path from somewhere else
|
|
184
|
+
changeType = 'created'
|
|
185
|
+
} else {
|
|
186
|
+
// File was renamed away from this path
|
|
187
|
+
changeType = 'deleted'
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// eventType === 'change'
|
|
191
|
+
changeType = 'modified'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const event: FileChangeEvent = {
|
|
195
|
+
type: changeType,
|
|
196
|
+
path: filePath,
|
|
197
|
+
timestamp: new Date()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.logger.debug({ event }, 'File change detected')
|
|
201
|
+
this.emit('change', event)
|
|
202
|
+
this.emit(changeType, event)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Force refresh of file states for a directory
|
|
207
|
+
*/
|
|
208
|
+
async refreshFileStates(dirPath: string): Promise<void> {
|
|
209
|
+
try {
|
|
210
|
+
const files = await this.scanDirectory(dirPath)
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
this.fileStates.set(file, true)
|
|
213
|
+
}
|
|
214
|
+
this.logger.debug({ dirPath, fileCount: files.length }, 'File states refreshed')
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.logger.error({ error, dirPath }, 'Failed to refresh file states')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Scan directory for markdown files
|
|
222
|
+
*/
|
|
223
|
+
private async scanDirectory(dirPath: string): Promise<string[]> {
|
|
224
|
+
const files: string[] = []
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
228
|
+
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
const fullPath = path.join(dirPath, entry.name)
|
|
231
|
+
|
|
232
|
+
if (entry.isDirectory()) {
|
|
233
|
+
const subFiles = await this.scanDirectory(fullPath)
|
|
234
|
+
files.push(...subFiles)
|
|
235
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
236
|
+
files.push(fullPath)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Directory might not exist yet
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return files
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get watcher status
|
|
248
|
+
*/
|
|
249
|
+
getStatus(): WatcherStatus {
|
|
250
|
+
return {
|
|
251
|
+
watching: Array.from(this.watchers.keys()),
|
|
252
|
+
activeDebounces: this.debounceTimers.size
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check if a path is being watched
|
|
258
|
+
*/
|
|
259
|
+
isWatching(dirPath: string): boolean {
|
|
260
|
+
return this.watchers.has(dirPath)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the number of active watchers
|
|
265
|
+
*/
|
|
266
|
+
getWatcherCount(): number {
|
|
267
|
+
return this.watchers.size
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Update debounce delay
|
|
272
|
+
*/
|
|
273
|
+
setDebounceMs(ms: number): void {
|
|
274
|
+
this.debounceMs = ms
|
|
275
|
+
this.logger.info({ debounceMs: ms }, 'Debounce delay updated')
|
|
276
|
+
}
|
|
277
|
+
}
|