mdcontext 0.0.1 → 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 (337) hide show
  1. package/.changeset/README.md +28 -0
  2. package/.changeset/config.json +11 -0
  3. package/.claude/settings.local.json +25 -0
  4. package/.github/workflows/ci.yml +83 -0
  5. package/.github/workflows/claude-code-review.yml +44 -0
  6. package/.github/workflows/claude.yml +85 -0
  7. package/.github/workflows/release.yml +113 -0
  8. package/.tldrignore +112 -0
  9. package/BACKLOG.md +338 -0
  10. package/CONTRIBUTING.md +186 -0
  11. package/NOTES/NOTES +44 -0
  12. package/README.md +434 -11
  13. package/biome.json +36 -0
  14. package/cspell.config.yaml +14 -0
  15. package/dist/chunk-23UPXDNL.js +3044 -0
  16. package/dist/chunk-2W7MO2DL.js +1366 -0
  17. package/dist/chunk-3NUAZGMA.js +1689 -0
  18. package/dist/chunk-7TOWB2XB.js +366 -0
  19. package/dist/chunk-7XOTOADQ.js +3065 -0
  20. package/dist/chunk-AH2PDM2K.js +3042 -0
  21. package/dist/chunk-BNXWSZ63.js +3742 -0
  22. package/dist/chunk-BTL5DJVU.js +3222 -0
  23. package/dist/chunk-HDHYG7E4.js +104 -0
  24. package/dist/chunk-HLR4KZBP.js +3234 -0
  25. package/dist/chunk-IP3FRFEB.js +1045 -0
  26. package/dist/chunk-KHU56VDO.js +3042 -0
  27. package/dist/chunk-KRYIFLQR.js +88 -0
  28. package/dist/chunk-LBSDNLEM.js +287 -0
  29. package/dist/chunk-MNTQ7HCP.js +2643 -0
  30. package/dist/chunk-MUJELQQ6.js +1387 -0
  31. package/dist/chunk-MXJGMSLV.js +2199 -0
  32. package/dist/chunk-N6QJGC3Z.js +2636 -0
  33. package/dist/chunk-OBELGBPM.js +1713 -0
  34. package/dist/chunk-OT7R5XTA.js +3192 -0
  35. package/dist/chunk-P7X4RA2T.js +106 -0
  36. package/dist/chunk-PIDUQNC2.js +3185 -0
  37. package/dist/chunk-POGCDIH4.js +3187 -0
  38. package/dist/chunk-PSIEOQGZ.js +3043 -0
  39. package/dist/chunk-PVRT3IHA.js +3238 -0
  40. package/dist/chunk-QNN4TT23.js +1430 -0
  41. package/dist/chunk-RE3R45RJ.js +3042 -0
  42. package/dist/chunk-S7E6TFX6.js +803 -0
  43. package/dist/chunk-SG6GLU4U.js +1378 -0
  44. package/dist/chunk-SJCDV2ST.js +274 -0
  45. package/dist/chunk-SYE5XLF3.js +104 -0
  46. package/dist/chunk-T5VLYBZD.js +103 -0
  47. package/dist/chunk-TOQB7VWU.js +3238 -0
  48. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  49. package/dist/chunk-VVTGZNBT.js +1629 -0
  50. package/dist/chunk-W7Q4RFEV.js +104 -0
  51. package/dist/chunk-XTYYVRLO.js +3190 -0
  52. package/dist/chunk-Y6MDYVJD.js +3063 -0
  53. package/dist/cli/main.d.ts +1 -0
  54. package/dist/cli/main.js +5458 -0
  55. package/dist/index.d.ts +653 -0
  56. package/dist/index.js +79 -0
  57. package/dist/mcp/server.d.ts +1 -0
  58. package/dist/mcp/server.js +472 -0
  59. package/dist/schema-BAWSG7KY.js +22 -0
  60. package/dist/schema-E3QUPL26.js +20 -0
  61. package/dist/schema-EHL7WUT6.js +20 -0
  62. package/docs/019-USAGE.md +625 -0
  63. package/docs/020-current-implementation.md +364 -0
  64. package/docs/021-DOGFOODING-FINDINGS.md +175 -0
  65. package/docs/BACKLOG.md +80 -0
  66. package/docs/CONFIG.md +1123 -0
  67. package/docs/DESIGN.md +439 -0
  68. package/docs/ERRORS.md +383 -0
  69. package/docs/PROJECT.md +88 -0
  70. package/docs/ROADMAP.md +407 -0
  71. package/docs/summarization.md +320 -0
  72. package/docs/test-links.md +9 -0
  73. package/justfile +40 -0
  74. package/package.json +74 -9
  75. package/pnpm-workspace.yaml +5 -0
  76. package/research/INDEX.md +315 -0
  77. package/research/code-review/README.md +90 -0
  78. package/research/code-review/cli-error-handling-review.md +979 -0
  79. package/research/code-review/code-review-validation-report.md +464 -0
  80. package/research/code-review/main-ts-review.md +1128 -0
  81. package/research/config-analysis/01-current-implementation.md +470 -0
  82. package/research/config-analysis/02-strategy-recommendation.md +428 -0
  83. package/research/config-analysis/03-task-candidates.md +715 -0
  84. package/research/config-analysis/033-research-configuration-management.md +828 -0
  85. package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
  86. package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
  87. package/research/config-docs/SUMMARY.md +357 -0
  88. package/research/config-docs/TEST-RESULTS.md +776 -0
  89. package/research/config-docs/TODO.md +542 -0
  90. package/research/config-docs/analysis.md +744 -0
  91. package/research/config-docs/fix-validation.md +502 -0
  92. package/research/config-docs/help-audit.md +264 -0
  93. package/research/config-docs/help-system-analysis.md +890 -0
  94. package/research/dogfood/consolidated-tool-evaluation.md +373 -0
  95. package/research/dogfood/strategy-a/a-synthesis.md +184 -0
  96. package/research/dogfood/strategy-a/a1-docs.md +226 -0
  97. package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
  98. package/research/dogfood/strategy-a/a3-llm.md +164 -0
  99. package/research/dogfood/strategy-b/b-synthesis.md +228 -0
  100. package/research/dogfood/strategy-b/b1-architecture.md +207 -0
  101. package/research/dogfood/strategy-b/b2-gaps.md +258 -0
  102. package/research/dogfood/strategy-b/b3-workflows.md +250 -0
  103. package/research/dogfood/strategy-c/c-synthesis.md +451 -0
  104. package/research/dogfood/strategy-c/c1-explorer.md +192 -0
  105. package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
  106. package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
  107. package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
  108. package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
  109. package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
  110. package/research/effect-cli-error-handling.md +845 -0
  111. package/research/effect-errors-as-values.md +943 -0
  112. package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
  113. package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
  114. package/research/errors-task-analysis/embeddings-analysis.md +709 -0
  115. package/research/errors-task-analysis/index-search-analysis.md +812 -0
  116. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  117. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  118. package/research/issue-review.md +603 -0
  119. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  120. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  121. package/research/llm-summarization/anthropic-2026.md +367 -0
  122. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  123. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  124. package/research/llm-summarization/openai-2026.md +473 -0
  125. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  126. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  127. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  128. package/research/llm-summarization/prototype-results.md +56 -0
  129. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  130. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  131. package/research/mdcontext-error-analysis.md +521 -0
  132. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  133. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  134. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  135. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  136. package/research/mdcontext-pudding/02-search.md +970 -0
  137. package/research/mdcontext-pudding/03-context.md +779 -0
  138. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  139. package/research/mdcontext-pudding/04-tree.md +704 -0
  140. package/research/mdcontext-pudding/05-config.md +1038 -0
  141. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  142. package/research/mdcontext-pudding/06-links.md +679 -0
  143. package/research/mdcontext-pudding/07-stats.md +693 -0
  144. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  145. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  146. package/research/mdcontext-pudding/README.md +168 -0
  147. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  148. package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
  149. package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
  150. package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
  151. package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
  152. package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
  153. package/research/research-quality-review.md +834 -0
  154. package/research/semantic-search/002-research-embedding-models.md +490 -0
  155. package/research/semantic-search/003-research-rag-alternatives.md +523 -0
  156. package/research/semantic-search/004-research-vector-search.md +841 -0
  157. package/research/semantic-search/032-research-semantic-search.md +427 -0
  158. package/research/semantic-search/embedding-text-analysis.md +156 -0
  159. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  160. package/research/semantic-search/query-processing-analysis.md +207 -0
  161. package/research/semantic-search/root-cause-and-solution.md +114 -0
  162. package/research/semantic-search/threshold-validation-report.md +69 -0
  163. package/research/semantic-search/vector-search-analysis.md +63 -0
  164. package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
  165. package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
  166. package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
  167. package/research/task-management-2026/03-lightweight-file-based.md +567 -0
  168. package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
  169. package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
  170. package/research/task-management-2026/linear/02-api-integrations.md +930 -0
  171. package/research/task-management-2026/linear/03-ai-features.md +368 -0
  172. package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
  173. package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
  174. package/research/test-path-issues.md +276 -0
  175. package/review/ALP-76/1-error-type-design.md +962 -0
  176. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  177. package/review/ALP-76/3-error-presentation.md +624 -0
  178. package/review/ALP-76/4-test-coverage.md +625 -0
  179. package/review/ALP-76/5-migration-completeness.md +440 -0
  180. package/review/ALP-76/6-effect-best-practices.md +755 -0
  181. package/scripts/apply-branch-protection.sh +47 -0
  182. package/scripts/branch-protection-templates.json +79 -0
  183. package/scripts/prototype-summarization.ts +346 -0
  184. package/scripts/rebuild-hnswlib.js +58 -0
  185. package/scripts/setup-branch-protection.sh +64 -0
  186. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  187. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  188. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  189. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  190. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  191. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  192. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  193. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  194. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  195. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  196. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  197. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  198. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  199. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  200. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  201. package/src/cli/argv-preprocessor.test.ts +210 -0
  202. package/src/cli/argv-preprocessor.ts +202 -0
  203. package/src/cli/cli.test.ts +627 -0
  204. package/src/cli/commands/backlinks.ts +54 -0
  205. package/src/cli/commands/config-cmd.ts +642 -0
  206. package/src/cli/commands/context.ts +285 -0
  207. package/src/cli/commands/duplicates.ts +122 -0
  208. package/src/cli/commands/embeddings.ts +529 -0
  209. package/src/cli/commands/index-cmd.ts +480 -0
  210. package/src/cli/commands/index.ts +16 -0
  211. package/src/cli/commands/links.ts +52 -0
  212. package/src/cli/commands/search.ts +1281 -0
  213. package/src/cli/commands/stats.ts +149 -0
  214. package/src/cli/commands/tree.ts +128 -0
  215. package/src/cli/config-layer.ts +176 -0
  216. package/src/cli/error-handler.test.ts +235 -0
  217. package/src/cli/error-handler.ts +655 -0
  218. package/src/cli/flag-schemas.ts +341 -0
  219. package/src/cli/help.ts +588 -0
  220. package/src/cli/index.ts +9 -0
  221. package/src/cli/main.ts +435 -0
  222. package/src/cli/options.ts +41 -0
  223. package/src/cli/shared-error-handling.ts +199 -0
  224. package/src/cli/typo-suggester.test.ts +105 -0
  225. package/src/cli/typo-suggester.ts +130 -0
  226. package/src/cli/utils.ts +259 -0
  227. package/src/config/file-provider.test.ts +320 -0
  228. package/src/config/file-provider.ts +273 -0
  229. package/src/config/index.ts +72 -0
  230. package/src/config/integration.test.ts +667 -0
  231. package/src/config/precedence.test.ts +277 -0
  232. package/src/config/precedence.ts +451 -0
  233. package/src/config/schema.test.ts +414 -0
  234. package/src/config/schema.ts +603 -0
  235. package/src/config/service.test.ts +320 -0
  236. package/src/config/service.ts +243 -0
  237. package/src/config/testing.test.ts +264 -0
  238. package/src/config/testing.ts +110 -0
  239. package/src/core/index.ts +1 -0
  240. package/src/core/types.ts +113 -0
  241. package/src/duplicates/detector.test.ts +183 -0
  242. package/src/duplicates/detector.ts +414 -0
  243. package/src/duplicates/index.ts +18 -0
  244. package/src/embeddings/embedding-namespace.test.ts +300 -0
  245. package/src/embeddings/embedding-namespace.ts +947 -0
  246. package/src/embeddings/heading-boost.test.ts +222 -0
  247. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  248. package/src/embeddings/hyde.test.ts +272 -0
  249. package/src/embeddings/hyde.ts +264 -0
  250. package/src/embeddings/index.ts +10 -0
  251. package/src/embeddings/openai-provider.ts +414 -0
  252. package/src/embeddings/pricing.json +22 -0
  253. package/src/embeddings/provider-constants.ts +204 -0
  254. package/src/embeddings/provider-errors.test.ts +967 -0
  255. package/src/embeddings/provider-errors.ts +565 -0
  256. package/src/embeddings/provider-factory.test.ts +240 -0
  257. package/src/embeddings/provider-factory.ts +225 -0
  258. package/src/embeddings/provider-integration.test.ts +788 -0
  259. package/src/embeddings/query-preprocessing.test.ts +187 -0
  260. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  261. package/src/embeddings/semantic-search.ts +1270 -0
  262. package/src/embeddings/types.ts +359 -0
  263. package/src/embeddings/vector-store.ts +708 -0
  264. package/src/embeddings/voyage-provider.ts +313 -0
  265. package/src/errors/errors.test.ts +845 -0
  266. package/src/errors/index.ts +533 -0
  267. package/src/index/ignore-patterns.test.ts +354 -0
  268. package/src/index/ignore-patterns.ts +305 -0
  269. package/src/index/index.ts +4 -0
  270. package/src/index/indexer.ts +684 -0
  271. package/src/index/storage.ts +260 -0
  272. package/src/index/types.ts +147 -0
  273. package/src/index/watcher.ts +189 -0
  274. package/src/index.ts +30 -0
  275. package/src/integration/search-keyword.test.ts +678 -0
  276. package/src/mcp/server.ts +612 -0
  277. package/src/parser/index.ts +1 -0
  278. package/src/parser/parser.test.ts +291 -0
  279. package/src/parser/parser.ts +394 -0
  280. package/src/parser/section-filter.test.ts +277 -0
  281. package/src/parser/section-filter.ts +392 -0
  282. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  283. package/src/search/bm25-store.ts +366 -0
  284. package/src/search/cross-encoder.test.ts +253 -0
  285. package/src/search/cross-encoder.ts +406 -0
  286. package/src/search/fuzzy-search.test.ts +419 -0
  287. package/src/search/fuzzy-search.ts +273 -0
  288. package/src/search/hybrid-search.ts +448 -0
  289. package/src/search/path-matcher.test.ts +276 -0
  290. package/src/search/path-matcher.ts +33 -0
  291. package/src/search/query-parser.test.ts +260 -0
  292. package/src/search/query-parser.ts +319 -0
  293. package/src/search/searcher.test.ts +280 -0
  294. package/src/search/searcher.ts +724 -0
  295. package/src/search/wink-bm25.d.ts +30 -0
  296. package/src/summarization/cli-providers/claude.ts +202 -0
  297. package/src/summarization/cli-providers/detection.test.ts +273 -0
  298. package/src/summarization/cli-providers/detection.ts +118 -0
  299. package/src/summarization/cli-providers/index.ts +8 -0
  300. package/src/summarization/cost.test.ts +139 -0
  301. package/src/summarization/cost.ts +102 -0
  302. package/src/summarization/error-handler.test.ts +127 -0
  303. package/src/summarization/error-handler.ts +111 -0
  304. package/src/summarization/index.ts +102 -0
  305. package/src/summarization/pipeline.test.ts +498 -0
  306. package/src/summarization/pipeline.ts +231 -0
  307. package/src/summarization/prompts.test.ts +269 -0
  308. package/src/summarization/prompts.ts +133 -0
  309. package/src/summarization/provider-factory.test.ts +396 -0
  310. package/src/summarization/provider-factory.ts +178 -0
  311. package/src/summarization/types.ts +184 -0
  312. package/src/summarize/budget-bugs.test.ts +620 -0
  313. package/src/summarize/formatters.ts +419 -0
  314. package/src/summarize/index.ts +20 -0
  315. package/src/summarize/summarizer.test.ts +275 -0
  316. package/src/summarize/summarizer.ts +597 -0
  317. package/src/summarize/verify-bugs.test.ts +238 -0
  318. package/src/types/huggingface-transformers.d.ts +66 -0
  319. package/src/utils/index.ts +1 -0
  320. package/src/utils/tokens.test.ts +142 -0
  321. package/src/utils/tokens.ts +186 -0
  322. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  323. package/tests/fixtures/cli/.mdcontext/config.json +8 -0
  324. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  325. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  326. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
  327. package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
  328. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -0
  329. package/tests/fixtures/cli/README.md +9 -0
  330. package/tests/fixtures/cli/api-reference.md +11 -0
  331. package/tests/fixtures/cli/getting-started.md +11 -0
  332. package/tests/integration/embed-index.test.ts +712 -0
  333. package/tests/integration/search-context.test.ts +469 -0
  334. package/tests/integration/search-semantic.test.ts +522 -0
  335. package/tsconfig.json +26 -0
  336. package/vitest.config.ts +16 -0
  337. package/vitest.setup.ts +12 -0
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mdcontext CLI - Token-efficient markdown analysis
5
+ *
6
+ * CORE COMMANDS
7
+ * mdcontext index [path] Index markdown files (default: .)
8
+ * mdcontext search <query> [path] Search by meaning or structure
9
+ * mdcontext context <files...> Get LLM-ready summary
10
+ * mdcontext tree [path|file] Show files or document outline
11
+ *
12
+ * LINK ANALYSIS
13
+ * mdcontext links <file> What does this link to?
14
+ * mdcontext backlinks <file> What links to this?
15
+ *
16
+ * INSPECTION
17
+ * mdcontext stats [path] Index statistics
18
+ */
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
+
30
+ import { CliConfig, Command } from '@effect/cli'
31
+ import { NodeContext, NodeRuntime } from '@effect/platform-node'
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'
36
+ import { preprocessArgv } from './argv-preprocessor.js'
37
+ import {
38
+ backlinksCommand,
39
+ configCommand,
40
+ contextCommand,
41
+ duplicatesCommand,
42
+ embeddingsCommand,
43
+ indexCommand,
44
+ linksCommand,
45
+ searchCommand,
46
+ statsCommand,
47
+ treeCommand,
48
+ } from './commands/index.js'
49
+ import { defaultCliConfigLayerSync } from './config-layer.js'
50
+ import {
51
+ formatEffectCliError,
52
+ isEffectCliValidationError,
53
+ } from './error-handler.js'
54
+ import {
55
+ checkBareSubcommandHelp,
56
+ checkSubcommandHelp,
57
+ shouldShowMainHelp,
58
+ showMainHelp,
59
+ } from './help.js'
60
+
61
+ // ============================================================================
62
+ // Main CLI
63
+ // ============================================================================
64
+
65
+ const mainCommand = Command.make('mdcontext').pipe(
66
+ Command.withDescription('Token-efficient markdown analysis for LLMs'),
67
+ Command.withSubcommands([
68
+ indexCommand,
69
+ searchCommand,
70
+ contextCommand,
71
+ treeCommand,
72
+ linksCommand,
73
+ backlinksCommand,
74
+ duplicatesCommand,
75
+ statsCommand,
76
+ configCommand,
77
+ embeddingsCommand,
78
+ ]),
79
+ )
80
+
81
+ const cli = Command.run(mainCommand, {
82
+ name: 'mdcontext',
83
+ version: CLI_VERSION,
84
+ })
85
+
86
+ // Clean CLI config: hide built-in options from help
87
+ const cliConfigLayer = CliConfig.layer({
88
+ showBuiltIns: false,
89
+ })
90
+
91
+ // ============================================================================
92
+ // Error Handling
93
+ // ============================================================================
94
+
95
+ // Note: Error formatting and validation checking moved to error-handler.ts
96
+
97
+ // ============================================================================
98
+ // Custom Help Handling
99
+ // ============================================================================
100
+
101
+ // Check for subcommand help before anything else
102
+ checkSubcommandHelp()
103
+
104
+ // Check for bare subcommand that has nested subcommands (e.g., "config")
105
+ checkBareSubcommandHelp()
106
+
107
+ // Check if we should show main help
108
+ if (shouldShowMainHelp()) {
109
+ showMainHelp()
110
+ process.exit(0)
111
+ }
112
+
113
+ // Preprocess argv to allow flexible flag positioning
114
+ const processedArgv = preprocessArgv(process.argv)
115
+
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')
140
+ process.exit(1)
141
+ }
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
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared CLI Options
3
+ *
4
+ * Common options used across multiple commands.
5
+ */
6
+
7
+ import { Options } from '@effect/cli'
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
+
19
+ /**
20
+ * Output as JSON
21
+ */
22
+ export const jsonOption = Options.boolean('json').pipe(
23
+ Options.withDescription('Output as JSON'),
24
+ Options.withDefault(false),
25
+ )
26
+
27
+ /**
28
+ * Pretty-print JSON output
29
+ */
30
+ export const prettyOption = Options.boolean('pretty').pipe(
31
+ Options.withDescription('Pretty-print JSON output'),
32
+ Options.withDefault(true),
33
+ )
34
+
35
+ /**
36
+ * Force full rebuild
37
+ */
38
+ export const forceOption = Options.boolean('force').pipe(
39
+ Options.withDescription('Force full rebuild, ignoring cache'),
40
+ Options.withDefault(false),
41
+ )
@@ -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