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
@@ -0,0 +1,642 @@
1
+ /**
2
+ * CONFIG Command
3
+ *
4
+ * Configuration management commands: init, check, etc.
5
+ */
6
+
7
+ import * as fs from 'node:fs'
8
+ import * as path from 'node:path'
9
+ import { Command, Options } from '@effect/cli'
10
+ import { Console, Effect, Option } from 'effect'
11
+ import {
12
+ CONFIG_FILE_NAMES,
13
+ defaultConfig,
14
+ findConfigFile,
15
+ loadConfigFile,
16
+ readEnvConfig,
17
+ } from '../../config/index.js'
18
+ import type { PartialMdContextConfig } from '../../config/service.js'
19
+ import { jsonOption, prettyOption } from '../options.js'
20
+ import { formatJson } from '../utils.js'
21
+
22
+ /**
23
+ * Generate the default config file content.
24
+ * Uses JavaScript with JSDoc types for type safety without requiring TypeScript loader.
25
+ */
26
+ const generateConfigContent = (format: 'js' | 'json'): string => {
27
+ if (format === 'json') {
28
+ return `{
29
+ "$schema": "https://mdcontext.dev/schema.json",
30
+ "index": {
31
+ "maxDepth": 10,
32
+ "excludePatterns": ["node_modules", ".git", "dist", "build"],
33
+ "fileExtensions": [".md", ".mdx"],
34
+ "followSymlinks": false,
35
+ "indexDir": ".mdcontext"
36
+ },
37
+ "search": {
38
+ "defaultLimit": 10,
39
+ "maxLimit": 100,
40
+ "minSimilarity": 0.35,
41
+ "includeSnippets": true,
42
+ "snippetLength": 200,
43
+ "autoIndexThreshold": 10
44
+ },
45
+ "embeddings": {
46
+ "provider": "openai",
47
+ "model": "text-embedding-3-small",
48
+ "dimensions": 512,
49
+ "batchSize": 100,
50
+ "maxRetries": 3,
51
+ "retryDelayMs": 1000,
52
+ "timeoutMs": 30000
53
+ },
54
+ "summarization": {
55
+ "briefTokenBudget": 100,
56
+ "summaryTokenBudget": 500,
57
+ "compressionRatio": 0.3,
58
+ "minSectionTokens": 20,
59
+ "maxTopics": 10,
60
+ "minPartialBudget": 50
61
+ },
62
+ "output": {
63
+ "format": "text",
64
+ "color": true,
65
+ "prettyJson": true,
66
+ "verbose": false,
67
+ "debug": false
68
+ }
69
+ }
70
+ `
71
+ }
72
+
73
+ // JavaScript format with JSDoc type annotation for type safety
74
+ return `/**
75
+ * mdcontext Configuration
76
+ *
77
+ * This file configures mdcontext behavior for this project.
78
+ * See https://mdcontext.dev/config for full documentation.
79
+ *
80
+ * @type {import('mdcontext').PartialMdContextConfig}
81
+ */
82
+ export default {
83
+ // Index settings - control how markdown files are discovered and parsed
84
+ index: {
85
+ // Maximum directory depth to traverse (default: 10)
86
+ maxDepth: 10,
87
+
88
+ // Patterns to exclude from indexing (default: common build/dep dirs)
89
+ excludePatterns: ['node_modules', '.git', 'dist', 'build'],
90
+
91
+ // File extensions to index (default: markdown files)
92
+ fileExtensions: ['.md', '.mdx'],
93
+
94
+ // Whether to follow symbolic links (default: false)
95
+ followSymlinks: false,
96
+
97
+ // Directory for index storage (default: '.mdcontext')
98
+ indexDir: '.mdcontext',
99
+ },
100
+
101
+ // Search settings - configure search behavior and defaults
102
+ search: {
103
+ // Default number of results to return (default: 10)
104
+ defaultLimit: 10,
105
+
106
+ // Maximum results allowed (default: 100)
107
+ maxLimit: 100,
108
+
109
+ // Minimum similarity score for semantic search (default: 0.35)
110
+ minSimilarity: 0.35,
111
+
112
+ // Include content snippets in results (default: true)
113
+ includeSnippets: true,
114
+
115
+ // Maximum snippet length in characters (default: 200)
116
+ snippetLength: 200,
117
+
118
+ // Auto-create semantic index if under this many seconds (default: 10)
119
+ autoIndexThreshold: 10,
120
+ },
121
+
122
+ // Embeddings settings - configure semantic search
123
+ embeddings: {
124
+ // Embedding provider: 'openai' (default), 'ollama', 'lm-studio', 'openrouter', or 'voyage'
125
+ provider: 'openai',
126
+
127
+ // Embedding model (varies by provider - default for OpenAI: 'text-embedding-3-small')
128
+ // Ollama: 'nomic-embed-text', LM Studio: depends on loaded model
129
+ model: 'text-embedding-3-small',
130
+
131
+ // Vector dimensions (lower = faster, higher = more accurate) (default: 512)
132
+ dimensions: 512,
133
+
134
+ // Batch size for API calls (default: 100)
135
+ batchSize: 100,
136
+
137
+ // Max retries for failed API calls (default: 3)
138
+ maxRetries: 3,
139
+
140
+ // Delay between retries in ms (default: 1000)
141
+ retryDelayMs: 1000,
142
+
143
+ // Request timeout in ms (default: 30000)
144
+ timeoutMs: 30000,
145
+
146
+ // API key - set via provider-specific environment variable:
147
+ // - OpenAI: OPENAI_API_KEY
148
+ // - OpenRouter: OPENROUTER_API_KEY
149
+ // - Ollama/LM Studio: No API key needed (local providers)
150
+ // apiKey: process.env.OPENAI_API_KEY,
151
+ },
152
+
153
+ // Summarization settings - configure context assembly
154
+ summarization: {
155
+ // Token budget for 'brief' compression level (default: 100)
156
+ briefTokenBudget: 100,
157
+
158
+ // Token budget for 'summary' compression level (default: 500)
159
+ summaryTokenBudget: 500,
160
+
161
+ // Target compression ratio for summaries (default: 0.3)
162
+ compressionRatio: 0.3,
163
+
164
+ // Minimum tokens for any section summary (default: 20)
165
+ minSectionTokens: 20,
166
+
167
+ // Maximum topics to extract from a document (default: 10)
168
+ maxTopics: 10,
169
+
170
+ // Minimum remaining budget for partial content (default: 50)
171
+ minPartialBudget: 50,
172
+ },
173
+
174
+ // Output settings - configure CLI output formatting
175
+ output: {
176
+ // Default output format: 'text' or 'json' (default: 'text')
177
+ format: 'text',
178
+
179
+ // Use colors in terminal output (default: true)
180
+ color: true,
181
+
182
+ // Pretty-print JSON output (default: true)
183
+ prettyJson: true,
184
+
185
+ // Show verbose output (default: false)
186
+ verbose: false,
187
+
188
+ // Show debug information (default: false)
189
+ debug: false,
190
+ },
191
+ }
192
+ `
193
+ }
194
+
195
+ /**
196
+ * Config init subcommand - creates a starter config file.
197
+ */
198
+ const initCommand = Command.make(
199
+ 'init',
200
+ {
201
+ format: Options.choice('format', ['js', 'json']).pipe(
202
+ Options.withAlias('f'),
203
+ Options.withDescription(
204
+ 'Config file format (js recommended for type safety)',
205
+ ),
206
+ Options.withDefault('js' as const),
207
+ ),
208
+ force: Options.boolean('force').pipe(
209
+ Options.withDescription('Overwrite existing config file'),
210
+ Options.withDefault(false),
211
+ ),
212
+ json: jsonOption,
213
+ pretty: prettyOption,
214
+ },
215
+ ({ format, force, json, pretty }) =>
216
+ Effect.gen(function* () {
217
+ const cwd = process.cwd()
218
+
219
+ // Check if a config file already exists
220
+ const existingConfig = findConfigFile(cwd)
221
+
222
+ if (existingConfig && !force) {
223
+ if (json) {
224
+ yield* Console.log(
225
+ formatJson(
226
+ {
227
+ error: 'Config file already exists',
228
+ path: existingConfig.path,
229
+ hint: 'Use --force to overwrite',
230
+ },
231
+ pretty,
232
+ ),
233
+ )
234
+ } else {
235
+ yield* Console.error(
236
+ `Config file already exists: ${existingConfig.path}`,
237
+ )
238
+ yield* Console.error('')
239
+ yield* Console.error('Use --force to overwrite.')
240
+ }
241
+ return
242
+ }
243
+
244
+ // Determine filename based on format
245
+ const filename =
246
+ format === 'json' ? 'mdcontext.config.json' : 'mdcontext.config.js'
247
+ const filepath = path.join(cwd, filename)
248
+
249
+ // Generate content
250
+ const content = generateConfigContent(format)
251
+
252
+ // Write the file
253
+ yield* Effect.try({
254
+ try: () => fs.writeFileSync(filepath, content, 'utf-8'),
255
+ catch: (e) => new Error(`Failed to write config file: ${e}`),
256
+ })
257
+
258
+ if (json) {
259
+ yield* Console.log(
260
+ formatJson(
261
+ {
262
+ created: filepath,
263
+ format,
264
+ },
265
+ pretty,
266
+ ),
267
+ )
268
+ } else {
269
+ yield* Console.log(`Created ${filename}`)
270
+ yield* Console.log('')
271
+ if (format === 'js') {
272
+ yield* Console.log('The config file includes:')
273
+ yield* Console.log(
274
+ ' - JSDoc type annotations for IDE autocompletion',
275
+ )
276
+ yield* Console.log(' - Documented default values')
277
+ yield* Console.log(
278
+ ' - All available options including summarization',
279
+ )
280
+ yield* Console.log('')
281
+ yield* Console.log(
282
+ 'Edit the file to customize mdcontext for your project.',
283
+ )
284
+ } else {
285
+ yield* Console.log(
286
+ 'Edit the file to customize mdcontext for your project.',
287
+ )
288
+ }
289
+ }
290
+ }),
291
+ ).pipe(Command.withDescription('Create a starter config file'))
292
+
293
+ /**
294
+ * Config show subcommand - displays current config.
295
+ */
296
+ const showCommand = Command.make(
297
+ 'show',
298
+ {
299
+ json: jsonOption,
300
+ pretty: prettyOption,
301
+ },
302
+ ({ json, pretty }) =>
303
+ Effect.gen(function* () {
304
+ const cwd = process.cwd()
305
+
306
+ // Find existing config file
307
+ const configPath = findConfigFile(cwd)
308
+
309
+ if (!configPath) {
310
+ if (json) {
311
+ yield* Console.log(
312
+ formatJson(
313
+ {
314
+ error: 'No config file found',
315
+ searchedIn: cwd,
316
+ searchedFor: CONFIG_FILE_NAMES,
317
+ },
318
+ pretty,
319
+ ),
320
+ )
321
+ } else {
322
+ yield* Console.log('No config file found.')
323
+ yield* Console.log('')
324
+ yield* Console.log('Searched for:')
325
+ for (const name of CONFIG_FILE_NAMES) {
326
+ yield* Console.log(` - ${name}`)
327
+ }
328
+ yield* Console.log('')
329
+ yield* Console.log("Run 'mdcontext config init' to create one.")
330
+ }
331
+ return
332
+ }
333
+
334
+ if (json) {
335
+ yield* Console.log(
336
+ formatJson(
337
+ {
338
+ configFile: configPath.path,
339
+ },
340
+ pretty,
341
+ ),
342
+ )
343
+ } else {
344
+ yield* Console.log(`Config file: ${configPath.path}`)
345
+ }
346
+ }),
347
+ ).pipe(Command.withDescription('Show config file location'))
348
+
349
+ // ============================================================================
350
+ // Config Check Types
351
+ // ============================================================================
352
+
353
+ type ConfigSource = 'default' | 'file' | 'env'
354
+
355
+ interface ConfigValueWithSource<T> {
356
+ value: T
357
+ source: ConfigSource
358
+ }
359
+
360
+ type ConfigSectionWithSources<T> = {
361
+ [K in keyof T]: ConfigValueWithSource<T[K]>
362
+ }
363
+
364
+ interface ConfigWithSources {
365
+ index: ConfigSectionWithSources<typeof defaultConfig.index>
366
+ search: ConfigSectionWithSources<typeof defaultConfig.search>
367
+ embeddings: ConfigSectionWithSources<typeof defaultConfig.embeddings>
368
+ summarization: ConfigSectionWithSources<typeof defaultConfig.summarization>
369
+ output: ConfigSectionWithSources<typeof defaultConfig.output>
370
+ paths: ConfigSectionWithSources<typeof defaultConfig.paths>
371
+ }
372
+
373
+ interface CheckResultJson {
374
+ valid: boolean
375
+ sourceFile: string | null
376
+ errors?: string[]
377
+ config: ConfigWithSources
378
+ }
379
+
380
+ // ============================================================================
381
+ // Config Check Helpers
382
+ // ============================================================================
383
+
384
+ /**
385
+ * Determine the source of a config value by checking env, file, and defaults.
386
+ */
387
+ const getValueSource = <T>(
388
+ key: string,
389
+ envConfig: Map<string, string>,
390
+ fileValue: T | undefined,
391
+ _defaultValue: T,
392
+ ): ConfigSource => {
393
+ if (envConfig.has(key)) {
394
+ return 'env'
395
+ }
396
+ if (fileValue !== undefined) {
397
+ return 'file'
398
+ }
399
+ return 'default'
400
+ }
401
+
402
+ /**
403
+ * Get effective value from the precedence chain.
404
+ */
405
+ const getEffectiveValue = <T>(
406
+ key: string,
407
+ envConfig: Map<string, string>,
408
+ fileValue: T | undefined,
409
+ defaultValue: T,
410
+ ): T => {
411
+ const envValue = envConfig.get(key)
412
+ if (envValue !== undefined) {
413
+ // Parse env value based on type of default
414
+ if (typeof defaultValue === 'boolean') {
415
+ return (envValue === 'true') as unknown as T
416
+ }
417
+ if (typeof defaultValue === 'number') {
418
+ return Number(envValue) as unknown as T
419
+ }
420
+ if (Array.isArray(defaultValue)) {
421
+ return envValue.split(',') as unknown as T
422
+ }
423
+ return envValue as unknown as T
424
+ }
425
+ if (fileValue !== undefined) {
426
+ return fileValue
427
+ }
428
+ return defaultValue
429
+ }
430
+
431
+ /**
432
+ * Build config section with source annotations.
433
+ */
434
+ const buildSectionWithSources = <T extends Record<string, unknown>>(
435
+ sectionName: string,
436
+ defaultSection: T,
437
+ fileSection: Partial<T> | undefined,
438
+ envConfig: Map<string, string>,
439
+ ): ConfigSectionWithSources<T> => {
440
+ const result: Record<string, ConfigValueWithSource<unknown>> = {}
441
+
442
+ for (const [key, defaultValue] of Object.entries(defaultSection)) {
443
+ const envKey = `${sectionName}.${key}`
444
+ const fileValue = fileSection?.[key as keyof T]
445
+
446
+ result[key] = {
447
+ value: getEffectiveValue(envKey, envConfig, fileValue, defaultValue),
448
+ source: getValueSource(envKey, envConfig, fileValue, defaultValue),
449
+ }
450
+ }
451
+
452
+ return result as ConfigSectionWithSources<T>
453
+ }
454
+
455
+ /**
456
+ * Format a value for text display.
457
+ */
458
+ const formatValue = (value: unknown): string => {
459
+ if (Option.isOption(value)) {
460
+ return Option.isSome(value) ? String(value.value) : '(not set)'
461
+ }
462
+ if (Array.isArray(value)) {
463
+ return JSON.stringify(value)
464
+ }
465
+ if (typeof value === 'string') {
466
+ return value
467
+ }
468
+ return String(value)
469
+ }
470
+
471
+ /**
472
+ * Format source annotation for text display.
473
+ */
474
+ const formatSourceAnnotation = (source: ConfigSource): string => {
475
+ switch (source) {
476
+ case 'file':
477
+ return '(from config file)'
478
+ case 'env':
479
+ return '(from environment)'
480
+ case 'default':
481
+ return '(default)'
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Convert config with sources to JSON format.
487
+ * Handles Option values by converting them to their underlying value or null.
488
+ */
489
+ const configToJsonFormat = (config: ConfigWithSources): ConfigWithSources => {
490
+ const convertSection = <
491
+ T extends Record<string, ConfigValueWithSource<unknown>>,
492
+ >(
493
+ section: T,
494
+ ): T => {
495
+ const result: Record<string, ConfigValueWithSource<unknown>> = {}
496
+ for (const [key, entry] of Object.entries(section)) {
497
+ let value = entry.value
498
+ if (Option.isOption(value)) {
499
+ value = Option.isSome(value) ? value.value : null
500
+ }
501
+ result[key] = { value, source: entry.source }
502
+ }
503
+ return result as T
504
+ }
505
+
506
+ return {
507
+ index: convertSection(config.index),
508
+ search: convertSection(config.search),
509
+ embeddings: convertSection(config.embeddings),
510
+ summarization: convertSection(config.summarization),
511
+ output: convertSection(config.output),
512
+ paths: convertSection(config.paths),
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Config check subcommand - validates config and shows effective values with sources.
518
+ */
519
+ const checkCommand = Command.make(
520
+ 'check',
521
+ {
522
+ json: jsonOption,
523
+ pretty: prettyOption,
524
+ },
525
+ ({ json, pretty }) =>
526
+ Effect.gen(function* () {
527
+ const cwd = process.cwd()
528
+ const errors: string[] = []
529
+
530
+ // Load config file if present
531
+ const configResult = yield* loadConfigFile(cwd).pipe(
532
+ Effect.catchTag('ConfigError', (e) => {
533
+ errors.push(e.message)
534
+ return Effect.succeed({ found: false, searched: [] } as const)
535
+ }),
536
+ )
537
+
538
+ const sourceFile = configResult.found ? configResult.path : null
539
+ const fileConfig: PartialMdContextConfig = configResult.found
540
+ ? configResult.config
541
+ : {}
542
+
543
+ // Read environment variables
544
+ const envConfig = readEnvConfig('MDCONTEXT')
545
+
546
+ // Build config with source annotations
547
+ const configWithSources: ConfigWithSources = {
548
+ index: buildSectionWithSources(
549
+ 'index',
550
+ defaultConfig.index,
551
+ fileConfig.index,
552
+ envConfig,
553
+ ),
554
+ search: buildSectionWithSources(
555
+ 'search',
556
+ defaultConfig.search,
557
+ fileConfig.search,
558
+ envConfig,
559
+ ),
560
+ embeddings: buildSectionWithSources(
561
+ 'embeddings',
562
+ defaultConfig.embeddings,
563
+ fileConfig.embeddings,
564
+ envConfig,
565
+ ),
566
+ summarization: buildSectionWithSources(
567
+ 'summarization',
568
+ defaultConfig.summarization,
569
+ fileConfig.summarization,
570
+ envConfig,
571
+ ),
572
+ output: buildSectionWithSources(
573
+ 'output',
574
+ defaultConfig.output,
575
+ fileConfig.output,
576
+ envConfig,
577
+ ),
578
+ paths: buildSectionWithSources(
579
+ 'paths',
580
+ defaultConfig.paths,
581
+ fileConfig.paths,
582
+ envConfig,
583
+ ),
584
+ }
585
+
586
+ const isValid = errors.length === 0
587
+
588
+ if (json) {
589
+ const result: CheckResultJson = {
590
+ valid: isValid,
591
+ sourceFile,
592
+ config: configToJsonFormat(configWithSources),
593
+ }
594
+ if (errors.length > 0) {
595
+ result.errors = errors
596
+ }
597
+ yield* Console.log(formatJson(result, pretty))
598
+ } else {
599
+ // Text format output
600
+ if (isValid) {
601
+ yield* Console.log('Configuration validated successfully!')
602
+ } else {
603
+ yield* Console.log('Configuration has errors:')
604
+ for (const error of errors) {
605
+ yield* Console.log(` - ${error}`)
606
+ }
607
+ }
608
+ yield* Console.log('')
609
+
610
+ if (sourceFile) {
611
+ yield* Console.log(`Source: ${sourceFile}`)
612
+ } else {
613
+ yield* Console.log('Source: No config file found (using defaults)')
614
+ }
615
+ yield* Console.log('')
616
+
617
+ yield* Console.log('Effective configuration:')
618
+
619
+ // Display each section
620
+ for (const [sectionName, section] of Object.entries(
621
+ configWithSources,
622
+ )) {
623
+ yield* Console.log(` ${sectionName}:`)
624
+ for (const [key, entry] of Object.entries(
625
+ section as Record<string, ConfigValueWithSource<unknown>>,
626
+ )) {
627
+ const valueStr = formatValue(entry.value)
628
+ const sourceStr = formatSourceAnnotation(entry.source)
629
+ yield* Console.log(` ${key}: ${valueStr} ${sourceStr}`)
630
+ }
631
+ }
632
+ }
633
+ }),
634
+ ).pipe(Command.withDescription('Validate and display effective configuration'))
635
+
636
+ /**
637
+ * Main config command with subcommands.
638
+ */
639
+ export const configCommand = Command.make('config').pipe(
640
+ Command.withDescription('Configuration management'),
641
+ Command.withSubcommands([initCommand, showCommand, checkCommand]),
642
+ )