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/main.ts CHANGED
@@ -17,20 +17,42 @@
17
17
  * mdcontext stats [path] Index statistics
18
18
  */
19
19
 
20
+ import * as fs from 'node:fs'
21
+ import { createRequire } from 'node:module'
22
+ import * as path from 'node:path'
23
+ import * as util from 'node:util'
24
+
25
+ // Read version from package.json using createRequire for ESM compatibility
26
+ const require = createRequire(import.meta.url)
27
+ const packageJson = require('../../package.json') as { version: string }
28
+ const CLI_VERSION: string = packageJson.version
29
+
20
30
  import { CliConfig, Command } from '@effect/cli'
21
31
  import { NodeContext, NodeRuntime } from '@effect/platform-node'
22
32
  import { Effect, Layer } from 'effect'
33
+ import { ConfigService, createConfigProviderSync } from '../config/index.js'
34
+ import { MdContextConfig } from '../config/schema.js'
35
+ import type { PartialMdContextConfig } from '../config/service.js'
23
36
  import { preprocessArgv } from './argv-preprocessor.js'
24
37
  import {
25
38
  backlinksCommand,
39
+ configCommand,
26
40
  contextCommand,
41
+ duplicatesCommand,
42
+ embeddingsCommand,
27
43
  indexCommand,
28
44
  linksCommand,
29
45
  searchCommand,
30
46
  statsCommand,
31
47
  treeCommand,
32
48
  } from './commands/index.js'
49
+ import { defaultCliConfigLayerSync } from './config-layer.js'
33
50
  import {
51
+ formatEffectCliError,
52
+ isEffectCliValidationError,
53
+ } from './error-handler.js'
54
+ import {
55
+ checkBareSubcommandHelp,
34
56
  checkSubcommandHelp,
35
57
  shouldShowMainHelp,
36
58
  showMainHelp,
@@ -49,13 +71,16 @@ const mainCommand = Command.make('mdcontext').pipe(
49
71
  treeCommand,
50
72
  linksCommand,
51
73
  backlinksCommand,
74
+ duplicatesCommand,
52
75
  statsCommand,
76
+ configCommand,
77
+ embeddingsCommand,
53
78
  ]),
54
79
  )
55
80
 
56
81
  const cli = Command.run(mainCommand, {
57
82
  name: 'mdcontext',
58
- version: '0.1.0',
83
+ version: CLI_VERSION,
59
84
  })
60
85
 
61
86
  // Clean CLI config: hide built-in options from help
@@ -67,47 +92,7 @@ const cliConfigLayer = CliConfig.layer({
67
92
  // Error Handling
68
93
  // ============================================================================
69
94
 
70
- // Custom error formatter
71
- const formatCliError = (error: unknown): string => {
72
- if (error && typeof error === 'object') {
73
- // Handle Effect CLI validation errors
74
- const err = error as Record<string, unknown>
75
- if (err._tag === 'ValidationError' && err.error) {
76
- const validationError = err.error as Record<string, unknown>
77
- // Extract the actual error message
78
- if (validationError._tag === 'Paragraph' && validationError.value) {
79
- const paragraph = validationError.value as Record<string, unknown>
80
- if (paragraph._tag === 'Text' && typeof paragraph.value === 'string') {
81
- return paragraph.value
82
- }
83
- }
84
- }
85
- // Handle MissingValue errors
86
- if (err._tag === 'MissingValue' && err.error) {
87
- const missingError = err.error as Record<string, unknown>
88
- if (missingError._tag === 'Paragraph' && missingError.value) {
89
- const paragraph = missingError.value as Record<string, unknown>
90
- if (paragraph._tag === 'Text' && typeof paragraph.value === 'string') {
91
- return paragraph.value
92
- }
93
- }
94
- }
95
- }
96
- return String(error)
97
- }
98
-
99
- // Check if error is a CLI validation error (should show friendly message)
100
- const isValidationError = (error: unknown): boolean => {
101
- if (error && typeof error === 'object') {
102
- const err = error as Record<string, unknown>
103
- return (
104
- err._tag === 'ValidationError' ||
105
- err._tag === 'MissingValue' ||
106
- err._tag === 'InvalidValue'
107
- )
108
- }
109
- return false
110
- }
95
+ // Note: Error formatting and validation checking moved to error-handler.ts
111
96
 
112
97
  // ============================================================================
113
98
  // Custom Help Handling
@@ -116,6 +101,9 @@ const isValidationError = (error: unknown): boolean => {
116
101
  // Check for subcommand help before anything else
117
102
  checkSubcommandHelp()
118
103
 
104
+ // Check for bare subcommand that has nested subcommands (e.g., "config")
105
+ checkBareSubcommandHelp()
106
+
119
107
  // Check if we should show main help
120
108
  if (shouldShowMainHelp()) {
121
109
  showMainHelp()
@@ -125,21 +113,323 @@ if (shouldShowMainHelp()) {
125
113
  // Preprocess argv to allow flexible flag positioning
126
114
  const processedArgv = preprocessArgv(process.argv)
127
115
 
128
- // Run with clean config and friendly errors
129
- Effect.suspend(() => cli(processedArgv)).pipe(
130
- Effect.provide(Layer.merge(NodeContext.layer, cliConfigLayer)),
131
- Effect.catchAll((error) =>
132
- Effect.sync(() => {
133
- // Only show friendly error for validation errors
134
- if (isValidationError(error)) {
135
- const message = formatCliError(error)
136
- console.error(`\nError: ${message}`)
137
- console.error('\nRun "mdcontext --help" for usage information.')
116
+ // ============================================================================
117
+ // Global --config Flag Handling
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Extract --config or -c flag from argv before CLI parsing.
122
+ * This allows loading custom config files before the CLI runs.
123
+ */
124
+ const extractConfigPath = (
125
+ argv: string[],
126
+ ): { configPath: string | undefined; filteredArgv: string[] } => {
127
+ const filteredArgv: string[] = []
128
+ let configPath: string | undefined
129
+
130
+ for (let i = 0; i < argv.length; i++) {
131
+ const arg = argv[i]
132
+ if (arg === undefined) continue
133
+
134
+ // --config=path or -c=path
135
+ if (arg.startsWith('--config=')) {
136
+ const value = arg.slice('--config='.length)
137
+ if (value.length === 0) {
138
+ console.error('\nError: --config requires a path')
139
+ console.error(' Usage: --config=path/to/config.js')
138
140
  process.exit(1)
139
141
  }
140
- // Re-throw other errors to be handled normally
141
- throw error
142
- }),
143
- ),
144
- NodeRuntime.runMain,
145
- )
142
+ configPath = value
143
+ continue
144
+ }
145
+ if (arg.startsWith('-c=')) {
146
+ const value = arg.slice('-c='.length)
147
+ if (value.length === 0) {
148
+ console.error('\nError: -c requires a path')
149
+ console.error(' Usage: -c=path/to/config.js')
150
+ process.exit(1)
151
+ }
152
+ configPath = value
153
+ continue
154
+ }
155
+
156
+ // --config path or -c path
157
+ if (arg === '--config' || arg === '-c') {
158
+ const nextArg = argv[i + 1]
159
+ if (!nextArg || nextArg.startsWith('-')) {
160
+ console.error('\nError: --config requires a path')
161
+ console.error(' Usage: --config path/to/config.js')
162
+ process.exit(1)
163
+ }
164
+ if (nextArg.length === 0) {
165
+ console.error('\nError: --config path cannot be empty')
166
+ process.exit(1)
167
+ }
168
+ configPath = nextArg
169
+ i++ // Skip the path argument
170
+ continue
171
+ }
172
+
173
+ filteredArgv.push(arg)
174
+ }
175
+
176
+ return { configPath, filteredArgv }
177
+ }
178
+
179
+ // Extract config path from processed argv
180
+ const { configPath: customConfigPath, filteredArgv } =
181
+ extractConfigPath(processedArgv)
182
+
183
+ // ============================================================================
184
+ // Config Loading Utilities (shared between async and sync paths)
185
+ // ============================================================================
186
+
187
+ /**
188
+ * Validate config file exists and exit with error if not found.
189
+ */
190
+ function validateConfigFileExists(resolvedPath: string): void {
191
+ if (!fs.existsSync(resolvedPath)) {
192
+ console.error(`\nError: Config file not found: ${resolvedPath}`)
193
+ process.exit(1)
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Handle config loading error with consistent formatting.
199
+ */
200
+ const handleConfigLoadError = (error: unknown, resolvedPath: string): never => {
201
+ console.error(`\nError: Failed to load config file: ${resolvedPath}`)
202
+ if (error instanceof Error) {
203
+ console.error(` ${error.message}`)
204
+ }
205
+ process.exit(1)
206
+ }
207
+
208
+ /**
209
+ * Valid top-level config keys that the config system recognizes.
210
+ */
211
+ const VALID_CONFIG_KEYS = [
212
+ 'index',
213
+ 'search',
214
+ 'embeddings',
215
+ 'summarization',
216
+ 'output',
217
+ 'paths',
218
+ ] as const
219
+
220
+ /**
221
+ * Validate that a loaded config is a valid object (not null, not array).
222
+ * Also validates that if it has keys, at least one is a recognized config key.
223
+ * Uses assertion function to narrow type for TypeScript.
224
+ */
225
+ function validateConfigObject(
226
+ config: unknown,
227
+ resolvedPath: string,
228
+ ): asserts config is PartialMdContextConfig {
229
+ // Check it's a non-null, non-array object
230
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
231
+ console.error(
232
+ `\nError: Config file must export a default object or named "config" export`,
233
+ )
234
+ console.error(` File: ${resolvedPath}`)
235
+ process.exit(1)
236
+ }
237
+
238
+ // Validate structure - if there are keys, at least one should be recognized
239
+ const configKeys = Object.keys(config)
240
+ const hasValidKey = configKeys.some((key) =>
241
+ VALID_CONFIG_KEYS.includes(key as (typeof VALID_CONFIG_KEYS)[number]),
242
+ )
243
+
244
+ if (configKeys.length > 0 && !hasValidKey) {
245
+ console.error(`\nError: Config file has no recognized configuration keys`)
246
+ console.error(` File: ${resolvedPath}`)
247
+ console.error(` Found keys: ${configKeys.join(', ')}`)
248
+ console.error(` Expected at least one of: ${VALID_CONFIG_KEYS.join(', ')}`)
249
+ process.exit(1)
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Create a ConfigService Layer from a validated config object.
255
+ */
256
+ const createConfigLayerFromConfig = (
257
+ fileConfig: PartialMdContextConfig,
258
+ ): Layer.Layer<ConfigService, never, never> => {
259
+ const provider = createConfigProviderSync({
260
+ fileConfig,
261
+ skipEnv: false,
262
+ })
263
+ const configResult = Effect.runSync(
264
+ MdContextConfig.pipe(Effect.withConfigProvider(provider)),
265
+ )
266
+ return Layer.succeed(ConfigService, configResult)
267
+ }
268
+
269
+ /**
270
+ * Load a TS/JS/MJS config file asynchronously using dynamic import.
271
+ * Returns a promise that resolves to a ConfigService Layer.
272
+ */
273
+ async function loadConfigAsync(
274
+ configPath: string,
275
+ ): Promise<Layer.Layer<ConfigService, never, never>> {
276
+ const resolvedPath = path.resolve(configPath)
277
+ validateConfigFileExists(resolvedPath)
278
+
279
+ try {
280
+ // Use dynamic import to load TS/JS/MJS files
281
+ const fileUrl = `file://${resolvedPath}`
282
+ const module = (await import(fileUrl)) as {
283
+ default?: PartialMdContextConfig
284
+ config?: PartialMdContextConfig
285
+ }
286
+ const fileConfig = module.default ?? module.config
287
+
288
+ validateConfigObject(fileConfig, resolvedPath)
289
+ return createConfigLayerFromConfig(fileConfig)
290
+ } catch (error) {
291
+ // handleConfigLoadError calls process.exit(1) and never returns
292
+ // TypeScript needs explicit return for type checking - this is unreachable
293
+ return handleConfigLoadError(error, resolvedPath)
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Determine if we need async loading (for TS/JS config files).
299
+ * All non-JSON config files need async loading via dynamic import.
300
+ */
301
+ const needsAsyncLoading = (configPath: string | undefined): boolean => {
302
+ if (!configPath) return false
303
+ const ext = path.extname(configPath).toLowerCase()
304
+ // Async load for all JS/TS variants, sync for JSON only
305
+ return ext !== '.json'
306
+ }
307
+
308
+ /**
309
+ * Create config layer synchronously (for JSON or no custom config).
310
+ */
311
+ function createConfigLayerSync(): Layer.Layer<ConfigService, never, never> {
312
+ if (!customConfigPath) {
313
+ return defaultCliConfigLayerSync
314
+ }
315
+
316
+ const resolvedPath = path.resolve(customConfigPath)
317
+ validateConfigFileExists(resolvedPath)
318
+
319
+ try {
320
+ const content = fs.readFileSync(resolvedPath, 'utf-8')
321
+
322
+ // Parse JSON with proper validation
323
+ let parsed: unknown
324
+ try {
325
+ parsed = JSON.parse(content)
326
+ } catch (parseError) {
327
+ console.error(`\nError: Invalid JSON in config file: ${resolvedPath}`)
328
+ console.error(
329
+ ` ${parseError instanceof Error ? parseError.message : String(parseError)}`,
330
+ )
331
+ process.exit(1)
332
+ }
333
+
334
+ // Validate structure before using
335
+ validateConfigObject(parsed, resolvedPath)
336
+ return createConfigLayerFromConfig(parsed)
337
+ } catch (error) {
338
+ // handleConfigLoadError calls process.exit(1) and never returns
339
+ return handleConfigLoadError(error, resolvedPath)
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Run the CLI with error handling.
345
+ */
346
+ const runCli = (
347
+ configLayer: Layer.Layer<ConfigService, never, never>,
348
+ ): void => {
349
+ const appLayers = Layer.mergeAll(
350
+ NodeContext.layer,
351
+ cliConfigLayer,
352
+ configLayer,
353
+ )
354
+
355
+ Effect.suspend(() => cli(filteredArgv)).pipe(
356
+ Effect.provide(appLayers),
357
+ Effect.tap(() =>
358
+ Effect.sync(() => {
359
+ // Force exit after successful completion to prevent hanging
360
+ // This is necessary because some dependencies (like OpenAI SDK)
361
+ // may keep the event loop alive with HTTP keep-alive connections
362
+ setImmediate(() => process.exit(0))
363
+ }),
364
+ ),
365
+ Effect.catchAll((error) =>
366
+ Effect.sync(() => {
367
+ if (isEffectCliValidationError(error)) {
368
+ const message = formatEffectCliError(error)
369
+ console.error(`\nError: ${message}`)
370
+ console.error('\nRun "mdcontext --help" for usage information.')
371
+ process.exit(1)
372
+ }
373
+ // Handle all other unexpected errors instead of rethrowing
374
+ console.error('\nUnexpected error:')
375
+ if (error instanceof Error) {
376
+ console.error(` ${error.message}`)
377
+ if (error.stack) {
378
+ console.error(`\nStack trace:`)
379
+ console.error(error.stack)
380
+ }
381
+ } else {
382
+ console.error(util.inspect(error, { depth: null }))
383
+ }
384
+ process.exit(2)
385
+ }),
386
+ ),
387
+ NodeRuntime.runMain,
388
+ )
389
+ }
390
+
391
+ // Handle async vs sync config loading based on file type
392
+ if (needsAsyncLoading(customConfigPath)) {
393
+ // Async path for TS/JS/MJS config files using async/await with proper error handling
394
+ ;(async () => {
395
+ // Runtime check for config path - TypeScript can't verify needsAsyncLoading's guard
396
+ if (!customConfigPath) {
397
+ console.error('\nError: Config path is required for async loading')
398
+ process.exit(1)
399
+ }
400
+
401
+ try {
402
+ const configLayer = await loadConfigAsync(customConfigPath)
403
+ runCli(configLayer)
404
+ } catch (error) {
405
+ // This catches errors from runCli, not loadConfigAsync
406
+ // (loadConfigAsync has its own error handling that calls process.exit)
407
+ console.error(`\nError: Failed to initialize CLI`)
408
+ if (error instanceof Error) {
409
+ console.error(` ${error.message}`)
410
+ if (error.stack) {
411
+ console.error(`\nStack trace:`)
412
+ console.error(error.stack)
413
+ }
414
+ }
415
+ process.exit(1)
416
+ }
417
+ })().catch((error) => {
418
+ // Catch any errors that escape the try-catch (e.g., errors before try block)
419
+ console.error('\nUnexpected error during initialization')
420
+ if (error instanceof Error) {
421
+ console.error(` ${error.message}`)
422
+ if (error.stack) {
423
+ console.error(`\nStack trace:`)
424
+ console.error(error.stack)
425
+ }
426
+ } else {
427
+ console.error(util.inspect(error, { depth: null }))
428
+ }
429
+ process.exit(1)
430
+ })
431
+ } else {
432
+ // Sync path for JSON configs or no custom config
433
+ const configLayer = createConfigLayerSync()
434
+ runCli(configLayer)
435
+ }
@@ -6,6 +6,16 @@
6
6
 
7
7
  import { Options } from '@effect/cli'
8
8
 
9
+ /**
10
+ * Global config file path override
11
+ * Allows specifying a custom config file instead of auto-detection.
12
+ */
13
+ export const configOption = Options.file('config').pipe(
14
+ Options.withAlias('c'),
15
+ Options.withDescription('Path to config file'),
16
+ Options.optional,
17
+ )
18
+
9
19
  /**
10
20
  * Output as JSON
11
21
  */
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Shared Error Handling Utilities
3
+ *
4
+ * This module provides reusable error handling patterns for CLI commands.
5
+ * It eliminates duplication in catchTags blocks across index-cmd.ts and search.ts.
6
+ *
7
+ * ## Design Principles
8
+ *
9
+ * 1. **Use proper Effect composition** - Never use Effect.runSync inside error handlers.
10
+ * Instead, return Effect values that compose properly in the Effect pipeline.
11
+ *
12
+ * 2. **Graceful degradation** - For optional operations, return null on failure
13
+ * and let the caller decide what to do next.
14
+ *
15
+ * 3. **Consistent logging** - Use Effect.logWarning for debugging info and
16
+ * Console.error for user-facing errors. Support silent mode for JSON output.
17
+ *
18
+ * ## When to Catch vs Propagate
19
+ *
20
+ * **CATCH errors when:**
21
+ * - The operation is optional (e.g., cost estimate for user prompt)
22
+ * - Failure should fall back gracefully (e.g., auto-index attempt)
23
+ *
24
+ * **PROPAGATE errors when:**
25
+ * - The operation is required for the command to succeed
26
+ * - The centralized error handler (error-handler.ts) should format the message
27
+ *
28
+ * ## Usage
29
+ * ```typescript
30
+ * const result = yield* someOperation.pipe(
31
+ * Effect.map((r): SomeType | null => r),
32
+ * Effect.catchTags(createEmbeddingErrorHandler({ silent: json }))
33
+ * )
34
+ * ```
35
+ */
36
+
37
+ import { Console, Effect } from 'effect'
38
+ import type { BuildEmbeddingsResult } from '../embeddings/semantic-search.js'
39
+ import type {
40
+ ApiKeyInvalidError,
41
+ ApiKeyMissingError,
42
+ EmbeddingError,
43
+ FileReadError,
44
+ IndexCorruptedError,
45
+ IndexNotFoundError,
46
+ VectorStoreError,
47
+ } from '../errors/index.js'
48
+
49
+ // ============================================================================
50
+ // Types
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Options for error handlers.
55
+ */
56
+ export interface ErrorHandlerOptions {
57
+ /**
58
+ * When true, suppress error output (for JSON mode).
59
+ */
60
+ readonly silent?: boolean
61
+ }
62
+
63
+ /**
64
+ * A result type that can be null when operation fails gracefully.
65
+ */
66
+ export type NullableResult<T> = T | null
67
+
68
+ // ============================================================================
69
+ // Logging Helpers
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Log an error message if not in silent mode.
74
+ * Returns an Effect that can be composed properly.
75
+ */
76
+ const logErrorUnlessSilent = (
77
+ message: string,
78
+ silent: boolean,
79
+ ): Effect.Effect<void, never, never> =>
80
+ silent ? Effect.void : Console.error(message)
81
+
82
+ /**
83
+ * Log a warning message if not in silent mode.
84
+ * Returns an Effect that can be composed properly.
85
+ */
86
+ const logWarningUnlessSilent = (
87
+ message: string,
88
+ silent: boolean,
89
+ ): Effect.Effect<void, never, never> =>
90
+ silent ? Effect.void : Effect.logWarning(message)
91
+
92
+ // ============================================================================
93
+ // Index/File Error Handlers
94
+ // ============================================================================
95
+
96
+ /**
97
+ * Create a handler for index-related errors that returns null on failure.
98
+ * Use this for operations where index errors should gracefully degrade.
99
+ */
100
+ export const createIndexErrorHandler = <T>(options: ErrorHandlerOptions = {}) =>
101
+ ({
102
+ IndexNotFoundError: (_e: IndexNotFoundError) =>
103
+ Effect.succeed(null as NullableResult<T>),
104
+ FileReadError: (e: FileReadError) =>
105
+ logWarningUnlessSilent(
106
+ `Could not read index files: ${e.message}`,
107
+ options.silent ?? false,
108
+ ).pipe(Effect.map(() => null as NullableResult<T>)),
109
+ IndexCorruptedError: (e: IndexCorruptedError) =>
110
+ logWarningUnlessSilent(
111
+ `Index is corrupted: ${e.details ?? e.reason}`,
112
+ options.silent ?? false,
113
+ ).pipe(Effect.map(() => null as NullableResult<T>)),
114
+ }) as const
115
+
116
+ // ============================================================================
117
+ // Embedding Error Handlers
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Create a comprehensive handler for embedding-related errors.
122
+ * Handles API key errors, index errors, embedding errors, and vector store errors.
123
+ * Returns null on failure for graceful degradation.
124
+ *
125
+ * Use this for:
126
+ * - Optional embedding operations (e.g., user prompt in index command)
127
+ * - Auto-index attempts in search command
128
+ * - Any embedding operation that should fall back gracefully
129
+ */
130
+ export const createEmbeddingErrorHandler = (
131
+ options: ErrorHandlerOptions = {},
132
+ ) => {
133
+ const silent = options.silent ?? false
134
+
135
+ return {
136
+ // API key errors - user needs to set up API key
137
+ ApiKeyMissingError: (e: ApiKeyMissingError) =>
138
+ logErrorUnlessSilent(`\n${e.message}`, silent).pipe(
139
+ Effect.map(() => null as BuildEmbeddingsResult | null),
140
+ ),
141
+ ApiKeyInvalidError: (e: ApiKeyInvalidError) =>
142
+ logErrorUnlessSilent(`\n${e.message}`, silent).pipe(
143
+ Effect.map(() => null as BuildEmbeddingsResult | null),
144
+ ),
145
+ // Index not found - shouldn't happen after buildIndex but handle gracefully
146
+ IndexNotFoundError: (_e: IndexNotFoundError) =>
147
+ Effect.succeed(null as BuildEmbeddingsResult | null),
148
+ // File system errors
149
+ FileReadError: (e: FileReadError) =>
150
+ logErrorUnlessSilent(
151
+ `\nCannot read index files: ${e.message}`,
152
+ silent,
153
+ ).pipe(Effect.map(() => null as BuildEmbeddingsResult | null)),
154
+ IndexCorruptedError: (e: IndexCorruptedError) =>
155
+ logErrorUnlessSilent(
156
+ `\nIndex is corrupted: ${e.details ?? e.reason}`,
157
+ silent,
158
+ ).pipe(Effect.map(() => null as BuildEmbeddingsResult | null)),
159
+ // Embedding errors - network, rate limit, etc
160
+ EmbeddingError: (e: EmbeddingError) =>
161
+ logErrorUnlessSilent(`\nEmbedding failed: ${e.message}`, silent).pipe(
162
+ Effect.map(() => null as BuildEmbeddingsResult | null),
163
+ ),
164
+ // Vector store errors
165
+ VectorStoreError: (e: VectorStoreError) =>
166
+ logErrorUnlessSilent(`\nVector store error: ${e.message}`, silent).pipe(
167
+ Effect.map(() => null as BuildEmbeddingsResult | null),
168
+ ),
169
+ } as const
170
+ }
171
+
172
+ /**
173
+ * Create a handler for cost estimation errors.
174
+ * Returns null on failure for graceful degradation.
175
+ *
176
+ * NOTE: Use with Effect.catchTags after Effect.map to preserve type:
177
+ * ```typescript
178
+ * const result = yield* operation.pipe(
179
+ * Effect.map((r): TargetType | null => r),
180
+ * Effect.catchTags(createCostEstimateErrorHandler())
181
+ * )
182
+ * ```
183
+ */
184
+ export const createCostEstimateErrorHandler = (
185
+ options: ErrorHandlerOptions = {},
186
+ ) =>
187
+ ({
188
+ IndexNotFoundError: (_e: IndexNotFoundError) => Effect.succeed(null),
189
+ FileReadError: (e: FileReadError) =>
190
+ logWarningUnlessSilent(
191
+ `Could not read index files: ${e.message}`,
192
+ options.silent ?? false,
193
+ ).pipe(Effect.map(() => null)),
194
+ IndexCorruptedError: (e: IndexCorruptedError) =>
195
+ logWarningUnlessSilent(
196
+ `Index is corrupted: ${e.details ?? e.reason}`,
197
+ options.silent ?? false,
198
+ ).pipe(Effect.map(() => null)),
199
+ }) as const