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/writer.ts
CHANGED
|
@@ -1,413 +1,413 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vault File Writer
|
|
3
|
-
* Utilities for safely updating vault content with atomic writes and backups
|
|
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 { Frontmatter } from './types'
|
|
11
|
-
import { TEMPLATES, renderTemplate, createProjectVariables, getTodayDate } from './templates'
|
|
12
|
-
import { RetryManager } from '@/utils'
|
|
13
|
-
|
|
14
|
-
export interface WriteOptions {
|
|
15
|
-
createBackup?: boolean
|
|
16
|
-
ensureDirectory?: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class VaultWriter {
|
|
20
|
-
private logger: Logger
|
|
21
|
-
private backupDir: string
|
|
22
|
-
private retryManager: RetryManager
|
|
23
|
-
|
|
24
|
-
constructor(logger: Logger, backupDir: string) {
|
|
25
|
-
this.logger = logger.child({ component: 'vault-writer' })
|
|
26
|
-
this.backupDir = backupDir
|
|
27
|
-
this.retryManager = new RetryManager(logger)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Write markdown file with frontmatter atomically
|
|
32
|
-
* Falls back to direct write if rename fails (e.g., Obsidian has file open)
|
|
33
|
-
*/
|
|
34
|
-
async writeMarkdownFile(
|
|
35
|
-
filePath: string,
|
|
36
|
-
frontmatter: Frontmatter,
|
|
37
|
-
content: string,
|
|
38
|
-
options: WriteOptions = {}
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
return this.retryManager.execute(
|
|
41
|
-
async () => {
|
|
42
|
-
const { createBackup = true, ensureDirectory = true } = options
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
if (createBackup && (await this.fileExists(filePath))) {
|
|
46
|
-
await this.createBackup(filePath)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const fileContent = matter.stringify(content, frontmatter)
|
|
50
|
-
|
|
51
|
-
if (ensureDirectory) {
|
|
52
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Try atomic write first (temp file + rename)
|
|
56
|
-
const tempPath = `${filePath}.tmp`
|
|
57
|
-
try {
|
|
58
|
-
await fs.writeFile(tempPath, fileContent, 'utf-8')
|
|
59
|
-
await fs.rename(tempPath, filePath)
|
|
60
|
-
} catch (renameError) {
|
|
61
|
-
// If rename fails (EPERM - file locked by Obsidian), write directly
|
|
62
|
-
const isPermError = (renameError as NodeJS.ErrnoException).code === 'EPERM'
|
|
63
|
-
if (isPermError) {
|
|
64
|
-
this.logger.warn({ filePath }, 'Atomic write failed (file locked), using direct write')
|
|
65
|
-
await fs.writeFile(filePath, fileContent, 'utf-8')
|
|
66
|
-
// Clean up temp file
|
|
67
|
-
try {
|
|
68
|
-
await fs.unlink(tempPath)
|
|
69
|
-
} catch {}
|
|
70
|
-
} else {
|
|
71
|
-
throw renameError
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
this.logger.info({ filePath }, 'File written successfully')
|
|
76
|
-
} catch (error) {
|
|
77
|
-
try {
|
|
78
|
-
await fs.unlink(`${filePath}.tmp`)
|
|
79
|
-
} catch {
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
this.logger.error({ error, filePath }, 'Failed to write file')
|
|
83
|
-
throw new Error(`Failed to write file ${filePath}: ${error}`)
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
maxRetries: 3,
|
|
88
|
-
retryCondition: RetryManager.isRetryableError,
|
|
89
|
-
onRetry: (error, attempt) => {
|
|
90
|
-
this.logger.warn(
|
|
91
|
-
{ error: error.message, attempt, filePath },
|
|
92
|
-
'Retrying file write'
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Update only the frontmatter of a file
|
|
101
|
-
*/
|
|
102
|
-
async updateFrontmatter(
|
|
103
|
-
filePath: string,
|
|
104
|
-
updates: Partial<Frontmatter>
|
|
105
|
-
): Promise<void> {
|
|
106
|
-
try {
|
|
107
|
-
// Read existing file
|
|
108
|
-
const rawContent = await fs.readFile(filePath, 'utf-8')
|
|
109
|
-
const { data, content } = matter(rawContent)
|
|
110
|
-
|
|
111
|
-
// Merge updates and update timestamp
|
|
112
|
-
const newFrontmatter: Frontmatter = {
|
|
113
|
-
...(data as Frontmatter),
|
|
114
|
-
...updates,
|
|
115
|
-
updated: getTodayDate()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Write back
|
|
119
|
-
await this.writeMarkdownFile(filePath, newFrontmatter, content, {
|
|
120
|
-
createBackup: true
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
this.logger.info({ filePath, updates }, 'Frontmatter updated')
|
|
124
|
-
} catch (error) {
|
|
125
|
-
this.logger.error({ error, filePath }, 'Failed to update frontmatter')
|
|
126
|
-
throw error
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Append content to a file
|
|
132
|
-
* Creates the file (and parent directories) if it doesn't exist
|
|
133
|
-
*/
|
|
134
|
-
async appendContent(
|
|
135
|
-
filePath: string,
|
|
136
|
-
newContent: string,
|
|
137
|
-
separator: string = '\n\n'
|
|
138
|
-
): Promise<void> {
|
|
139
|
-
try {
|
|
140
|
-
let rawContent: string
|
|
141
|
-
try {
|
|
142
|
-
rawContent = await fs.readFile(filePath, 'utf-8')
|
|
143
|
-
} catch (readError: any) {
|
|
144
|
-
if (readError.code === 'ENOENT') {
|
|
145
|
-
// File doesn't exist — create it with the new content
|
|
146
|
-
const today = getTodayDate()
|
|
147
|
-
const frontmatter: Frontmatter = {
|
|
148
|
-
created: today,
|
|
149
|
-
updated: today
|
|
150
|
-
}
|
|
151
|
-
await this.writeMarkdownFile(filePath, frontmatter, newContent, {
|
|
152
|
-
createBackup: false,
|
|
153
|
-
ensureDirectory: true
|
|
154
|
-
})
|
|
155
|
-
this.logger.info({ filePath }, 'File created with initial content')
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
throw readError
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const { data, content } = matter(rawContent)
|
|
162
|
-
|
|
163
|
-
// Append new content
|
|
164
|
-
const updatedContent = content.trim() + separator + newContent
|
|
165
|
-
|
|
166
|
-
// Update timestamp in frontmatter
|
|
167
|
-
const today = getTodayDate()
|
|
168
|
-
const updatedFrontmatter: Frontmatter = {
|
|
169
|
-
...(data as Frontmatter),
|
|
170
|
-
updated: today,
|
|
171
|
-
last_updated: today
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Write back
|
|
175
|
-
await this.writeMarkdownFile(filePath, updatedFrontmatter, updatedContent, {
|
|
176
|
-
createBackup: true
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
this.logger.info({ filePath }, 'Content appended')
|
|
180
|
-
} catch (error) {
|
|
181
|
-
this.logger.error({ error, filePath }, 'Failed to append content')
|
|
182
|
-
throw error
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Prepend content to a file (after frontmatter)
|
|
188
|
-
*/
|
|
189
|
-
async prependContent(
|
|
190
|
-
filePath: string,
|
|
191
|
-
newContent: string,
|
|
192
|
-
separator: string = '\n\n'
|
|
193
|
-
): Promise<void> {
|
|
194
|
-
try {
|
|
195
|
-
// Read existing file
|
|
196
|
-
const rawContent = await fs.readFile(filePath, 'utf-8')
|
|
197
|
-
const { data, content } = matter(rawContent)
|
|
198
|
-
|
|
199
|
-
// Prepend new content
|
|
200
|
-
const updatedContent = newContent + separator + content.trim()
|
|
201
|
-
|
|
202
|
-
// Update timestamp in frontmatter
|
|
203
|
-
const updatedFrontmatter: Frontmatter = {
|
|
204
|
-
...(data as Frontmatter),
|
|
205
|
-
updated: getTodayDate()
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Write back
|
|
209
|
-
await this.writeMarkdownFile(filePath, updatedFrontmatter, updatedContent, {
|
|
210
|
-
createBackup: true
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
this.logger.info({ filePath }, 'Content prepended')
|
|
214
|
-
} catch (error) {
|
|
215
|
-
this.logger.error({ error, filePath }, 'Failed to prepend content')
|
|
216
|
-
throw error
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Replace a section in a file by header
|
|
222
|
-
*/
|
|
223
|
-
async replaceSection(
|
|
224
|
-
filePath: string,
|
|
225
|
-
sectionHeader: string,
|
|
226
|
-
newSectionContent: string
|
|
227
|
-
): Promise<boolean> {
|
|
228
|
-
try {
|
|
229
|
-
const rawContent = await fs.readFile(filePath, 'utf-8')
|
|
230
|
-
const { data, content } = matter(rawContent)
|
|
231
|
-
|
|
232
|
-
// Find the section using regex
|
|
233
|
-
const headerPattern = new RegExp(
|
|
234
|
-
`(^|\\n)(#{1,6}\\s*${this.escapeRegex(sectionHeader)}\\s*\\n)`,
|
|
235
|
-
'i'
|
|
236
|
-
)
|
|
237
|
-
const match = content.match(headerPattern)
|
|
238
|
-
|
|
239
|
-
if (!match || !match[1] || !match[2]) {
|
|
240
|
-
this.logger.warn({ filePath, sectionHeader }, 'Section not found')
|
|
241
|
-
return false
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Find the end of the section (next header of same or higher level)
|
|
245
|
-
const headerMatch = match[2].match(/^#+/)
|
|
246
|
-
const headerLevel = headerMatch?.[0]?.length ?? 1
|
|
247
|
-
const startIndex = (match.index ?? 0) + match[0].length
|
|
248
|
-
const remainingContent = content.substring(startIndex)
|
|
249
|
-
|
|
250
|
-
const nextHeaderPattern = new RegExp(`\\n#{1,${headerLevel}}\\s+`)
|
|
251
|
-
const nextMatch = remainingContent.match(nextHeaderPattern)
|
|
252
|
-
const endIndex = nextMatch
|
|
253
|
-
? startIndex + (nextMatch.index ?? 0)
|
|
254
|
-
: content.length
|
|
255
|
-
|
|
256
|
-
// Build new content
|
|
257
|
-
const before = content.substring(0, (match.index ?? 0) + match[1].length)
|
|
258
|
-
const sectionTitle = match[2]
|
|
259
|
-
const after = content.substring(endIndex)
|
|
260
|
-
const updatedContent = before + sectionTitle + newSectionContent + after
|
|
261
|
-
|
|
262
|
-
// Update frontmatter timestamp
|
|
263
|
-
const updatedFrontmatter: Frontmatter = {
|
|
264
|
-
...(data as Frontmatter),
|
|
265
|
-
updated: getTodayDate()
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
await this.writeMarkdownFile(filePath, updatedFrontmatter, updatedContent, {
|
|
269
|
-
createBackup: true
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
this.logger.info({ filePath, sectionHeader }, 'Section replaced')
|
|
273
|
-
return true
|
|
274
|
-
} catch (error) {
|
|
275
|
-
this.logger.error({ error, filePath }, 'Failed to replace section')
|
|
276
|
-
throw error
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Create a backup of a file
|
|
282
|
-
*/
|
|
283
|
-
private async createBackup(filePath: string): Promise<string | null> {
|
|
284
|
-
try {
|
|
285
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
286
|
-
const basename = path.basename(filePath)
|
|
287
|
-
const backupPath = path.join(this.backupDir, `${basename}.${timestamp}.bak`)
|
|
288
|
-
|
|
289
|
-
await fs.mkdir(this.backupDir, { recursive: true })
|
|
290
|
-
await fs.copyFile(filePath, backupPath)
|
|
291
|
-
|
|
292
|
-
this.logger.debug({ filePath, backupPath }, 'Backup created')
|
|
293
|
-
return backupPath
|
|
294
|
-
} catch (error) {
|
|
295
|
-
this.logger.warn({ error, filePath }, 'Failed to create backup')
|
|
296
|
-
// Don't throw - backup failure shouldn't prevent write
|
|
297
|
-
return null
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Check if file exists
|
|
303
|
-
*/
|
|
304
|
-
private async fileExists(filePath: string): Promise<boolean> {
|
|
305
|
-
try {
|
|
306
|
-
await fs.access(filePath)
|
|
307
|
-
return true
|
|
308
|
-
} catch {
|
|
309
|
-
return false
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Delete a file
|
|
315
|
-
*/
|
|
316
|
-
async deleteFile(
|
|
317
|
-
filePath: string,
|
|
318
|
-
createBackup: boolean = true
|
|
319
|
-
): Promise<void> {
|
|
320
|
-
try {
|
|
321
|
-
if (createBackup) {
|
|
322
|
-
await this.createBackup(filePath)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
await fs.unlink(filePath)
|
|
326
|
-
this.logger.info({ filePath }, 'File deleted')
|
|
327
|
-
} catch (error) {
|
|
328
|
-
this.logger.error({ error, filePath }, 'Failed to delete file')
|
|
329
|
-
throw error
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Create directory structure for a new project
|
|
335
|
-
*/
|
|
336
|
-
async createProjectStructure(
|
|
337
|
-
projectPath: string,
|
|
338
|
-
projectName: string
|
|
339
|
-
): Promise<void> {
|
|
340
|
-
try {
|
|
341
|
-
await fs.mkdir(projectPath, { recursive: true })
|
|
342
|
-
|
|
343
|
-
const variables = createProjectVariables(projectName)
|
|
344
|
-
|
|
345
|
-
// Create each file from template
|
|
346
|
-
const files = [
|
|
347
|
-
{ name: 'context.md', template: TEMPLATES.PROJECT_CONTEXT },
|
|
348
|
-
{ name: 'decisions.md', template: TEMPLATES.DECISIONS_LOG },
|
|
349
|
-
{ name: 'progress.md', template: TEMPLATES.PROGRESS_TRACKER },
|
|
350
|
-
{ name: 'standards.md', template: TEMPLATES.CODING_STANDARDS },
|
|
351
|
-
{ name: 'patterns.md', template: TEMPLATES.PATTERNS_LOG },
|
|
352
|
-
{ name: 'corrections.md', template: TEMPLATES.CORRECTIONS_LOG }
|
|
353
|
-
]
|
|
354
|
-
|
|
355
|
-
for (const file of files) {
|
|
356
|
-
const content = renderTemplate(file.template, variables)
|
|
357
|
-
const filePath = path.join(projectPath, file.name)
|
|
358
|
-
|
|
359
|
-
// Parse the rendered template to separate frontmatter and content
|
|
360
|
-
const { data, content: body } = matter(content)
|
|
361
|
-
|
|
362
|
-
await this.writeMarkdownFile(filePath, data as Frontmatter, body, {
|
|
363
|
-
createBackup: false,
|
|
364
|
-
ensureDirectory: false
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
this.logger.info({ projectPath, projectName }, 'Project structure created')
|
|
369
|
-
} catch (error) {
|
|
370
|
-
this.logger.error({ error, projectPath }, 'Failed to create project')
|
|
371
|
-
throw error
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Clean old backups (keep only recent ones)
|
|
377
|
-
*/
|
|
378
|
-
async cleanOldBackups(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> {
|
|
379
|
-
try {
|
|
380
|
-
const entries = await fs.readdir(this.backupDir, { withFileTypes: true })
|
|
381
|
-
const now = Date.now()
|
|
382
|
-
let cleaned = 0
|
|
383
|
-
|
|
384
|
-
for (const entry of entries) {
|
|
385
|
-
if (!entry.isFile() || !entry.name.endsWith('.bak')) continue
|
|
386
|
-
|
|
387
|
-
const filePath = path.join(this.backupDir, entry.name)
|
|
388
|
-
const stats = await fs.stat(filePath)
|
|
389
|
-
|
|
390
|
-
if (now - stats.mtimeMs > maxAge) {
|
|
391
|
-
await fs.unlink(filePath)
|
|
392
|
-
cleaned++
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (cleaned > 0) {
|
|
397
|
-
this.logger.info({ cleaned }, 'Old backups cleaned')
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return cleaned
|
|
401
|
-
} catch (error) {
|
|
402
|
-
this.logger.warn({ error }, 'Failed to clean old backups')
|
|
403
|
-
return 0
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Escape special regex characters
|
|
409
|
-
*/
|
|
410
|
-
private escapeRegex(str: string): string {
|
|
411
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
412
|
-
}
|
|
413
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Vault File Writer
|
|
3
|
+
* Utilities for safely updating vault content with atomic writes and backups
|
|
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 { Frontmatter } from './types'
|
|
11
|
+
import { TEMPLATES, renderTemplate, createProjectVariables, getTodayDate } from './templates'
|
|
12
|
+
import { RetryManager } from '@/utils'
|
|
13
|
+
|
|
14
|
+
export interface WriteOptions {
|
|
15
|
+
createBackup?: boolean
|
|
16
|
+
ensureDirectory?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class VaultWriter {
|
|
20
|
+
private logger: Logger
|
|
21
|
+
private backupDir: string
|
|
22
|
+
private retryManager: RetryManager
|
|
23
|
+
|
|
24
|
+
constructor(logger: Logger, backupDir: string) {
|
|
25
|
+
this.logger = logger.child({ component: 'vault-writer' })
|
|
26
|
+
this.backupDir = backupDir
|
|
27
|
+
this.retryManager = new RetryManager(logger)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Write markdown file with frontmatter atomically
|
|
32
|
+
* Falls back to direct write if rename fails (e.g., Obsidian has file open)
|
|
33
|
+
*/
|
|
34
|
+
async writeMarkdownFile(
|
|
35
|
+
filePath: string,
|
|
36
|
+
frontmatter: Frontmatter,
|
|
37
|
+
content: string,
|
|
38
|
+
options: WriteOptions = {}
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
return this.retryManager.execute(
|
|
41
|
+
async () => {
|
|
42
|
+
const { createBackup = true, ensureDirectory = true } = options
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
if (createBackup && (await this.fileExists(filePath))) {
|
|
46
|
+
await this.createBackup(filePath)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fileContent = matter.stringify(content, frontmatter)
|
|
50
|
+
|
|
51
|
+
if (ensureDirectory) {
|
|
52
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Try atomic write first (temp file + rename)
|
|
56
|
+
const tempPath = `${filePath}.tmp`
|
|
57
|
+
try {
|
|
58
|
+
await fs.writeFile(tempPath, fileContent, 'utf-8')
|
|
59
|
+
await fs.rename(tempPath, filePath)
|
|
60
|
+
} catch (renameError) {
|
|
61
|
+
// If rename fails (EPERM - file locked by Obsidian), write directly
|
|
62
|
+
const isPermError = (renameError as NodeJS.ErrnoException).code === 'EPERM'
|
|
63
|
+
if (isPermError) {
|
|
64
|
+
this.logger.warn({ filePath }, 'Atomic write failed (file locked), using direct write')
|
|
65
|
+
await fs.writeFile(filePath, fileContent, 'utf-8')
|
|
66
|
+
// Clean up temp file
|
|
67
|
+
try {
|
|
68
|
+
await fs.unlink(tempPath)
|
|
69
|
+
} catch {}
|
|
70
|
+
} else {
|
|
71
|
+
throw renameError
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.logger.info({ filePath }, 'File written successfully')
|
|
76
|
+
} catch (error) {
|
|
77
|
+
try {
|
|
78
|
+
await fs.unlink(`${filePath}.tmp`)
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.logger.error({ error, filePath }, 'Failed to write file')
|
|
83
|
+
throw new Error(`Failed to write file ${filePath}: ${error}`)
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
maxRetries: 3,
|
|
88
|
+
retryCondition: RetryManager.isRetryableError,
|
|
89
|
+
onRetry: (error, attempt) => {
|
|
90
|
+
this.logger.warn(
|
|
91
|
+
{ error: error.message, attempt, filePath },
|
|
92
|
+
'Retrying file write'
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Update only the frontmatter of a file
|
|
101
|
+
*/
|
|
102
|
+
async updateFrontmatter(
|
|
103
|
+
filePath: string,
|
|
104
|
+
updates: Partial<Frontmatter>
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
// Read existing file
|
|
108
|
+
const rawContent = await fs.readFile(filePath, 'utf-8')
|
|
109
|
+
const { data, content } = matter(rawContent)
|
|
110
|
+
|
|
111
|
+
// Merge updates and update timestamp
|
|
112
|
+
const newFrontmatter: Frontmatter = {
|
|
113
|
+
...(data as Frontmatter),
|
|
114
|
+
...updates,
|
|
115
|
+
updated: getTodayDate()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Write back
|
|
119
|
+
await this.writeMarkdownFile(filePath, newFrontmatter, content, {
|
|
120
|
+
createBackup: true
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
this.logger.info({ filePath, updates }, 'Frontmatter updated')
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.logger.error({ error, filePath }, 'Failed to update frontmatter')
|
|
126
|
+
throw error
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Append content to a file
|
|
132
|
+
* Creates the file (and parent directories) if it doesn't exist
|
|
133
|
+
*/
|
|
134
|
+
async appendContent(
|
|
135
|
+
filePath: string,
|
|
136
|
+
newContent: string,
|
|
137
|
+
separator: string = '\n\n'
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
try {
|
|
140
|
+
let rawContent: string
|
|
141
|
+
try {
|
|
142
|
+
rawContent = await fs.readFile(filePath, 'utf-8')
|
|
143
|
+
} catch (readError: any) {
|
|
144
|
+
if (readError.code === 'ENOENT') {
|
|
145
|
+
// File doesn't exist — create it with the new content
|
|
146
|
+
const today = getTodayDate()
|
|
147
|
+
const frontmatter: Frontmatter = {
|
|
148
|
+
created: today,
|
|
149
|
+
updated: today
|
|
150
|
+
}
|
|
151
|
+
await this.writeMarkdownFile(filePath, frontmatter, newContent, {
|
|
152
|
+
createBackup: false,
|
|
153
|
+
ensureDirectory: true
|
|
154
|
+
})
|
|
155
|
+
this.logger.info({ filePath }, 'File created with initial content')
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
throw readError
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { data, content } = matter(rawContent)
|
|
162
|
+
|
|
163
|
+
// Append new content
|
|
164
|
+
const updatedContent = content.trim() + separator + newContent
|
|
165
|
+
|
|
166
|
+
// Update timestamp in frontmatter
|
|
167
|
+
const today = getTodayDate()
|
|
168
|
+
const updatedFrontmatter: Frontmatter = {
|
|
169
|
+
...(data as Frontmatter),
|
|
170
|
+
updated: today,
|
|
171
|
+
last_updated: today
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Write back
|
|
175
|
+
await this.writeMarkdownFile(filePath, updatedFrontmatter, updatedContent, {
|
|
176
|
+
createBackup: true
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
this.logger.info({ filePath }, 'Content appended')
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.logger.error({ error, filePath }, 'Failed to append content')
|
|
182
|
+
throw error
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Prepend content to a file (after frontmatter)
|
|
188
|
+
*/
|
|
189
|
+
async prependContent(
|
|
190
|
+
filePath: string,
|
|
191
|
+
newContent: string,
|
|
192
|
+
separator: string = '\n\n'
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
// Read existing file
|
|
196
|
+
const rawContent = await fs.readFile(filePath, 'utf-8')
|
|
197
|
+
const { data, content } = matter(rawContent)
|
|
198
|
+
|
|
199
|
+
// Prepend new content
|
|
200
|
+
const updatedContent = newContent + separator + content.trim()
|
|
201
|
+
|
|
202
|
+
// Update timestamp in frontmatter
|
|
203
|
+
const updatedFrontmatter: Frontmatter = {
|
|
204
|
+
...(data as Frontmatter),
|
|
205
|
+
updated: getTodayDate()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Write back
|
|
209
|
+
await this.writeMarkdownFile(filePath, updatedFrontmatter, updatedContent, {
|
|
210
|
+
createBackup: true
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
this.logger.info({ filePath }, 'Content prepended')
|
|
214
|
+
} catch (error) {
|
|
215
|
+
this.logger.error({ error, filePath }, 'Failed to prepend content')
|
|
216
|
+
throw error
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Replace a section in a file by header
|
|
222
|
+
*/
|
|
223
|
+
async replaceSection(
|
|
224
|
+
filePath: string,
|
|
225
|
+
sectionHeader: string,
|
|
226
|
+
newSectionContent: string
|
|
227
|
+
): Promise<boolean> {
|
|
228
|
+
try {
|
|
229
|
+
const rawContent = await fs.readFile(filePath, 'utf-8')
|
|
230
|
+
const { data, content } = matter(rawContent)
|
|
231
|
+
|
|
232
|
+
// Find the section using regex
|
|
233
|
+
const headerPattern = new RegExp(
|
|
234
|
+
`(^|\\n)(#{1,6}\\s*${this.escapeRegex(sectionHeader)}\\s*\\n)`,
|
|
235
|
+
'i'
|
|
236
|
+
)
|
|
237
|
+
const match = content.match(headerPattern)
|
|
238
|
+
|
|
239
|
+
if (!match || !match[1] || !match[2]) {
|
|
240
|
+
this.logger.warn({ filePath, sectionHeader }, 'Section not found')
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Find the end of the section (next header of same or higher level)
|
|
245
|
+
const headerMatch = match[2].match(/^#+/)
|
|
246
|
+
const headerLevel = headerMatch?.[0]?.length ?? 1
|
|
247
|
+
const startIndex = (match.index ?? 0) + match[0].length
|
|
248
|
+
const remainingContent = content.substring(startIndex)
|
|
249
|
+
|
|
250
|
+
const nextHeaderPattern = new RegExp(`\\n#{1,${headerLevel}}\\s+`)
|
|
251
|
+
const nextMatch = remainingContent.match(nextHeaderPattern)
|
|
252
|
+
const endIndex = nextMatch
|
|
253
|
+
? startIndex + (nextMatch.index ?? 0)
|
|
254
|
+
: content.length
|
|
255
|
+
|
|
256
|
+
// Build new content
|
|
257
|
+
const before = content.substring(0, (match.index ?? 0) + match[1].length)
|
|
258
|
+
const sectionTitle = match[2]
|
|
259
|
+
const after = content.substring(endIndex)
|
|
260
|
+
const updatedContent = before + sectionTitle + newSectionContent + after
|
|
261
|
+
|
|
262
|
+
// Update frontmatter timestamp
|
|
263
|
+
const updatedFrontmatter: Frontmatter = {
|
|
264
|
+
...(data as Frontmatter),
|
|
265
|
+
updated: getTodayDate()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await this.writeMarkdownFile(filePath, updatedFrontmatter, updatedContent, {
|
|
269
|
+
createBackup: true
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
this.logger.info({ filePath, sectionHeader }, 'Section replaced')
|
|
273
|
+
return true
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this.logger.error({ error, filePath }, 'Failed to replace section')
|
|
276
|
+
throw error
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Create a backup of a file
|
|
282
|
+
*/
|
|
283
|
+
private async createBackup(filePath: string): Promise<string | null> {
|
|
284
|
+
try {
|
|
285
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
286
|
+
const basename = path.basename(filePath)
|
|
287
|
+
const backupPath = path.join(this.backupDir, `${basename}.${timestamp}.bak`)
|
|
288
|
+
|
|
289
|
+
await fs.mkdir(this.backupDir, { recursive: true })
|
|
290
|
+
await fs.copyFile(filePath, backupPath)
|
|
291
|
+
|
|
292
|
+
this.logger.debug({ filePath, backupPath }, 'Backup created')
|
|
293
|
+
return backupPath
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.logger.warn({ error, filePath }, 'Failed to create backup')
|
|
296
|
+
// Don't throw - backup failure shouldn't prevent write
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if file exists
|
|
303
|
+
*/
|
|
304
|
+
private async fileExists(filePath: string): Promise<boolean> {
|
|
305
|
+
try {
|
|
306
|
+
await fs.access(filePath)
|
|
307
|
+
return true
|
|
308
|
+
} catch {
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Delete a file
|
|
315
|
+
*/
|
|
316
|
+
async deleteFile(
|
|
317
|
+
filePath: string,
|
|
318
|
+
createBackup: boolean = true
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
try {
|
|
321
|
+
if (createBackup) {
|
|
322
|
+
await this.createBackup(filePath)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await fs.unlink(filePath)
|
|
326
|
+
this.logger.info({ filePath }, 'File deleted')
|
|
327
|
+
} catch (error) {
|
|
328
|
+
this.logger.error({ error, filePath }, 'Failed to delete file')
|
|
329
|
+
throw error
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Create directory structure for a new project
|
|
335
|
+
*/
|
|
336
|
+
async createProjectStructure(
|
|
337
|
+
projectPath: string,
|
|
338
|
+
projectName: string
|
|
339
|
+
): Promise<void> {
|
|
340
|
+
try {
|
|
341
|
+
await fs.mkdir(projectPath, { recursive: true })
|
|
342
|
+
|
|
343
|
+
const variables = createProjectVariables(projectName)
|
|
344
|
+
|
|
345
|
+
// Create each file from template
|
|
346
|
+
const files = [
|
|
347
|
+
{ name: 'context.md', template: TEMPLATES.PROJECT_CONTEXT },
|
|
348
|
+
{ name: 'decisions.md', template: TEMPLATES.DECISIONS_LOG },
|
|
349
|
+
{ name: 'progress.md', template: TEMPLATES.PROGRESS_TRACKER },
|
|
350
|
+
{ name: 'standards.md', template: TEMPLATES.CODING_STANDARDS },
|
|
351
|
+
{ name: 'patterns.md', template: TEMPLATES.PATTERNS_LOG },
|
|
352
|
+
{ name: 'corrections.md', template: TEMPLATES.CORRECTIONS_LOG }
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
for (const file of files) {
|
|
356
|
+
const content = renderTemplate(file.template, variables)
|
|
357
|
+
const filePath = path.join(projectPath, file.name)
|
|
358
|
+
|
|
359
|
+
// Parse the rendered template to separate frontmatter and content
|
|
360
|
+
const { data, content: body } = matter(content)
|
|
361
|
+
|
|
362
|
+
await this.writeMarkdownFile(filePath, data as Frontmatter, body, {
|
|
363
|
+
createBackup: false,
|
|
364
|
+
ensureDirectory: false
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.logger.info({ projectPath, projectName }, 'Project structure created')
|
|
369
|
+
} catch (error) {
|
|
370
|
+
this.logger.error({ error, projectPath }, 'Failed to create project')
|
|
371
|
+
throw error
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Clean old backups (keep only recent ones)
|
|
377
|
+
*/
|
|
378
|
+
async cleanOldBackups(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> {
|
|
379
|
+
try {
|
|
380
|
+
const entries = await fs.readdir(this.backupDir, { withFileTypes: true })
|
|
381
|
+
const now = Date.now()
|
|
382
|
+
let cleaned = 0
|
|
383
|
+
|
|
384
|
+
for (const entry of entries) {
|
|
385
|
+
if (!entry.isFile() || !entry.name.endsWith('.bak')) continue
|
|
386
|
+
|
|
387
|
+
const filePath = path.join(this.backupDir, entry.name)
|
|
388
|
+
const stats = await fs.stat(filePath)
|
|
389
|
+
|
|
390
|
+
if (now - stats.mtimeMs > maxAge) {
|
|
391
|
+
await fs.unlink(filePath)
|
|
392
|
+
cleaned++
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (cleaned > 0) {
|
|
397
|
+
this.logger.info({ cleaned }, 'Old backups cleaned')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return cleaned
|
|
401
|
+
} catch (error) {
|
|
402
|
+
this.logger.warn({ error }, 'Failed to clean old backups')
|
|
403
|
+
return 0
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Escape special regex characters
|
|
409
|
+
*/
|
|
410
|
+
private escapeRegex(str: string): string {
|
|
411
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
412
|
+
}
|
|
413
|
+
}
|