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.
Files changed (251) hide show
  1. package/.changeset/config.json +9 -9
  2. package/.claude/settings.local.json +25 -0
  3. package/.github/workflows/claude-code-review.yml +44 -0
  4. package/.github/workflows/claude.yml +85 -0
  5. package/CONTRIBUTING.md +186 -0
  6. package/NOTES/NOTES +44 -0
  7. package/README.md +206 -3
  8. package/biome.json +1 -1
  9. package/dist/chunk-23UPXDNL.js +3044 -0
  10. package/dist/chunk-2W7MO2DL.js +1366 -0
  11. package/dist/chunk-3NUAZGMA.js +1689 -0
  12. package/dist/chunk-7TOWB2XB.js +366 -0
  13. package/dist/chunk-7XOTOADQ.js +3065 -0
  14. package/dist/chunk-AH2PDM2K.js +3042 -0
  15. package/dist/chunk-BNXWSZ63.js +3742 -0
  16. package/dist/chunk-BTL5DJVU.js +3222 -0
  17. package/dist/chunk-HDHYG7E4.js +104 -0
  18. package/dist/chunk-HLR4KZBP.js +3234 -0
  19. package/dist/chunk-IP3FRFEB.js +1045 -0
  20. package/dist/chunk-KHU56VDO.js +3042 -0
  21. package/dist/chunk-KRYIFLQR.js +85 -89
  22. package/dist/chunk-LBSDNLEM.js +287 -0
  23. package/dist/chunk-MNTQ7HCP.js +2643 -0
  24. package/dist/chunk-MUJELQQ6.js +1387 -0
  25. package/dist/chunk-MXJGMSLV.js +2199 -0
  26. package/dist/chunk-N6QJGC3Z.js +2636 -0
  27. package/dist/chunk-OBELGBPM.js +1713 -0
  28. package/dist/chunk-OT7R5XTA.js +3192 -0
  29. package/dist/chunk-P7X4RA2T.js +106 -0
  30. package/dist/chunk-PIDUQNC2.js +3185 -0
  31. package/dist/chunk-POGCDIH4.js +3187 -0
  32. package/dist/chunk-PSIEOQGZ.js +3043 -0
  33. package/dist/chunk-PVRT3IHA.js +3238 -0
  34. package/dist/chunk-QNN4TT23.js +1430 -0
  35. package/dist/chunk-RE3R45RJ.js +3042 -0
  36. package/dist/chunk-S7E6TFX6.js +718 -657
  37. package/dist/chunk-SG6GLU4U.js +1378 -0
  38. package/dist/chunk-SJCDV2ST.js +274 -0
  39. package/dist/chunk-SYE5XLF3.js +104 -0
  40. package/dist/chunk-T5VLYBZD.js +103 -0
  41. package/dist/chunk-TOQB7VWU.js +3238 -0
  42. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  43. package/dist/chunk-VVTGZNBT.js +1533 -1423
  44. package/dist/chunk-W7Q4RFEV.js +104 -0
  45. package/dist/chunk-XTYYVRLO.js +3190 -0
  46. package/dist/chunk-Y6MDYVJD.js +3063 -0
  47. package/dist/cli/main.js +4072 -629
  48. package/dist/index.d.ts +420 -33
  49. package/dist/index.js +8 -15
  50. package/dist/mcp/server.js +103 -7
  51. package/dist/schema-BAWSG7KY.js +22 -0
  52. package/dist/schema-E3QUPL26.js +20 -0
  53. package/dist/schema-EHL7WUT6.js +20 -0
  54. package/docs/019-USAGE.md +44 -5
  55. package/docs/020-current-implementation.md +8 -8
  56. package/docs/021-DOGFOODING-FINDINGS.md +1 -1
  57. package/docs/CONFIG.md +1123 -0
  58. package/docs/ERRORS.md +383 -0
  59. package/docs/summarization.md +320 -0
  60. package/justfile +40 -0
  61. package/package.json +39 -33
  62. package/research/INDEX.md +315 -0
  63. package/research/code-review/README.md +90 -0
  64. package/research/code-review/cli-error-handling-review.md +979 -0
  65. package/research/code-review/code-review-validation-report.md +464 -0
  66. package/research/code-review/main-ts-review.md +1128 -0
  67. package/research/config-docs/SUMMARY.md +357 -0
  68. package/research/config-docs/TEST-RESULTS.md +776 -0
  69. package/research/config-docs/TODO.md +542 -0
  70. package/research/config-docs/analysis.md +744 -0
  71. package/research/config-docs/fix-validation.md +502 -0
  72. package/research/config-docs/help-audit.md +264 -0
  73. package/research/config-docs/help-system-analysis.md +890 -0
  74. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  75. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  76. package/research/issue-review.md +603 -0
  77. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  78. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  79. package/research/llm-summarization/anthropic-2026.md +367 -0
  80. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  81. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  82. package/research/llm-summarization/openai-2026.md +473 -0
  83. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  84. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  85. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  86. package/research/llm-summarization/prototype-results.md +56 -0
  87. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  88. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  89. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  90. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  91. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  92. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  93. package/research/mdcontext-pudding/02-search.md +970 -0
  94. package/research/mdcontext-pudding/03-context.md +779 -0
  95. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  96. package/research/mdcontext-pudding/04-tree.md +704 -0
  97. package/research/mdcontext-pudding/05-config.md +1038 -0
  98. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  99. package/research/mdcontext-pudding/06-links.md +679 -0
  100. package/research/mdcontext-pudding/07-stats.md +693 -0
  101. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  102. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  103. package/research/mdcontext-pudding/README.md +168 -0
  104. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  105. package/research/research-quality-review.md +834 -0
  106. package/research/semantic-search/embedding-text-analysis.md +156 -0
  107. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  108. package/research/semantic-search/query-processing-analysis.md +207 -0
  109. package/research/semantic-search/root-cause-and-solution.md +114 -0
  110. package/research/semantic-search/threshold-validation-report.md +69 -0
  111. package/research/semantic-search/vector-search-analysis.md +63 -0
  112. package/research/test-path-issues.md +276 -0
  113. package/review/ALP-76/1-error-type-design.md +962 -0
  114. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  115. package/review/ALP-76/3-error-presentation.md +624 -0
  116. package/review/ALP-76/4-test-coverage.md +625 -0
  117. package/review/ALP-76/5-migration-completeness.md +440 -0
  118. package/review/ALP-76/6-effect-best-practices.md +755 -0
  119. package/scripts/apply-branch-protection.sh +47 -0
  120. package/scripts/branch-protection-templates.json +79 -0
  121. package/scripts/prototype-summarization.ts +346 -0
  122. package/scripts/rebuild-hnswlib.js +32 -37
  123. package/scripts/setup-branch-protection.sh +64 -0
  124. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  125. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  126. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  127. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  128. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  129. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  130. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  131. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  132. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  133. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  134. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  135. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  136. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  137. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  138. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  139. package/src/cli/argv-preprocessor.test.ts +2 -2
  140. package/src/cli/cli.test.ts +230 -33
  141. package/src/cli/commands/config-cmd.ts +642 -0
  142. package/src/cli/commands/context.ts +97 -9
  143. package/src/cli/commands/duplicates.ts +122 -0
  144. package/src/cli/commands/embeddings.ts +529 -0
  145. package/src/cli/commands/index-cmd.ts +210 -30
  146. package/src/cli/commands/index.ts +3 -0
  147. package/src/cli/commands/search.ts +894 -64
  148. package/src/cli/commands/stats.ts +3 -0
  149. package/src/cli/commands/tree.ts +26 -5
  150. package/src/cli/config-layer.ts +176 -0
  151. package/src/cli/error-handler.test.ts +235 -0
  152. package/src/cli/error-handler.ts +655 -0
  153. package/src/cli/flag-schemas.ts +66 -0
  154. package/src/cli/help.ts +209 -7
  155. package/src/cli/main.ts +348 -58
  156. package/src/cli/options.ts +10 -0
  157. package/src/cli/shared-error-handling.ts +199 -0
  158. package/src/cli/utils.ts +150 -17
  159. package/src/config/file-provider.test.ts +320 -0
  160. package/src/config/file-provider.ts +273 -0
  161. package/src/config/index.ts +72 -0
  162. package/src/config/integration.test.ts +667 -0
  163. package/src/config/precedence.test.ts +277 -0
  164. package/src/config/precedence.ts +451 -0
  165. package/src/config/schema.test.ts +414 -0
  166. package/src/config/schema.ts +603 -0
  167. package/src/config/service.test.ts +320 -0
  168. package/src/config/service.ts +243 -0
  169. package/src/config/testing.test.ts +264 -0
  170. package/src/config/testing.ts +110 -0
  171. package/src/core/types.ts +6 -33
  172. package/src/duplicates/detector.test.ts +183 -0
  173. package/src/duplicates/detector.ts +414 -0
  174. package/src/duplicates/index.ts +18 -0
  175. package/src/embeddings/embedding-namespace.test.ts +300 -0
  176. package/src/embeddings/embedding-namespace.ts +947 -0
  177. package/src/embeddings/heading-boost.test.ts +222 -0
  178. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  179. package/src/embeddings/hyde.test.ts +272 -0
  180. package/src/embeddings/hyde.ts +264 -0
  181. package/src/embeddings/index.ts +2 -0
  182. package/src/embeddings/openai-provider.ts +332 -83
  183. package/src/embeddings/pricing.json +22 -0
  184. package/src/embeddings/provider-constants.ts +204 -0
  185. package/src/embeddings/provider-errors.test.ts +967 -0
  186. package/src/embeddings/provider-errors.ts +565 -0
  187. package/src/embeddings/provider-factory.test.ts +240 -0
  188. package/src/embeddings/provider-factory.ts +225 -0
  189. package/src/embeddings/provider-integration.test.ts +788 -0
  190. package/src/embeddings/query-preprocessing.test.ts +187 -0
  191. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  192. package/src/embeddings/semantic-search.ts +780 -93
  193. package/src/embeddings/types.ts +293 -16
  194. package/src/embeddings/vector-store.ts +486 -77
  195. package/src/embeddings/voyage-provider.ts +313 -0
  196. package/src/errors/errors.test.ts +845 -0
  197. package/src/errors/index.ts +533 -0
  198. package/src/index/ignore-patterns.test.ts +354 -0
  199. package/src/index/ignore-patterns.ts +305 -0
  200. package/src/index/indexer.ts +286 -48
  201. package/src/index/storage.ts +94 -30
  202. package/src/index/types.ts +40 -2
  203. package/src/index/watcher.ts +67 -9
  204. package/src/index.ts +22 -0
  205. package/src/integration/search-keyword.test.ts +678 -0
  206. package/src/mcp/server.ts +135 -6
  207. package/src/parser/parser.ts +18 -19
  208. package/src/parser/section-filter.test.ts +277 -0
  209. package/src/parser/section-filter.ts +125 -3
  210. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  211. package/src/search/bm25-store.ts +366 -0
  212. package/src/search/cross-encoder.test.ts +253 -0
  213. package/src/search/cross-encoder.ts +406 -0
  214. package/src/search/fuzzy-search.test.ts +419 -0
  215. package/src/search/fuzzy-search.ts +273 -0
  216. package/src/search/hybrid-search.ts +448 -0
  217. package/src/search/path-matcher.test.ts +276 -0
  218. package/src/search/path-matcher.ts +33 -0
  219. package/src/search/searcher.test.ts +99 -1
  220. package/src/search/searcher.ts +189 -67
  221. package/src/search/wink-bm25.d.ts +30 -0
  222. package/src/summarization/cli-providers/claude.ts +202 -0
  223. package/src/summarization/cli-providers/detection.test.ts +273 -0
  224. package/src/summarization/cli-providers/detection.ts +118 -0
  225. package/src/summarization/cli-providers/index.ts +8 -0
  226. package/src/summarization/cost.test.ts +139 -0
  227. package/src/summarization/cost.ts +102 -0
  228. package/src/summarization/error-handler.test.ts +127 -0
  229. package/src/summarization/error-handler.ts +111 -0
  230. package/src/summarization/index.ts +102 -0
  231. package/src/summarization/pipeline.test.ts +498 -0
  232. package/src/summarization/pipeline.ts +231 -0
  233. package/src/summarization/prompts.test.ts +269 -0
  234. package/src/summarization/prompts.ts +133 -0
  235. package/src/summarization/provider-factory.test.ts +396 -0
  236. package/src/summarization/provider-factory.ts +178 -0
  237. package/src/summarization/types.ts +184 -0
  238. package/src/summarize/summarizer.ts +104 -35
  239. package/src/types/huggingface-transformers.d.ts +66 -0
  240. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  241. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  242. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  243. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
  244. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
  245. package/tests/integration/embed-index.test.ts +712 -0
  246. package/tests/integration/search-context.test.ts +469 -0
  247. package/tests/integration/search-semantic.test.ts +522 -0
  248. package/vitest.config.ts +1 -6
  249. package/AGENTS.md +0 -46
  250. package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
  251. 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 fsPromises.access(vectorsPath)
65
- return true
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
- const sectionsPath = path.join(dir, '.mdcontext', 'indexes', 'sections.json')
84
- const vectorsMetaPath = path.join(dir, '.mdcontext', 'vectors.meta.json')
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 vectors metadata
225
+ // Check namespaced embeddings
106
226
  try {
107
- const content = await fsPromises.readFile(vectorsMetaPath, 'utf-8')
108
- const meta = JSON.parse(content)
109
- embeddingsExist = true
110
- vectorCount = Object.keys(meta.entries || {}).length
111
- // Use vector meta updatedAt if available
112
- if (meta.updatedAt) {
113
- lastUpdated = meta.updatedAt
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
+ })