mdcontext 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +9 -9
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +206 -3
- package/biome.json +1 -1
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +85 -89
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +718 -657
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1533 -1423
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.js +4072 -629
- package/dist/index.d.ts +420 -33
- package/dist/index.js +8 -15
- package/dist/mcp/server.js +103 -7
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +44 -5
- package/docs/020-current-implementation.md +8 -8
- package/docs/021-DOGFOODING-FINDINGS.md +1 -1
- package/docs/CONFIG.md +1123 -0
- package/docs/ERRORS.md +383 -0
- package/docs/summarization.md +320 -0
- package/justfile +40 -0
- package/package.json +39 -33
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -0
- package/research/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -0
- package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
- package/research/research-quality-review.md +834 -0
- package/research/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -0
- package/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +32 -37
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +2 -2
- package/src/cli/cli.test.ts +230 -33
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +97 -9
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +210 -30
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/search.ts +894 -64
- package/src/cli/commands/stats.ts +3 -0
- package/src/cli/commands/tree.ts +26 -5
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +66 -0
- package/src/cli/help.ts +209 -7
- package/src/cli/main.ts +348 -58
- package/src/cli/options.ts +10 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/utils.ts +150 -17
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/types.ts +6 -33
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +2 -0
- package/src/embeddings/openai-provider.ts +332 -83
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +780 -93
- package/src/embeddings/types.ts +293 -16
- package/src/embeddings/vector-store.ts +486 -77
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/indexer.ts +286 -48
- package/src/index/storage.ts +94 -30
- package/src/index/types.ts +40 -2
- package/src/index/watcher.ts +67 -9
- package/src/index.ts +22 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +135 -6
- package/src/parser/parser.ts +18 -19
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +125 -3
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/searcher.test.ts +99 -1
- package/src/search/searcher.ts +189 -67
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -0
- package/src/summarize/summarizer.ts +104 -35
- package/src/types/huggingface-transformers.d.ts +66 -0
- package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
- package/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/vitest.config.ts +1 -6
- package/AGENTS.md +0 -46
- package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/vectors.meta.json +0 -1264
package/src/cli/utils.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fsPromises from 'node:fs/promises'
|
|
8
8
|
import * as path from 'node:path'
|
|
9
|
+
import { Effect } from 'effect'
|
|
10
|
+
import { listNamespaces } from '../embeddings/embedding-namespace.js'
|
|
11
|
+
import { DirectoryWalkError } from '../errors/index.js'
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Format object as JSON string
|
|
@@ -22,7 +25,8 @@ export const isMarkdownFile = (filename: string): boolean => {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
|
-
* Recursively walk directory and collect markdown files
|
|
28
|
+
* Recursively walk directory and collect markdown files (async version).
|
|
29
|
+
* @deprecated Use walkDirEffect for typed error handling
|
|
26
30
|
*/
|
|
27
31
|
export const walkDir = async (dir: string): Promise<string[]> => {
|
|
28
32
|
const files: string[] = []
|
|
@@ -47,6 +51,49 @@ export const walkDir = async (dir: string): Promise<string[]> => {
|
|
|
47
51
|
return files
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Recursively walk directory and collect markdown files.
|
|
56
|
+
*
|
|
57
|
+
* @param dir - Directory to walk
|
|
58
|
+
* @returns List of markdown file paths
|
|
59
|
+
*
|
|
60
|
+
* @throws DirectoryWalkError - Cannot read or traverse directory
|
|
61
|
+
*/
|
|
62
|
+
export const walkDirEffect = (
|
|
63
|
+
dir: string,
|
|
64
|
+
): Effect.Effect<readonly string[], DirectoryWalkError> =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const files: string[] = []
|
|
67
|
+
|
|
68
|
+
const entries = yield* Effect.tryPromise({
|
|
69
|
+
try: () => fsPromises.readdir(dir, { withFileTypes: true }),
|
|
70
|
+
catch: (e) =>
|
|
71
|
+
new DirectoryWalkError({
|
|
72
|
+
path: dir,
|
|
73
|
+
message: `Cannot read directory: ${e instanceof Error ? e.message : String(e)}`,
|
|
74
|
+
cause: e,
|
|
75
|
+
}),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const fullPath = path.join(dir, entry.name)
|
|
80
|
+
|
|
81
|
+
// Skip hidden directories and node_modules
|
|
82
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
const subFiles = yield* walkDirEffect(fullPath)
|
|
88
|
+
files.push(...subFiles)
|
|
89
|
+
} else if (entry.isFile() && isMarkdownFile(entry.name)) {
|
|
90
|
+
files.push(fullPath)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return files
|
|
95
|
+
})
|
|
96
|
+
|
|
50
97
|
/**
|
|
51
98
|
* Check if a query looks like a regex pattern
|
|
52
99
|
*/
|
|
@@ -56,18 +103,66 @@ export const isRegexPattern = (query: string): boolean => {
|
|
|
56
103
|
}
|
|
57
104
|
|
|
58
105
|
/**
|
|
59
|
-
* Check if embeddings exist for a directory
|
|
106
|
+
* Check if embeddings exist for a directory.
|
|
107
|
+
* Checks for namespaced embeddings in .mdcontext/embeddings/<namespace>/vectors.bin
|
|
60
108
|
*/
|
|
61
109
|
export const hasEmbeddings = async (dir: string): Promise<boolean> => {
|
|
62
|
-
const vectorsPath = path.join(dir, '.mdcontext', 'vectors.bin')
|
|
63
110
|
try {
|
|
64
|
-
await
|
|
65
|
-
|
|
111
|
+
const namespaces = await Effect.runPromise(
|
|
112
|
+
listNamespaces(dir).pipe(Effect.catchAll(() => Effect.succeed([]))),
|
|
113
|
+
)
|
|
114
|
+
return namespaces.length > 0
|
|
66
115
|
} catch {
|
|
67
116
|
return false
|
|
68
117
|
}
|
|
69
118
|
}
|
|
70
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Find the nearest parent directory containing an mdcontext index.
|
|
122
|
+
* Searches from the specified directory up to the filesystem root.
|
|
123
|
+
*
|
|
124
|
+
* @param startDir - Directory to start searching from
|
|
125
|
+
* @returns The directory containing the index, or null if not found
|
|
126
|
+
*/
|
|
127
|
+
export const findIndexRoot = async (
|
|
128
|
+
startDir: string,
|
|
129
|
+
): Promise<string | null> => {
|
|
130
|
+
let currentDir = path.resolve(startDir)
|
|
131
|
+
const root = path.parse(currentDir).root
|
|
132
|
+
|
|
133
|
+
while (currentDir !== root) {
|
|
134
|
+
const sectionsPath = path.join(
|
|
135
|
+
currentDir,
|
|
136
|
+
'.mdcontext',
|
|
137
|
+
'indexes',
|
|
138
|
+
'sections.json',
|
|
139
|
+
)
|
|
140
|
+
try {
|
|
141
|
+
await fsPromises.access(sectionsPath)
|
|
142
|
+
return currentDir // Found an index
|
|
143
|
+
} catch {
|
|
144
|
+
// No index here, try parent
|
|
145
|
+
const parent = path.dirname(currentDir)
|
|
146
|
+
if (parent === currentDir) break // Reached root
|
|
147
|
+
currentDir = parent
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Also check root
|
|
152
|
+
const rootSectionsPath = path.join(
|
|
153
|
+
root,
|
|
154
|
+
'.mdcontext',
|
|
155
|
+
'indexes',
|
|
156
|
+
'sections.json',
|
|
157
|
+
)
|
|
158
|
+
try {
|
|
159
|
+
await fsPromises.access(rootSectionsPath)
|
|
160
|
+
return root
|
|
161
|
+
} catch {
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
71
166
|
/**
|
|
72
167
|
* Get index information for display
|
|
73
168
|
*/
|
|
@@ -77,11 +172,14 @@ export interface IndexInfo {
|
|
|
77
172
|
sectionCount?: number | undefined
|
|
78
173
|
embeddingsExist: boolean
|
|
79
174
|
vectorCount?: number | undefined
|
|
175
|
+
/** The actual directory where the index was found (may differ from requested dir) */
|
|
176
|
+
indexRoot?: string | undefined
|
|
80
177
|
}
|
|
81
178
|
|
|
82
179
|
export const getIndexInfo = async (dir: string): Promise<IndexInfo> => {
|
|
83
|
-
|
|
84
|
-
|
|
180
|
+
// First try the specified directory
|
|
181
|
+
let indexRoot = dir
|
|
182
|
+
let sectionsPath = path.join(dir, '.mdcontext', 'indexes', 'sections.json')
|
|
85
183
|
|
|
86
184
|
let exists = false
|
|
87
185
|
let lastUpdated: string | undefined
|
|
@@ -89,7 +187,7 @@ export const getIndexInfo = async (dir: string): Promise<IndexInfo> => {
|
|
|
89
187
|
let embeddingsExist = false
|
|
90
188
|
let vectorCount: number | undefined
|
|
91
189
|
|
|
92
|
-
// Check sections index
|
|
190
|
+
// Check sections index in specified directory
|
|
93
191
|
try {
|
|
94
192
|
const stat = await fsPromises.stat(sectionsPath)
|
|
95
193
|
exists = true
|
|
@@ -99,18 +197,52 @@ export const getIndexInfo = async (dir: string): Promise<IndexInfo> => {
|
|
|
99
197
|
const sections = JSON.parse(content)
|
|
100
198
|
sectionCount = Object.keys(sections.sections || {}).length
|
|
101
199
|
} catch {
|
|
102
|
-
// Index doesn't exist
|
|
200
|
+
// Index doesn't exist in specified directory, try to find in parent directories
|
|
201
|
+
const foundRoot = await findIndexRoot(dir)
|
|
202
|
+
if (foundRoot) {
|
|
203
|
+
indexRoot = foundRoot
|
|
204
|
+
sectionsPath = path.join(
|
|
205
|
+
foundRoot,
|
|
206
|
+
'.mdcontext',
|
|
207
|
+
'indexes',
|
|
208
|
+
'sections.json',
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const stat = await fsPromises.stat(sectionsPath)
|
|
213
|
+
exists = true
|
|
214
|
+
lastUpdated = stat.mtime.toISOString()
|
|
215
|
+
|
|
216
|
+
const content = await fsPromises.readFile(sectionsPath, 'utf-8')
|
|
217
|
+
const sections = JSON.parse(content)
|
|
218
|
+
sectionCount = Object.keys(sections.sections || {}).length
|
|
219
|
+
} catch {
|
|
220
|
+
// Still failed
|
|
221
|
+
}
|
|
222
|
+
}
|
|
103
223
|
}
|
|
104
224
|
|
|
105
|
-
// Check
|
|
225
|
+
// Check namespaced embeddings
|
|
106
226
|
try {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
227
|
+
const namespaces = await Effect.runPromise(
|
|
228
|
+
listNamespaces(indexRoot).pipe(Effect.catchAll(() => Effect.succeed([]))),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if (namespaces.length > 0) {
|
|
232
|
+
embeddingsExist = true
|
|
233
|
+
// Find active namespace or use first one
|
|
234
|
+
const activeNs = namespaces.find((ns) => ns.isActive) ?? namespaces[0]
|
|
235
|
+
if (activeNs) {
|
|
236
|
+
vectorCount = activeNs.vectorCount
|
|
237
|
+
// Use namespace's updatedAt if more recent
|
|
238
|
+
if (activeNs.updatedAt) {
|
|
239
|
+
const nsDate = new Date(activeNs.updatedAt)
|
|
240
|
+
const currentDate = lastUpdated ? new Date(lastUpdated) : new Date(0)
|
|
241
|
+
if (nsDate > currentDate) {
|
|
242
|
+
lastUpdated = activeNs.updatedAt
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
114
246
|
}
|
|
115
247
|
} catch {
|
|
116
248
|
// Embeddings don't exist
|
|
@@ -122,5 +254,6 @@ export const getIndexInfo = async (dir: string): Promise<IndexInfo> => {
|
|
|
122
254
|
sectionCount,
|
|
123
255
|
embeddingsExist,
|
|
124
256
|
vectorCount,
|
|
257
|
+
indexRoot: exists && indexRoot !== dir ? indexRoot : undefined,
|
|
125
258
|
}
|
|
126
259
|
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based ConfigProvider Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for loading configuration from files and creating ConfigProviders.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs'
|
|
8
|
+
import * as os from 'node:os'
|
|
9
|
+
import * as path from 'node:path'
|
|
10
|
+
import { Effect } from 'effect'
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
12
|
+
import {
|
|
13
|
+
CONFIG_FILE_NAMES,
|
|
14
|
+
createFileConfigProvider,
|
|
15
|
+
findConfigFile,
|
|
16
|
+
loadConfigFile,
|
|
17
|
+
loadConfigFromPath,
|
|
18
|
+
loadFileConfigProvider,
|
|
19
|
+
} from './file-provider.js'
|
|
20
|
+
import { MdContextConfig } from './schema.js'
|
|
21
|
+
|
|
22
|
+
describe('File-based ConfigProvider', () => {
|
|
23
|
+
let tempDir: string
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('CONFIG_FILE_NAMES', () => {
|
|
34
|
+
it('should have the expected file names in order of precedence', () => {
|
|
35
|
+
expect(CONFIG_FILE_NAMES).toEqual([
|
|
36
|
+
'mdcontext.config.ts',
|
|
37
|
+
'mdcontext.config.js',
|
|
38
|
+
'mdcontext.config.mjs',
|
|
39
|
+
'mdcontext.config.json',
|
|
40
|
+
'.mdcontextrc',
|
|
41
|
+
'.mdcontextrc.json',
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('findConfigFile', () => {
|
|
47
|
+
it('should return null when no config file exists', () => {
|
|
48
|
+
const result = findConfigFile(tempDir)
|
|
49
|
+
expect(result).toBeNull()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should find mdcontext.config.json', () => {
|
|
53
|
+
const configPath = path.join(tempDir, 'mdcontext.config.json')
|
|
54
|
+
fs.writeFileSync(configPath, '{}')
|
|
55
|
+
|
|
56
|
+
const result = findConfigFile(tempDir)
|
|
57
|
+
expect(result).not.toBeNull()
|
|
58
|
+
expect(result?.path).toBe(configPath)
|
|
59
|
+
expect(result?.format).toBe('json')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should find .mdcontextrc', () => {
|
|
63
|
+
const configPath = path.join(tempDir, '.mdcontextrc')
|
|
64
|
+
fs.writeFileSync(configPath, '{}')
|
|
65
|
+
|
|
66
|
+
const result = findConfigFile(tempDir)
|
|
67
|
+
expect(result).not.toBeNull()
|
|
68
|
+
expect(result?.path).toBe(configPath)
|
|
69
|
+
expect(result?.format).toBe('json')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should find config in parent directory', () => {
|
|
73
|
+
const subDir = path.join(tempDir, 'subdir')
|
|
74
|
+
fs.mkdirSync(subDir)
|
|
75
|
+
const configPath = path.join(tempDir, 'mdcontext.config.json')
|
|
76
|
+
fs.writeFileSync(configPath, '{}')
|
|
77
|
+
|
|
78
|
+
const result = findConfigFile(subDir)
|
|
79
|
+
expect(result).not.toBeNull()
|
|
80
|
+
expect(result?.path).toBe(configPath)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should prefer higher precedence files', () => {
|
|
84
|
+
// Create both .ts and .json files
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
path.join(tempDir, 'mdcontext.config.ts'),
|
|
87
|
+
'export default {}',
|
|
88
|
+
)
|
|
89
|
+
fs.writeFileSync(path.join(tempDir, 'mdcontext.config.json'), '{}')
|
|
90
|
+
|
|
91
|
+
const result = findConfigFile(tempDir)
|
|
92
|
+
expect(result).not.toBeNull()
|
|
93
|
+
expect(result?.path).toBe(path.join(tempDir, 'mdcontext.config.ts'))
|
|
94
|
+
expect(result?.format).toBe('ts')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should identify .js format correctly', () => {
|
|
98
|
+
const configPath = path.join(tempDir, 'mdcontext.config.js')
|
|
99
|
+
fs.writeFileSync(configPath, 'module.exports = {}')
|
|
100
|
+
|
|
101
|
+
const result = findConfigFile(tempDir)
|
|
102
|
+
expect(result?.format).toBe('js')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should identify .mjs format correctly', () => {
|
|
106
|
+
const configPath = path.join(tempDir, 'mdcontext.config.mjs')
|
|
107
|
+
fs.writeFileSync(configPath, 'export default {}')
|
|
108
|
+
|
|
109
|
+
const result = findConfigFile(tempDir)
|
|
110
|
+
expect(result?.format).toBe('js')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('loadConfigFile', () => {
|
|
115
|
+
it('should return found: false when no config exists', async () => {
|
|
116
|
+
const result = await Effect.runPromise(loadConfigFile(tempDir))
|
|
117
|
+
expect(result.found).toBe(false)
|
|
118
|
+
if (!result.found) {
|
|
119
|
+
expect(result.searched.length).toBeGreaterThan(0)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should load JSON config file', async () => {
|
|
124
|
+
const config = {
|
|
125
|
+
index: { maxDepth: 5 },
|
|
126
|
+
output: { verbose: true },
|
|
127
|
+
}
|
|
128
|
+
fs.writeFileSync(
|
|
129
|
+
path.join(tempDir, 'mdcontext.config.json'),
|
|
130
|
+
JSON.stringify(config),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const result = await Effect.runPromise(loadConfigFile(tempDir))
|
|
134
|
+
expect(result.found).toBe(true)
|
|
135
|
+
if (result.found) {
|
|
136
|
+
expect(result.config).toEqual(config)
|
|
137
|
+
expect(result.path).toContain('mdcontext.config.json')
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should load .mdcontextrc file', async () => {
|
|
142
|
+
const config = { search: { defaultLimit: 20 } }
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
path.join(tempDir, '.mdcontextrc'),
|
|
145
|
+
JSON.stringify(config),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const result = await Effect.runPromise(loadConfigFile(tempDir))
|
|
149
|
+
expect(result.found).toBe(true)
|
|
150
|
+
if (result.found) {
|
|
151
|
+
expect(result.config).toEqual(config)
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should fail with ConfigError for invalid JSON', async () => {
|
|
156
|
+
fs.writeFileSync(
|
|
157
|
+
path.join(tempDir, 'mdcontext.config.json'),
|
|
158
|
+
'not valid json',
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const result = await Effect.runPromiseExit(loadConfigFile(tempDir))
|
|
162
|
+
expect(result._tag).toBe('Failure')
|
|
163
|
+
if (result._tag === 'Failure') {
|
|
164
|
+
const error = result.cause
|
|
165
|
+
expect(String(error)).toContain('ConfigError')
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('loadConfigFromPath', () => {
|
|
171
|
+
it('should load config from explicit path', async () => {
|
|
172
|
+
const configPath = path.join(tempDir, 'custom.json')
|
|
173
|
+
const config = { index: { maxDepth: 15 } }
|
|
174
|
+
fs.writeFileSync(configPath, JSON.stringify(config))
|
|
175
|
+
|
|
176
|
+
const result = await Effect.runPromise(loadConfigFromPath(configPath))
|
|
177
|
+
expect(result).toEqual(config)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should fail with ConfigError when file does not exist', async () => {
|
|
181
|
+
const configPath = path.join(tempDir, 'nonexistent.json')
|
|
182
|
+
|
|
183
|
+
const result = await Effect.runPromiseExit(loadConfigFromPath(configPath))
|
|
184
|
+
expect(result._tag).toBe('Failure')
|
|
185
|
+
if (result._tag === 'Failure') {
|
|
186
|
+
const error = String(result.cause)
|
|
187
|
+
expect(error).toContain('ConfigError')
|
|
188
|
+
expect(error).toContain('not found')
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should fail with ConfigError for invalid JSON', async () => {
|
|
193
|
+
const configPath = path.join(tempDir, 'invalid.json')
|
|
194
|
+
fs.writeFileSync(configPath, 'invalid json content')
|
|
195
|
+
|
|
196
|
+
const result = await Effect.runPromiseExit(loadConfigFromPath(configPath))
|
|
197
|
+
expect(result._tag).toBe('Failure')
|
|
198
|
+
if (result._tag === 'Failure') {
|
|
199
|
+
expect(String(result.cause)).toContain('ConfigError')
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('createFileConfigProvider', () => {
|
|
205
|
+
it('should create a ConfigProvider from partial config', async () => {
|
|
206
|
+
const config = {
|
|
207
|
+
index: { maxDepth: 25 },
|
|
208
|
+
search: { minSimilarity: 0.8 },
|
|
209
|
+
}
|
|
210
|
+
const provider = createFileConfigProvider(config)
|
|
211
|
+
|
|
212
|
+
const program = Effect.gen(function* () {
|
|
213
|
+
return yield* MdContextConfig
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const result = await Effect.runPromise(
|
|
217
|
+
Effect.withConfigProvider(program, provider),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
expect(result.index.maxDepth).toBe(25)
|
|
221
|
+
expect(result.search.minSimilarity).toBe(0.8)
|
|
222
|
+
// Defaults should be used for unspecified values
|
|
223
|
+
expect(result.output.format).toBe('text')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should work with nested config structure', async () => {
|
|
227
|
+
const config = {
|
|
228
|
+
embeddings: {
|
|
229
|
+
model: 'text-embedding-3-large',
|
|
230
|
+
batchSize: 50,
|
|
231
|
+
},
|
|
232
|
+
paths: {
|
|
233
|
+
cacheDir: '/custom/cache',
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
const provider = createFileConfigProvider(config)
|
|
237
|
+
|
|
238
|
+
const program = Effect.gen(function* () {
|
|
239
|
+
return yield* MdContextConfig
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const result = await Effect.runPromise(
|
|
243
|
+
Effect.withConfigProvider(program, provider),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
expect(result.embeddings.model).toBe('text-embedding-3-large')
|
|
247
|
+
expect(result.embeddings.batchSize).toBe(50)
|
|
248
|
+
expect(result.paths.cacheDir).toBe('/custom/cache')
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('loadFileConfigProvider', () => {
|
|
253
|
+
it('should return empty provider when no config file exists', async () => {
|
|
254
|
+
const provider = await Effect.runPromise(loadFileConfigProvider(tempDir))
|
|
255
|
+
|
|
256
|
+
// Provider should exist but provide no overrides
|
|
257
|
+
const program = Effect.gen(function* () {
|
|
258
|
+
return yield* MdContextConfig
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const result = await Effect.runPromise(
|
|
262
|
+
Effect.withConfigProvider(program, provider),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
// All defaults should be used
|
|
266
|
+
expect(result.index.maxDepth).toBe(10)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('should load config and create provider in one step', async () => {
|
|
270
|
+
const config = {
|
|
271
|
+
index: { maxDepth: 30 },
|
|
272
|
+
output: { debug: true },
|
|
273
|
+
}
|
|
274
|
+
fs.writeFileSync(
|
|
275
|
+
path.join(tempDir, 'mdcontext.config.json'),
|
|
276
|
+
JSON.stringify(config),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
const provider = await Effect.runPromise(loadFileConfigProvider(tempDir))
|
|
280
|
+
|
|
281
|
+
const program = Effect.gen(function* () {
|
|
282
|
+
return yield* MdContextConfig
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const result = await Effect.runPromise(
|
|
286
|
+
Effect.withConfigProvider(program, provider),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
expect(result.index.maxDepth).toBe(30)
|
|
290
|
+
expect(result.output.debug).toBe(true)
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe('JavaScript/TypeScript config loading', () => {
|
|
295
|
+
it('should load .mjs config with default export', async () => {
|
|
296
|
+
const configPath = path.join(tempDir, 'mdcontext.config.mjs')
|
|
297
|
+
fs.writeFileSync(configPath, `export default { index: { maxDepth: 42 } }`)
|
|
298
|
+
|
|
299
|
+
const result = await Effect.runPromise(loadConfigFile(tempDir))
|
|
300
|
+
expect(result.found).toBe(true)
|
|
301
|
+
if (result.found) {
|
|
302
|
+
expect(result.config.index?.maxDepth).toBe(42)
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should load .mjs config with named export', async () => {
|
|
307
|
+
const configPath = path.join(tempDir, 'mdcontext.config.mjs')
|
|
308
|
+
fs.writeFileSync(
|
|
309
|
+
configPath,
|
|
310
|
+
`export const config = { search: { defaultLimit: 50 } }`,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const result = await Effect.runPromise(loadConfigFile(tempDir))
|
|
314
|
+
expect(result.found).toBe(true)
|
|
315
|
+
if (result.found) {
|
|
316
|
+
expect(result.config.search?.defaultLimit).toBe(50)
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
})
|