claude-brain 0.30.2 → 0.30.3
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 +241 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +29 -29
- package/package.json +7 -3
- 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/scripts/postinstall.mjs +531 -531
- package/src/automation/decision-detector.ts +452 -452
- 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 +210 -205
- package/src/cli/auto-setup.ts +75 -75
- package/src/cli/auto-start.ts +266 -266
- package/src/cli/bin.ts +264 -264
- package/src/cli/commands/autostart.ts +90 -90
- package/src/cli/commands/chroma.ts +578 -577
- package/src/cli/commands/export-training.ts +70 -70
- package/src/cli/commands/export.ts +130 -130
- package/src/cli/commands/git-hook.ts +183 -183
- package/src/cli/commands/hooks.ts +217 -217
- package/src/cli/commands/init.ts +123 -123
- package/src/cli/commands/install-mcp.ts +122 -111
- package/src/cli/commands/models.ts +979 -979
- package/src/cli/commands/pack.ts +200 -200
- package/src/cli/commands/refresh.ts +344 -339
- package/src/cli/commands/reindex.ts +120 -120
- package/src/cli/commands/serve.ts +466 -463
- package/src/cli/commands/start.ts +44 -44
- package/src/cli/commands/status.ts +220 -203
- package/src/cli/commands/uninstall-mcp.ts +45 -41
- package/src/cli/commands/update.ts +130 -124
- package/src/cli/migrate-chroma.ts +106 -106
- 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/code-intelligence/indexer.ts +352 -352
- package/src/code-intelligence/linker.ts +178 -178
- package/src/code-intelligence/parser.ts +484 -484
- package/src/code-intelligence/query.ts +291 -291
- package/src/code-intelligence/schema.ts +83 -83
- package/src/code-intelligence/types.ts +95 -95
- package/src/config/defaults.ts +52 -52
- package/src/config/home.ts +56 -56
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +192 -192
- package/src/config/schema.ts +446 -415
- package/src/config/validator.ts +182 -182
- package/src/context/assembler.ts +407 -400
- package/src/context/index.ts +79 -79
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +122 -121
- package/src/health/index.ts +233 -232
- package/src/hooks/brain-hook.ts +134 -131
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/claude-code-mastery.md +112 -112
- package/src/hooks/context-hook.ts +260 -245
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +211 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +306 -288
- package/src/hooks/interceptor-hook.ts +204 -201
- package/src/hooks/passive-classifier.ts +397 -397
- package/src/hooks/queue.ts +160 -129
- package/src/hooks/session-tracker.ts +312 -312
- package/src/hooks/types.ts +52 -52
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +7 -7
- package/src/intelligence/hf-downloader.ts +222 -222
- package/src/intelligence/hf-manifest.json +78 -78
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/inference-router.ts +762 -762
- package/src/intelligence/model-manager.ts +263 -245
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +213 -207
- package/src/intelligence/prediction/index.ts +7 -7
- package/src/intelligence/prediction/recommender.ts +276 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
- package/src/intelligence/reasoning/index.ts +7 -7
- package/src/intelligence/temporal/evolution.ts +193 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +272 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/intelligence/tokenizer.ts +118 -118
- package/src/knowledge/entity-extractor.ts +447 -443
- 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 +166 -166
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +211 -192
- package/src/memory/chroma/collection-manager.ts +92 -92
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +177 -175
- 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 +319 -315
- package/src/memory/chroma/store.ts +755 -747
- package/src/memory/compression.ts +121 -121
- package/src/memory/consolidation/archiver.ts +162 -165
- package/src/memory/consolidation/merger.ts +182 -186
- package/src/memory/consolidation/scorer.ts +136 -136
- package/src/memory/database.ts +9 -0
- package/src/memory/dual-write.ts +145 -0
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +347 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/fts5-search.ts +692 -633
- package/src/memory/index.ts +943 -1060
- package/src/memory/migrations/add-fts5.ts +118 -108
- package/src/memory/patterns.ts +438 -438
- package/src/memory/pruning.ts +60 -60
- package/src/memory/schema.ts +88 -88
- package/src/memory/store.ts +911 -787
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- 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 -297
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +221 -221
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +221 -221
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +165 -165
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +203 -203
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +252 -252
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +189 -188
- package/src/retrieval/reranker/model.ts +99 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +454 -454
- package/src/routing/handlers/exploration-handler.ts +369 -0
- package/src/routing/handlers/index.ts +19 -0
- package/src/routing/handlers/memory-handler.ts +273 -0
- package/src/routing/handlers/mutation-handler.ts +241 -0
- package/src/routing/handlers/recall-handler.ts +642 -0
- package/src/routing/handlers/shared.ts +515 -0
- package/src/routing/handlers/types.ts +48 -0
- package/src/routing/intent-classifier.ts +552 -552
- package/src/routing/response-filter.ts +399 -391
- package/src/routing/router.ts +245 -2193
- package/src/routing/search-engine.ts +521 -514
- package/src/routing/types.ts +104 -94
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/auto-updater.ts +283 -276
- package/src/server/handlers/call-tool.ts +159 -159
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/auto-remember.ts +165 -165
- package/src/server/handlers/tools/brain.ts +86 -86
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/get-code-standards.ts +123 -123
- package/src/server/handlers/tools/get-corrections.ts +152 -152
- package/src/server/handlers/tools/get-patterns.ts +156 -156
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/index.ts +30 -30
- package/src/server/handlers/tools/init-project.ts +756 -756
- package/src/server/handlers/tools/list-projects.ts +126 -126
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +132 -132
- package/src/server/handlers/tools/record-correction.ts +131 -131
- package/src/server/handlers/tools/remember-decision.ts +168 -168
- package/src/server/handlers/tools/schemas.ts +179 -179
- package/src/server/handlers/tools/search-code.ts +122 -122
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/http-api.ts +215 -1229
- package/src/server/mcp-proxy.ts +85 -84
- package/src/server/mcp-server.ts +285 -284
- package/src/server/middleware/auth.ts +39 -0
- package/src/server/middleware/error-handler.ts +37 -0
- package/src/server/middleware/rate-limit.ts +53 -0
- package/src/server/middleware/validate.ts +42 -0
- package/src/server/pid-manager.ts +137 -136
- package/src/server/providers/resources.ts +581 -581
- package/src/server/routes/code.ts +228 -0
- package/src/server/routes/context.ts +26 -0
- package/src/server/routes/health.ts +19 -0
- package/src/server/routes/helpers.ts +100 -0
- package/src/server/routes/hooks.ts +197 -0
- package/src/server/routes/mcp.ts +47 -0
- package/src/server/routes/memory.ts +397 -0
- package/src/server/routes/models.ts +96 -0
- package/src/server/routes/projects.ts +89 -0
- package/src/server/routes/types.ts +21 -0
- package/src/server/schemas/api-schemas.ts +202 -0
- package/src/server/services.ts +720 -720
- package/src/server/utils/memory-indicator.ts +84 -84
- package/src/server/utils/response-formatter.ts +129 -129
- package/src/server/web-viewer.ts +1145 -1115
- package/src/setup/index.ts +38 -38
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.ts +666 -666
- package/src/tools/types.ts +412 -412
- package/src/training/data-store.ts +320 -298
- package/src/training/retrain-pipeline.ts +399 -394
- package/src/utils/error-handler.ts +136 -136
- package/src/utils/index.ts +58 -58
- package/src/utils/kill-port.ts +55 -53
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/safe-path.ts +43 -0
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/index.ts +4 -3
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +4 -1
- package/src/vault/reader.ts +44 -1
- package/src/vault/watcher.ts +24 -1
- package/src/vault/writer.ts +487 -413
- package/skills/persistent-memory/SKILL.md +0 -148
- package/skills/persistent-memory/references/tool-reference.md +0 -90
package/src/utils/timing.ts
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Timing Utility
|
|
3
|
-
* Phase 22: Wraps async operations with performance timing.
|
|
4
|
-
* Logs warnings for operations exceeding 200ms.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const SLOW_THRESHOLD_MS = 200
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Wrap an async operation with timing instrumentation.
|
|
11
|
-
* Logs a warning if the operation exceeds 200ms.
|
|
12
|
-
*/
|
|
13
|
-
export async function timed<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
14
|
-
const start = performance.now()
|
|
15
|
-
try {
|
|
16
|
-
const result = await fn()
|
|
17
|
-
const elapsed = performance.now() - start
|
|
18
|
-
if (elapsed > SLOW_THRESHOLD_MS) {
|
|
19
|
-
// Use console.error to avoid interfering with MCP stdio
|
|
20
|
-
console.error(`[SLOW] ${label}: ${Math.round(elapsed)}ms (budget: ${SLOW_THRESHOLD_MS}ms)`)
|
|
21
|
-
}
|
|
22
|
-
return result
|
|
23
|
-
} catch (error) {
|
|
24
|
-
const elapsed = performance.now() - start
|
|
25
|
-
console.error(`[ERROR] ${label}: ${Math.round(elapsed)}ms — ${error instanceof Error ? error.message : 'unknown'}`)
|
|
26
|
-
throw error
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Synchronous version for non-async operations.
|
|
32
|
-
*/
|
|
33
|
-
export function timedSync<T>(label: string, fn: () => T): T {
|
|
34
|
-
const start = performance.now()
|
|
35
|
-
try {
|
|
36
|
-
const result = fn()
|
|
37
|
-
const elapsed = performance.now() - start
|
|
38
|
-
if (elapsed > SLOW_THRESHOLD_MS) {
|
|
39
|
-
console.error(`[SLOW] ${label}: ${Math.round(elapsed)}ms (budget: ${SLOW_THRESHOLD_MS}ms)`)
|
|
40
|
-
}
|
|
41
|
-
return result
|
|
42
|
-
} catch (error) {
|
|
43
|
-
const elapsed = performance.now() - start
|
|
44
|
-
console.error(`[ERROR] ${label}: ${Math.round(elapsed)}ms — ${error instanceof Error ? error.message : 'unknown'}`)
|
|
45
|
-
throw error
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Timing Utility
|
|
3
|
+
* Phase 22: Wraps async operations with performance timing.
|
|
4
|
+
* Logs warnings for operations exceeding 200ms.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const SLOW_THRESHOLD_MS = 200
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wrap an async operation with timing instrumentation.
|
|
11
|
+
* Logs a warning if the operation exceeds 200ms.
|
|
12
|
+
*/
|
|
13
|
+
export async function timed<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
14
|
+
const start = performance.now()
|
|
15
|
+
try {
|
|
16
|
+
const result = await fn()
|
|
17
|
+
const elapsed = performance.now() - start
|
|
18
|
+
if (elapsed > SLOW_THRESHOLD_MS) {
|
|
19
|
+
// Use console.error to avoid interfering with MCP stdio
|
|
20
|
+
console.error(`[SLOW] ${label}: ${Math.round(elapsed)}ms (budget: ${SLOW_THRESHOLD_MS}ms)`)
|
|
21
|
+
}
|
|
22
|
+
return result
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const elapsed = performance.now() - start
|
|
25
|
+
console.error(`[ERROR] ${label}: ${Math.round(elapsed)}ms — ${error instanceof Error ? error.message : 'unknown'}`)
|
|
26
|
+
throw error
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Synchronous version for non-async operations.
|
|
32
|
+
*/
|
|
33
|
+
export function timedSync<T>(label: string, fn: () => T): T {
|
|
34
|
+
const start = performance.now()
|
|
35
|
+
try {
|
|
36
|
+
const result = fn()
|
|
37
|
+
const elapsed = performance.now() - start
|
|
38
|
+
if (elapsed > SLOW_THRESHOLD_MS) {
|
|
39
|
+
console.error(`[SLOW] ${label}: ${Math.round(elapsed)}ms (budget: ${SLOW_THRESHOLD_MS}ms)`)
|
|
40
|
+
}
|
|
41
|
+
return result
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const elapsed = performance.now() - start
|
|
44
|
+
console.error(`[ERROR] ${label}: ${Math.round(elapsed)}ms — ${error instanceof Error ? error.message : 'unknown'}`)
|
|
45
|
+
throw error
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/utils/transaction.ts
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import type { Logger } from 'pino'
|
|
2
|
-
|
|
3
|
-
export class TransactionManager {
|
|
4
|
-
private logger: Logger
|
|
5
|
-
private operations: Array<() => Promise<void>> = []
|
|
6
|
-
private rollbacks: Array<() => Promise<void>> = []
|
|
7
|
-
|
|
8
|
-
constructor(logger: Logger) {
|
|
9
|
-
this.logger = logger.child({ component: 'transaction-manager' })
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
add(
|
|
13
|
-
operation: () => Promise<void>,
|
|
14
|
-
rollback: () => Promise<void>
|
|
15
|
-
): void {
|
|
16
|
-
this.operations.push(operation)
|
|
17
|
-
this.rollbacks.unshift(rollback)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async execute(): Promise<void> {
|
|
21
|
-
const executedCount = 0
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
for (let i = 0; i < this.operations.length; i++) {
|
|
25
|
-
await this.operations[i]!()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
this.logger.info(
|
|
29
|
-
{ operationCount: this.operations.length },
|
|
30
|
-
'Transaction completed successfully'
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
} catch (error) {
|
|
34
|
-
this.logger.error(
|
|
35
|
-
{ error, executedCount },
|
|
36
|
-
'Transaction failed, rolling back'
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
await this.rollback()
|
|
40
|
-
throw error
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private async rollback(): Promise<void> {
|
|
45
|
-
for (const rollback of this.rollbacks) {
|
|
46
|
-
try {
|
|
47
|
-
await rollback()
|
|
48
|
-
} catch (error) {
|
|
49
|
-
this.logger.error(
|
|
50
|
-
{ error },
|
|
51
|
-
'Rollback operation failed'
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
this.logger.info('Rollback completed')
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
clear(): void {
|
|
60
|
-
this.operations = []
|
|
61
|
-
this.rollbacks = []
|
|
62
|
-
}
|
|
63
|
-
}
|
|
1
|
+
import type { Logger } from 'pino'
|
|
2
|
+
|
|
3
|
+
export class TransactionManager {
|
|
4
|
+
private logger: Logger
|
|
5
|
+
private operations: Array<() => Promise<void>> = []
|
|
6
|
+
private rollbacks: Array<() => Promise<void>> = []
|
|
7
|
+
|
|
8
|
+
constructor(logger: Logger) {
|
|
9
|
+
this.logger = logger.child({ component: 'transaction-manager' })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
add(
|
|
13
|
+
operation: () => Promise<void>,
|
|
14
|
+
rollback: () => Promise<void>
|
|
15
|
+
): void {
|
|
16
|
+
this.operations.push(operation)
|
|
17
|
+
this.rollbacks.unshift(rollback)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async execute(): Promise<void> {
|
|
21
|
+
const executedCount = 0
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
for (let i = 0; i < this.operations.length; i++) {
|
|
25
|
+
await this.operations[i]!()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.logger.info(
|
|
29
|
+
{ operationCount: this.operations.length },
|
|
30
|
+
'Transaction completed successfully'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
} catch (error) {
|
|
34
|
+
this.logger.error(
|
|
35
|
+
{ error, executedCount },
|
|
36
|
+
'Transaction failed, rolling back'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
await this.rollback()
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async rollback(): Promise<void> {
|
|
45
|
+
for (const rollback of this.rollbacks) {
|
|
46
|
+
try {
|
|
47
|
+
await rollback()
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.logger.error(
|
|
50
|
+
{ error },
|
|
51
|
+
'Rollback operation failed'
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.logger.info('Rollback completed')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
clear(): void {
|
|
60
|
+
this.operations = []
|
|
61
|
+
this.rollbacks = []
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/vault/index.ts
CHANGED
|
@@ -52,10 +52,11 @@ export class VaultManager {
|
|
|
52
52
|
this.paths = VaultPathUtils.getVaultPaths(vaultRoot)
|
|
53
53
|
this.reader = new VaultReader(logger, {
|
|
54
54
|
maxCacheSize: 100,
|
|
55
|
-
cacheMaxAge: 5 * 60 * 1000
|
|
55
|
+
cacheMaxAge: 5 * 60 * 1000,
|
|
56
|
+
baseDir: vaultRoot
|
|
56
57
|
})
|
|
57
|
-
this.writer = new VaultWriter(logger, this.config.backupDir)
|
|
58
|
-
this.watcher = new VaultWatcher(logger, this.config.debounceMs)
|
|
58
|
+
this.writer = new VaultWriter(logger, this.config.backupDir, vaultRoot)
|
|
59
|
+
this.watcher = new VaultWatcher(logger, this.config.debounceMs, vaultRoot)
|
|
59
60
|
this.query = new VaultQuery(this.reader, logger)
|
|
60
61
|
|
|
61
62
|
// Wire up watcher to invalidate cache on file changes
|
package/src/vault/paths.ts
CHANGED
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vault Path Utilities
|
|
3
|
-
* Utilities for working with vault directory structure and paths
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import path from 'path'
|
|
7
|
-
import type { VaultPaths, ProjectPaths } from './types'
|
|
8
|
-
|
|
9
|
-
export class VaultPathUtils {
|
|
10
|
-
/**
|
|
11
|
-
* Get all vault paths from root
|
|
12
|
-
*/
|
|
13
|
-
static getVaultPaths(vaultRoot: string): VaultPaths {
|
|
14
|
-
return {
|
|
15
|
-
root: vaultRoot,
|
|
16
|
-
projects: path.join(vaultRoot, 'Projects'),
|
|
17
|
-
global: path.join(vaultRoot, 'Global'),
|
|
18
|
-
templates: path.join(vaultRoot, 'Templates'),
|
|
19
|
-
memory: path.join(vaultRoot, 'Memory')
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get all paths for a specific project
|
|
25
|
-
*/
|
|
26
|
-
static getProjectPaths(
|
|
27
|
-
vaultRoot: string,
|
|
28
|
-
projectName: string
|
|
29
|
-
): ProjectPaths {
|
|
30
|
-
const projectRoot = path.join(vaultRoot, 'Projects', projectName)
|
|
31
|
-
return {
|
|
32
|
-
root: projectRoot,
|
|
33
|
-
context: path.join(projectRoot, 'context.md'),
|
|
34
|
-
decisions: path.join(projectRoot, 'decisions.md'),
|
|
35
|
-
progress: path.join(projectRoot, 'progress.md'),
|
|
36
|
-
standards: path.join(projectRoot, 'standards.md'),
|
|
37
|
-
patterns: path.join(projectRoot, 'patterns.md'),
|
|
38
|
-
corrections: path.join(projectRoot, 'corrections.md')
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Validate project name (kebab-case, no special chars)
|
|
44
|
-
*/
|
|
45
|
-
static isValidProjectName(name: string): boolean {
|
|
46
|
-
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Normalize project name to kebab-case
|
|
51
|
-
*/
|
|
52
|
-
static normalizeProjectName(name: string): string {
|
|
53
|
-
return name
|
|
54
|
-
.toLowerCase()
|
|
55
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
56
|
-
.replace(/^-+|-+$/g, '')
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Extract project name from a file path
|
|
61
|
-
*/
|
|
62
|
-
static extractProjectName(filePath: string, vaultRoot: string): string | null {
|
|
63
|
-
const projectsPath = path.join(vaultRoot, 'Projects')
|
|
64
|
-
const relativePath = path.relative(projectsPath, filePath)
|
|
65
|
-
|
|
66
|
-
if (relativePath.startsWith('..')) {
|
|
67
|
-
return null // File is not inside Projects directory
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const parts = relativePath.split(path.sep)
|
|
71
|
-
const projectName = parts[0]
|
|
72
|
-
return projectName !== undefined && projectName !== '' ? projectName : null
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Check if a path is inside the vault
|
|
77
|
-
*/
|
|
78
|
-
static isInsideVault(filePath: string, vaultRoot: string): boolean {
|
|
79
|
-
const resolvedPath = path.resolve(filePath)
|
|
80
|
-
const resolvedRoot = path.resolve(vaultRoot)
|
|
81
|
-
// Ensure the vault root ends with separator for proper prefix matching
|
|
82
|
-
// This prevents /vault matching /vaultx
|
|
83
|
-
const normalizedRoot = resolvedRoot.endsWith(path.sep)
|
|
84
|
-
? resolvedRoot
|
|
85
|
-
: resolvedRoot + path.sep
|
|
86
|
-
return resolvedPath === resolvedRoot || resolvedPath.startsWith(normalizedRoot)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get the relative path from vault root
|
|
91
|
-
*/
|
|
92
|
-
static getRelativePath(filePath: string, vaultRoot: string): string {
|
|
93
|
-
return path.relative(vaultRoot, filePath)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Join paths safely within the vault
|
|
98
|
-
*/
|
|
99
|
-
static joinVaultPath(vaultRoot: string, ...segments: string[]): string {
|
|
100
|
-
const joined = path.join(vaultRoot, ...segments)
|
|
101
|
-
if (!VaultPathUtils.isInsideVault(joined, vaultRoot)) {
|
|
102
|
-
throw new Error('Path traversal detected: path escapes vault root')
|
|
103
|
-
}
|
|
104
|
-
return joined
|
|
105
|
-
}
|
|
106
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Vault Path Utilities
|
|
3
|
+
* Utilities for working with vault directory structure and paths
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import type { VaultPaths, ProjectPaths } from './types'
|
|
8
|
+
|
|
9
|
+
export class VaultPathUtils {
|
|
10
|
+
/**
|
|
11
|
+
* Get all vault paths from root
|
|
12
|
+
*/
|
|
13
|
+
static getVaultPaths(vaultRoot: string): VaultPaths {
|
|
14
|
+
return {
|
|
15
|
+
root: vaultRoot,
|
|
16
|
+
projects: path.join(vaultRoot, 'Projects'),
|
|
17
|
+
global: path.join(vaultRoot, 'Global'),
|
|
18
|
+
templates: path.join(vaultRoot, 'Templates'),
|
|
19
|
+
memory: path.join(vaultRoot, 'Memory')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get all paths for a specific project
|
|
25
|
+
*/
|
|
26
|
+
static getProjectPaths(
|
|
27
|
+
vaultRoot: string,
|
|
28
|
+
projectName: string
|
|
29
|
+
): ProjectPaths {
|
|
30
|
+
const projectRoot = path.join(vaultRoot, 'Projects', projectName)
|
|
31
|
+
return {
|
|
32
|
+
root: projectRoot,
|
|
33
|
+
context: path.join(projectRoot, 'context.md'),
|
|
34
|
+
decisions: path.join(projectRoot, 'decisions.md'),
|
|
35
|
+
progress: path.join(projectRoot, 'progress.md'),
|
|
36
|
+
standards: path.join(projectRoot, 'standards.md'),
|
|
37
|
+
patterns: path.join(projectRoot, 'patterns.md'),
|
|
38
|
+
corrections: path.join(projectRoot, 'corrections.md')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate project name (kebab-case, no special chars)
|
|
44
|
+
*/
|
|
45
|
+
static isValidProjectName(name: string): boolean {
|
|
46
|
+
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalize project name to kebab-case
|
|
51
|
+
*/
|
|
52
|
+
static normalizeProjectName(name: string): string {
|
|
53
|
+
return name
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
56
|
+
.replace(/^-+|-+$/g, '')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract project name from a file path
|
|
61
|
+
*/
|
|
62
|
+
static extractProjectName(filePath: string, vaultRoot: string): string | null {
|
|
63
|
+
const projectsPath = path.join(vaultRoot, 'Projects')
|
|
64
|
+
const relativePath = path.relative(projectsPath, filePath)
|
|
65
|
+
|
|
66
|
+
if (relativePath.startsWith('..')) {
|
|
67
|
+
return null // File is not inside Projects directory
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parts = relativePath.split(path.sep)
|
|
71
|
+
const projectName = parts[0]
|
|
72
|
+
return projectName !== undefined && projectName !== '' ? projectName : null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a path is inside the vault
|
|
77
|
+
*/
|
|
78
|
+
static isInsideVault(filePath: string, vaultRoot: string): boolean {
|
|
79
|
+
const resolvedPath = path.resolve(filePath)
|
|
80
|
+
const resolvedRoot = path.resolve(vaultRoot)
|
|
81
|
+
// Ensure the vault root ends with separator for proper prefix matching
|
|
82
|
+
// This prevents /vault matching /vaultx
|
|
83
|
+
const normalizedRoot = resolvedRoot.endsWith(path.sep)
|
|
84
|
+
? resolvedRoot
|
|
85
|
+
: resolvedRoot + path.sep
|
|
86
|
+
return resolvedPath === resolvedRoot || resolvedPath.startsWith(normalizedRoot)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the relative path from vault root
|
|
91
|
+
*/
|
|
92
|
+
static getRelativePath(filePath: string, vaultRoot: string): string {
|
|
93
|
+
return path.relative(vaultRoot, filePath)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Join paths safely within the vault
|
|
98
|
+
*/
|
|
99
|
+
static joinVaultPath(vaultRoot: string, ...segments: string[]): string {
|
|
100
|
+
const joined = path.join(vaultRoot, ...segments)
|
|
101
|
+
if (!VaultPathUtils.isInsideVault(joined, vaultRoot)) {
|
|
102
|
+
throw new Error('Path traversal detected: path escapes vault root')
|
|
103
|
+
}
|
|
104
|
+
return joined
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/vault/query.ts
CHANGED
|
@@ -71,7 +71,10 @@ export class VaultQuery {
|
|
|
71
71
|
// Read all files
|
|
72
72
|
const markdownFiles = await Promise.all(
|
|
73
73
|
allFiles.map(f =>
|
|
74
|
-
this.reader.readMarkdownFile(f).catch(() =>
|
|
74
|
+
this.reader.readMarkdownFile(f).catch((error) => {
|
|
75
|
+
this.logger.warn({ error, file: f }, 'Failed to read markdown file')
|
|
76
|
+
return null
|
|
77
|
+
})
|
|
75
78
|
)
|
|
76
79
|
)
|
|
77
80
|
|
package/src/vault/reader.ts
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'path'
|
|
|
8
8
|
import matter from 'gray-matter'
|
|
9
9
|
import type { Logger } from 'pino'
|
|
10
10
|
import type { MarkdownFile, Frontmatter } from './types'
|
|
11
|
+
import { validatePath } from '@/utils/safe-path'
|
|
11
12
|
|
|
12
13
|
export interface CacheStats {
|
|
13
14
|
size: number
|
|
@@ -20,16 +21,18 @@ export class VaultReader {
|
|
|
20
21
|
private logger: Logger
|
|
21
22
|
private maxCacheSize: number
|
|
22
23
|
private cacheMaxAge: number // milliseconds
|
|
24
|
+
private baseDir: string
|
|
23
25
|
|
|
24
26
|
constructor(
|
|
25
27
|
logger: Logger,
|
|
26
|
-
options: { maxCacheSize?: number; cacheMaxAge?: number } = {}
|
|
28
|
+
options: { maxCacheSize?: number; cacheMaxAge?: number; baseDir?: string } = {}
|
|
27
29
|
) {
|
|
28
30
|
this.logger = logger.child({ component: 'vault-reader' })
|
|
29
31
|
this.cache = new Map()
|
|
30
32
|
this.cacheTimestamps = new Map()
|
|
31
33
|
this.maxCacheSize = options.maxCacheSize ?? 100
|
|
32
34
|
this.cacheMaxAge = options.cacheMaxAge ?? 5 * 60 * 1000 // 5 minutes default
|
|
35
|
+
this.baseDir = options.baseDir ?? ''
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
@@ -39,6 +42,16 @@ export class VaultReader {
|
|
|
39
42
|
filePath: string,
|
|
40
43
|
useCache: boolean = true
|
|
41
44
|
): Promise<MarkdownFile> {
|
|
45
|
+
// Validate path against base directory
|
|
46
|
+
if (this.baseDir) {
|
|
47
|
+
try {
|
|
48
|
+
validatePath(filePath, this.baseDir)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.logger.error({ error, filePath, baseDir: this.baseDir }, 'Path traversal blocked in readMarkdownFile')
|
|
51
|
+
throw new Error(`Path validation failed for ${filePath}: ${error}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
// Check cache first
|
|
43
56
|
if (useCache && this.isCacheValid(filePath)) {
|
|
44
57
|
this.logger.debug({ filePath }, 'Cache hit')
|
|
@@ -104,6 +117,16 @@ export class VaultReader {
|
|
|
104
117
|
dirPath: string,
|
|
105
118
|
recursive: boolean = false
|
|
106
119
|
): Promise<string[]> {
|
|
120
|
+
// Validate path against base directory
|
|
121
|
+
if (this.baseDir) {
|
|
122
|
+
try {
|
|
123
|
+
validatePath(dirPath, this.baseDir)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.logger.error({ error, dirPath, baseDir: this.baseDir }, 'Path traversal blocked in readDirectory')
|
|
126
|
+
throw new Error(`Path validation failed for ${dirPath}: ${error}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
try {
|
|
108
131
|
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
109
132
|
let files: string[] = []
|
|
@@ -130,6 +153,16 @@ export class VaultReader {
|
|
|
130
153
|
* Get all project directories
|
|
131
154
|
*/
|
|
132
155
|
async getProjectDirectories(projectsPath: string): Promise<string[]> {
|
|
156
|
+
// Validate path against base directory
|
|
157
|
+
if (this.baseDir) {
|
|
158
|
+
try {
|
|
159
|
+
validatePath(projectsPath, this.baseDir)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this.logger.error({ error, projectsPath, baseDir: this.baseDir }, 'Path traversal blocked in getProjectDirectories')
|
|
162
|
+
throw new Error(`Path validation failed for ${projectsPath}: ${error}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
133
166
|
try {
|
|
134
167
|
const entries = await fs.readdir(projectsPath, { withFileTypes: true })
|
|
135
168
|
return entries
|
|
@@ -149,6 +182,16 @@ export class VaultReader {
|
|
|
149
182
|
created: Date
|
|
150
183
|
modified: Date
|
|
151
184
|
} | null> {
|
|
185
|
+
// Validate path against base directory
|
|
186
|
+
if (this.baseDir) {
|
|
187
|
+
try {
|
|
188
|
+
validatePath(filePath, this.baseDir)
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.error({ error, filePath, baseDir: this.baseDir }, 'Path traversal blocked in getFileStats')
|
|
191
|
+
throw new Error(`Path validation failed for ${filePath}: ${error}`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
152
195
|
try {
|
|
153
196
|
const stats = await fs.stat(filePath)
|
|
154
197
|
return {
|
package/src/vault/watcher.ts
CHANGED
|
@@ -8,6 +8,7 @@ import fs from 'fs/promises'
|
|
|
8
8
|
import path from 'path'
|
|
9
9
|
import { EventEmitter } from 'events'
|
|
10
10
|
import type { Logger } from 'pino'
|
|
11
|
+
import { validatePath } from '@/utils/safe-path'
|
|
11
12
|
|
|
12
13
|
export type FileChangeType = 'created' | 'modified' | 'deleted'
|
|
13
14
|
|
|
@@ -36,14 +37,16 @@ export class VaultWatcher extends EventEmitter {
|
|
|
36
37
|
private debounceTimers: Map<string, NodeJS.Timeout>
|
|
37
38
|
private debounceMs: number
|
|
38
39
|
private fileStates: Map<string, boolean> // Track if file exists
|
|
40
|
+
private baseDir: string
|
|
39
41
|
|
|
40
|
-
constructor(logger: Logger, debounceMs: number = 500) {
|
|
42
|
+
constructor(logger: Logger, debounceMs: number = 500, baseDir: string = '') {
|
|
41
43
|
super()
|
|
42
44
|
this.logger = logger.child({ component: 'vault-watcher' })
|
|
43
45
|
this.watchers = new Map()
|
|
44
46
|
this.debounceTimers = new Map()
|
|
45
47
|
this.debounceMs = debounceMs
|
|
46
48
|
this.fileStates = new Map()
|
|
49
|
+
this.baseDir = baseDir
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/**
|
|
@@ -70,6 +73,16 @@ export class VaultWatcher extends EventEmitter {
|
|
|
70
73
|
* Start watching a directory
|
|
71
74
|
*/
|
|
72
75
|
watch(dirPath: string, recursive: boolean = true): void {
|
|
76
|
+
// Validate path against base directory
|
|
77
|
+
if (this.baseDir) {
|
|
78
|
+
try {
|
|
79
|
+
validatePath(dirPath, this.baseDir)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.logger.error({ error, dirPath, baseDir: this.baseDir }, 'Path traversal blocked in watch')
|
|
82
|
+
throw new Error(`Path validation failed for ${dirPath}: ${error}`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
if (this.watchers.has(dirPath)) {
|
|
74
87
|
this.logger.warn({ dirPath }, 'Already watching directory')
|
|
75
88
|
return
|
|
@@ -105,6 +118,16 @@ export class VaultWatcher extends EventEmitter {
|
|
|
105
118
|
* Stop watching a directory
|
|
106
119
|
*/
|
|
107
120
|
unwatch(dirPath: string): void {
|
|
121
|
+
// Validate path against base directory
|
|
122
|
+
if (this.baseDir) {
|
|
123
|
+
try {
|
|
124
|
+
validatePath(dirPath, this.baseDir)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.logger.error({ error, dirPath, baseDir: this.baseDir }, 'Path traversal blocked in unwatch')
|
|
127
|
+
throw new Error(`Path validation failed for ${dirPath}: ${error}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
108
131
|
const watcher = this.watchers.get(dirPath)
|
|
109
132
|
if (watcher) {
|
|
110
133
|
watcher.close()
|