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,627 @@
1
+ // cspell:words jsno limt xyznonexistent123
2
+ /**
3
+ * E2E tests for mdcontext CLI commands
4
+ * Tests actual CLI execution against dynamically generated test fixtures
5
+ *
6
+ * Test fixture setup:
7
+ * - beforeAll:
8
+ * - When REBUILD_TEST_INDEX=true: Builds index (documents, sections, links) - fast, no API key needed
9
+ * - When INCLUDE_EMBED_TESTS=true: Also builds embeddings (requires OPENAI_API_KEY)
10
+ * - afterAll: frees tiktoken encoder
11
+ *
12
+ * Running tests:
13
+ * - `pnpm test` - Runs with keyword search only (no API key needed)
14
+ * - `pnpm test:full` - Runs all tests including semantic search (requires OPENAI_API_KEY)
15
+ */
16
+
17
+ import { exec } from 'node:child_process'
18
+ import * as path from 'node:path'
19
+ import { promisify } from 'node:util'
20
+ import { Effect } from 'effect'
21
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
22
+ import { buildEmbeddings } from '../embeddings/semantic-search.js'
23
+ import { buildIndex } from '../index/indexer.js'
24
+ import { freeEncoder } from '../utils/tokens.js'
25
+
26
+ const execAsync = promisify(exec)
27
+
28
+ const REBUILD_TEST_INDEX = process.env.REBUILD_TEST_INDEX === 'true'
29
+ const INCLUDE_EMBED_TESTS = process.env.INCLUDE_EMBED_TESTS === 'true'
30
+ const TEST_FIXTURE_DIR = path.join(process.cwd(), 'tests', 'fixtures', 'cli')
31
+ const CLI = `node ${path.join(process.cwd(), 'dist', 'cli', 'main.js')}`
32
+
33
+ const run = async (
34
+ args: string,
35
+ options: { cwd?: string; expectError?: boolean } = {},
36
+ ): Promise<string> => {
37
+ const cwd = options.cwd ?? TEST_FIXTURE_DIR
38
+ try {
39
+ const { stdout } = await execAsync(`${CLI} ${args}`, {
40
+ cwd,
41
+ encoding: 'utf-8',
42
+ })
43
+ return stdout.trim()
44
+ } catch (error: unknown) {
45
+ if (options.expectError) {
46
+ const execError = error as { stderr?: string; stdout?: string }
47
+ return execError.stderr || execError.stdout || ''
48
+ }
49
+ throw error
50
+ }
51
+ }
52
+
53
+ describe.concurrent('mdcontext CLI e2e', () => {
54
+ beforeAll(async () => {
55
+ if (REBUILD_TEST_INDEX) {
56
+ // Build the index and embeddings only once for faster tests
57
+ console.log('Rebuilding test fixture index and embeddings...')
58
+ // Build the index (fast, no API key needed)
59
+ await Effect.runPromise(buildIndex(TEST_FIXTURE_DIR, { force: true }))
60
+ console.log('Index rebuilt.')
61
+
62
+ if (INCLUDE_EMBED_TESTS) {
63
+ console.log('Rebuilding test fixture embeddings...')
64
+ await Effect.runPromise(
65
+ buildEmbeddings(TEST_FIXTURE_DIR, { force: true }),
66
+ )
67
+ console.log('Embeddings rebuilt.')
68
+ }
69
+ }
70
+ })
71
+
72
+ afterAll(async () => {
73
+ // Free tiktoken encoder to prevent process hang
74
+ freeEncoder()
75
+ })
76
+
77
+ describe('--version', () => {
78
+ it('shows version number', async () => {
79
+ const output = await run('--version')
80
+ expect(output).toMatch(/^\d+\.\d+\.\d+$/)
81
+ })
82
+ })
83
+
84
+ describe('--help', () => {
85
+ it('shows help with all commands', async () => {
86
+ const output = await run('--help')
87
+ expect(output).toContain('index')
88
+ expect(output).toContain('search')
89
+ expect(output).toContain('context')
90
+ expect(output).toContain('tree')
91
+ expect(output).toContain('links')
92
+ expect(output).toContain('backlinks')
93
+ expect(output).toContain('stats')
94
+ })
95
+ })
96
+
97
+ describe('subcommand --help', () => {
98
+ const subcommands = [
99
+ 'index',
100
+ 'search',
101
+ 'context',
102
+ 'tree',
103
+ 'links',
104
+ 'backlinks',
105
+ 'stats',
106
+ ]
107
+
108
+ for (const cmd of subcommands) {
109
+ it(`${cmd} --help shows examples and options`, async () => {
110
+ const output = await run(`${cmd} --help`)
111
+ expect(output).toContain('USAGE')
112
+ expect(output).toContain('EXAMPLES')
113
+ expect(output).toContain('OPTIONS')
114
+ expect(output).toContain(`mdcontext ${cmd}`)
115
+ expect(output).not.toContain('A true or false value')
116
+ expect(output).not.toContain('This setting is optional')
117
+ })
118
+ }
119
+
120
+ it('index help shows embedding and watch options', async () => {
121
+ const output = await run('index --help')
122
+ expect(output).toContain('--embed')
123
+ expect(output).toContain('--watch')
124
+ expect(output).toContain('--force')
125
+ })
126
+
127
+ it('search help shows keyword and limit options', async () => {
128
+ const output = await run('search --help')
129
+ expect(output).toContain('--keyword')
130
+ expect(output).toContain('--limit')
131
+ expect(output).toContain('--threshold')
132
+ })
133
+
134
+ it('context help shows token budget option', async () => {
135
+ const output = await run('context --help')
136
+ expect(output).toContain('--tokens')
137
+ expect(output).toContain('--brief')
138
+ expect(output).toContain('--full')
139
+ })
140
+
141
+ it('shows notes section when relevant', async () => {
142
+ const indexHelp = await run('index --help')
143
+ expect(indexHelp).toContain('NOTES')
144
+ expect(indexHelp).toContain('.mdcontext')
145
+
146
+ const searchHelp = await run('search --help')
147
+ expect(searchHelp).toContain('NOTES')
148
+ expect(searchHelp).toContain('semantic')
149
+ })
150
+ })
151
+
152
+ describe('tree command', () => {
153
+ it('lists markdown files in directory', async () => {
154
+ const output = await run('tree')
155
+ expect(output).toContain('Markdown files')
156
+ expect(output).toContain('.md')
157
+ expect(output).toContain('Total:')
158
+ })
159
+
160
+ it('shows document outline for single file', async () => {
161
+ const output = await run('tree README.md')
162
+ expect(output).toContain('# ')
163
+ expect(output).toContain('tokens')
164
+ expect(output).toContain('##')
165
+ })
166
+
167
+ it('defaults to current directory', async () => {
168
+ const output = await run('tree')
169
+ expect(output).toContain('Markdown files')
170
+ })
171
+ })
172
+
173
+ describe('search command', () => {
174
+ it('performs keyword search with -k flag', async () => {
175
+ const output = await run('search -k "getting started"')
176
+ expect(output).toContain('[keyword]')
177
+ expect(output).toContain('Results:')
178
+ })
179
+
180
+ it.skipIf(!INCLUDE_EMBED_TESTS)(
181
+ 'handles no results gracefully',
182
+ async () => {
183
+ const output = await run('search "xyznonexistent123"')
184
+ expect(output).toContain('Results: 0')
185
+ },
186
+ )
187
+
188
+ it('supports -k flag for explicit keyword search', async () => {
189
+ const output = await run('search -k "API Reference"')
190
+ expect(output).toContain('Content search')
191
+ })
192
+
193
+ it.skipIf(!INCLUDE_EMBED_TESTS)(
194
+ 'supports -n flag to limit results',
195
+ async () => {
196
+ const output = await run('search -n 2 "the"')
197
+ const lines = output
198
+ .split('\n')
199
+ .filter((l) => l.trim().match(/^\w+.*\.md/))
200
+ expect(lines.length).toBeLessThanOrEqual(2)
201
+ },
202
+ )
203
+
204
+ it('shows mode indicator in output', async () => {
205
+ const output = await run('search -k "getting started"')
206
+ expect(output).toContain('[keyword]')
207
+ })
208
+
209
+ it.skipIf(!INCLUDE_EMBED_TESTS)(
210
+ 'supports boolean AND operator',
211
+ async () => {
212
+ const output = await run('search "test AND fixture"')
213
+ expect(output).toContain('Results:')
214
+ },
215
+ )
216
+
217
+ it.skipIf(!INCLUDE_EMBED_TESTS)(
218
+ 'supports boolean OR operator',
219
+ async () => {
220
+ const output = await run('search "installation OR endpoints"')
221
+ expect(output).toContain('Results:')
222
+ },
223
+ )
224
+
225
+ it.skipIf(!INCLUDE_EMBED_TESTS)(
226
+ 'supports boolean NOT operator',
227
+ async () => {
228
+ const output = await run('search "test NOT endpoints"')
229
+ expect(output).toContain('Results:')
230
+ },
231
+ )
232
+
233
+ it.skipIf(!INCLUDE_EMBED_TESTS)(
234
+ 'supports quoted phrase search',
235
+ async () => {
236
+ const output = await run('search \'"Getting Started"\' .')
237
+ expect(output).toContain('Results:')
238
+ },
239
+ )
240
+
241
+ it('supports --mode flag', async () => {
242
+ const output = await run('search --mode keyword "getting started"')
243
+ expect(output).toContain('[keyword]')
244
+ })
245
+
246
+ it.skipIf(!INCLUDE_EMBED_TESTS)(
247
+ 'performs semantic search when embeddings exist',
248
+ async () => {
249
+ const output = await run('search --mode semantic "getting started"')
250
+ expect(output).toContain('[semantic]')
251
+ },
252
+ )
253
+ })
254
+
255
+ describe('context command', () => {
256
+ it('summarizes single file', async () => {
257
+ const output = await run('context README.md')
258
+ expect(output).toContain('# ')
259
+ expect(output).toContain('Tokens:')
260
+ })
261
+
262
+ it.skipIf(!INCLUDE_EMBED_TESTS)('summarizes multiple files', async () => {
263
+ const output = await run('context ./README.md ./getting-started.md')
264
+ expect(output).toContain('Context Assembly')
265
+ expect(output).toContain('Sources: 2')
266
+ })
267
+
268
+ it('shows accurate token count with -t flag', async () => {
269
+ const output = await run('context -t 200 README.md')
270
+ expect(output).toContain('Tokens:')
271
+ })
272
+
273
+ it('supports --brief flag', async () => {
274
+ const brief = await run('context --brief README.md')
275
+ const full = await run('context README.md')
276
+ expect(brief.length).toBeLessThanOrEqual(full.length)
277
+ })
278
+
279
+ it('supports --sections flag to list available sections', async () => {
280
+ const output = await run('context README.md --sections')
281
+ expect(output).toContain('Available sections:')
282
+ expect(output).toContain('tokens')
283
+ })
284
+
285
+ it('supports --section flag to extract specific section', async () => {
286
+ const output = await run('context README.md --section "1"')
287
+ expect(output).toContain('Sections:')
288
+ expect(output).toContain('#')
289
+ })
290
+
291
+ it('supports --sections with --json output', async () => {
292
+ const output = await run('context README.md --sections --json')
293
+ const parsed = JSON.parse(output)
294
+ expect(parsed.sections).toBeDefined()
295
+ expect(Array.isArray(parsed.sections)).toBe(true)
296
+ expect(parsed.sections[0]).toHaveProperty('number')
297
+ expect(parsed.sections[0]).toHaveProperty('heading')
298
+ expect(parsed.sections[0]).toHaveProperty('tokens')
299
+ })
300
+
301
+ it('supports --full flag to disable truncation', async () => {
302
+ const output = await run('context README.md --full')
303
+ expect(output).not.toContain('Truncated')
304
+ })
305
+ })
306
+
307
+ describe('search command context lines', () => {
308
+ it('supports -C flag for context lines', async () => {
309
+ const output = await run('search -k "test" . -C 2')
310
+ expect(output).toContain('[keyword]')
311
+ })
312
+
313
+ it.skipIf(!INCLUDE_EMBED_TESTS)(
314
+ 'supports -B and -A flags for asymmetric context',
315
+ async () => {
316
+ const output = await run('search "test" . -B 1 -A 3')
317
+ expect(output).toContain('[semantic]')
318
+ },
319
+ )
320
+
321
+ it('includes contextLines in JSON output', async () => {
322
+ const output = await run('search -k "test" . -C 2 --json -n 1')
323
+ const parsed = JSON.parse(output)
324
+ expect(parsed.contextBefore).toBe(2)
325
+ expect(parsed.contextAfter).toBe(2)
326
+ if (parsed.results.length > 0 && parsed.results[0].matches) {
327
+ expect(parsed.results[0].matches[0]).toHaveProperty('contextLines')
328
+ }
329
+ })
330
+ })
331
+
332
+ describe('links command', () => {
333
+ it('shows outgoing links from file', async () => {
334
+ const output = await run('links README.md')
335
+ expect(output).toContain('Outgoing links')
336
+ expect(output).toContain('Total:')
337
+ })
338
+ })
339
+
340
+ describe('backlinks command', () => {
341
+ it('shows incoming links to file', async () => {
342
+ const output = await run('backlinks getting-started.md')
343
+ expect(output).toContain('Incoming links')
344
+ expect(output).toContain('Total:')
345
+ })
346
+ })
347
+
348
+ describe('stats command', () => {
349
+ it('shows index statistics', async () => {
350
+ const output = await run('stats')
351
+ expect(output.length).toBeGreaterThan(0)
352
+ })
353
+ })
354
+
355
+ describe('error handling', () => {
356
+ it('handles non-existent file gracefully', async () => {
357
+ const output = await run('tree nonexistent-file-xyz.md', {
358
+ expectError: true,
359
+ })
360
+ expect(output.toLowerCase()).toMatch(/error|not found|no such/i)
361
+ })
362
+
363
+ it('handles non-existent directory gracefully', async () => {
364
+ const output = await run('tree nonexistent-dir-xyz/', {
365
+ expectError: true,
366
+ })
367
+ expect(output.toLowerCase()).toMatch(/error|not found|no such/i)
368
+ })
369
+ })
370
+
371
+ describe('unknown flag handling', () => {
372
+ it('shows clear error for unknown flag', async () => {
373
+ const output = await run('context -z README.md', { expectError: true })
374
+ expect(output).toContain("Unknown option '-z' for 'context'")
375
+ expect(output).toContain('Valid options for')
376
+ })
377
+
378
+ it('suggests typo correction for --jsno', async () => {
379
+ const output = await run('context --jsno README.md', {
380
+ expectError: true,
381
+ })
382
+ expect(output).toContain("Unknown option '--jsno' for 'context'")
383
+ expect(output).toContain("Did you mean '--json'?")
384
+ })
385
+
386
+ it('suggests typo correction for --limt', async () => {
387
+ const output = await run('search --limt 5 "test" .', {
388
+ expectError: true,
389
+ })
390
+ expect(output).toContain("Unknown option '--limt' for 'search'")
391
+ expect(output).toContain("Did you mean '--limit'?")
392
+ })
393
+
394
+ it('lists valid options in error message', async () => {
395
+ const output = await run('context --invalid README.md', {
396
+ expectError: true,
397
+ })
398
+ expect(output).toContain('--tokens')
399
+ expect(output).toContain('--brief')
400
+ expect(output).toContain('--json')
401
+ })
402
+
403
+ it('handles unknown flag with value', async () => {
404
+ const output = await run('context --foo=bar README.md', {
405
+ expectError: true,
406
+ })
407
+ expect(output).toContain("Unknown option '--foo'")
408
+ })
409
+
410
+ it('reports first unknown flag only', async () => {
411
+ const output = await run('context --foo --bar README.md', {
412
+ expectError: true,
413
+ })
414
+ expect(output).toContain("Unknown option '--foo'")
415
+ })
416
+ })
417
+
418
+ describe('flexible flag positioning', () => {
419
+ it('search: allows query before flags', async () => {
420
+ const output = await run('search -k "getting started" -n 2 .')
421
+ expect(output).toContain('Content search')
422
+ expect(output).toContain('Results:')
423
+ })
424
+
425
+ it('search: allows path after flags', async () => {
426
+ const output = await run('search -k "API Reference" .')
427
+ expect(output).toContain('Content search')
428
+ })
429
+
430
+ it('context: allows files before flags', async () => {
431
+ const output = await run('context README.md --brief')
432
+ expect(output).toContain('# ')
433
+ })
434
+
435
+ it('context: allows -t flag after file', async () => {
436
+ const output = await run('context README.md -t 500')
437
+ expect(output).toContain('Tokens:')
438
+ })
439
+
440
+ it('tree: allows path before --json flag', async () => {
441
+ const output = await run('tree . --json')
442
+ expect(output).toContain('[')
443
+ expect(output).toContain('relativePath')
444
+ })
445
+
446
+ it('search: handles --limit=value syntax', async () => {
447
+ const output = await run('search -k "getting started" --limit=2 .')
448
+ expect(output).toContain('Content search')
449
+ })
450
+ })
451
+
452
+ describe('config loading error handling', () => {
453
+ it('shows error for non-existent config file', async () => {
454
+ const output = await run('--config /nonexistent/path.json --help', {
455
+ expectError: true,
456
+ })
457
+ expect(output).toContain('Error: Config file not found')
458
+ expect(output).toContain('path.json')
459
+ })
460
+
461
+ it('shows error for invalid JSON config file', async () => {
462
+ // Create a temp file with invalid JSON
463
+ const fs = await import('node:fs')
464
+ const os = await import('node:os')
465
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
466
+ const invalidConfigPath = path.join(tempDir, 'invalid.json')
467
+ fs.writeFileSync(invalidConfigPath, 'not valid json { broken')
468
+
469
+ try {
470
+ const output = await run(`--config ${invalidConfigPath} --help`, {
471
+ expectError: true,
472
+ })
473
+ expect(output).toContain('Error: Invalid JSON in config file')
474
+ expect(output).toContain(invalidConfigPath)
475
+ } finally {
476
+ fs.rmSync(tempDir, { recursive: true, force: true })
477
+ }
478
+ })
479
+
480
+ it('shows error for JS config that exports non-object', async () => {
481
+ const fs = await import('node:fs')
482
+ const os = await import('node:os')
483
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
484
+ const badConfigPath = path.join(tempDir, 'bad.config.mjs')
485
+ fs.writeFileSync(badConfigPath, 'export default "not an object"')
486
+
487
+ try {
488
+ const output = await run(`--config ${badConfigPath} --help`, {
489
+ expectError: true,
490
+ })
491
+ expect(output).toContain(
492
+ 'Error: Config file must export a default object',
493
+ )
494
+ expect(output).toContain(badConfigPath)
495
+ } finally {
496
+ fs.rmSync(tempDir, { recursive: true, force: true })
497
+ }
498
+ })
499
+
500
+ it('shows error for JS config that exports null', async () => {
501
+ const fs = await import('node:fs')
502
+ const os = await import('node:os')
503
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
504
+ const nullConfigPath = path.join(tempDir, 'null.config.mjs')
505
+ fs.writeFileSync(nullConfigPath, 'export default null')
506
+
507
+ try {
508
+ const output = await run(`--config ${nullConfigPath} --help`, {
509
+ expectError: true,
510
+ })
511
+ expect(output).toContain(
512
+ 'Error: Config file must export a default object',
513
+ )
514
+ expect(output).toContain(nullConfigPath)
515
+ } finally {
516
+ fs.rmSync(tempDir, { recursive: true, force: true })
517
+ }
518
+ })
519
+
520
+ it('shows error for JS config that exports array', async () => {
521
+ const fs = await import('node:fs')
522
+ const os = await import('node:os')
523
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
524
+ const arrayConfigPath = path.join(tempDir, 'array.config.mjs')
525
+ fs.writeFileSync(
526
+ arrayConfigPath,
527
+ 'export default [{ index: { maxDepth: 5 } }]',
528
+ )
529
+
530
+ try {
531
+ const output = await run(`--config ${arrayConfigPath} --help`, {
532
+ expectError: true,
533
+ })
534
+ expect(output).toContain(
535
+ 'Error: Config file must export a default object',
536
+ )
537
+ expect(output).toContain(arrayConfigPath)
538
+ } finally {
539
+ fs.rmSync(tempDir, { recursive: true, force: true })
540
+ }
541
+ })
542
+
543
+ it('shows error for JS config with syntax error', async () => {
544
+ const fs = await import('node:fs')
545
+ const os = await import('node:os')
546
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
547
+ const syntaxErrorPath = path.join(tempDir, 'syntax-error.config.mjs')
548
+ fs.writeFileSync(
549
+ syntaxErrorPath,
550
+ 'export default { invalid syntax here >>>',
551
+ )
552
+
553
+ try {
554
+ const output = await run(`--config ${syntaxErrorPath} --help`, {
555
+ expectError: true,
556
+ })
557
+ expect(output).toContain('Error')
558
+ } finally {
559
+ fs.rmSync(tempDir, { recursive: true, force: true })
560
+ }
561
+ })
562
+
563
+ it('shows error for JS config with no exports', async () => {
564
+ const fs = await import('node:fs')
565
+ const os = await import('node:os')
566
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
567
+ const noExportPath = path.join(tempDir, 'no-export.config.mjs')
568
+ fs.writeFileSync(
569
+ noExportPath,
570
+ 'const config = { index: { maxDepth: 5 } }; // no export',
571
+ )
572
+
573
+ try {
574
+ const output = await run(`--config ${noExportPath} --help`, {
575
+ expectError: true,
576
+ })
577
+ expect(output).toContain(
578
+ 'Error: Config file must export a default object',
579
+ )
580
+ } finally {
581
+ fs.rmSync(tempDir, { recursive: true, force: true })
582
+ }
583
+ })
584
+
585
+ it('loads valid JSON config file successfully', async () => {
586
+ const fs = await import('node:fs')
587
+ const os = await import('node:os')
588
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
589
+ const validConfigPath = path.join(tempDir, 'valid.json')
590
+ fs.writeFileSync(
591
+ validConfigPath,
592
+ JSON.stringify({ index: { maxDepth: 5 } }),
593
+ )
594
+
595
+ try {
596
+ const output = await run(`--config ${validConfigPath} --help`)
597
+ // Should show help without errors
598
+ expect(output).toContain('index')
599
+ expect(output).toContain('search')
600
+ expect(output).not.toContain('Error')
601
+ } finally {
602
+ fs.rmSync(tempDir, { recursive: true, force: true })
603
+ }
604
+ })
605
+
606
+ it('loads valid MJS config file successfully', async () => {
607
+ const fs = await import('node:fs')
608
+ const os = await import('node:os')
609
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdcontext-test-'))
610
+ const validConfigPath = path.join(tempDir, 'valid.config.mjs')
611
+ fs.writeFileSync(
612
+ validConfigPath,
613
+ 'export default { index: { maxDepth: 5 } }',
614
+ )
615
+
616
+ try {
617
+ const output = await run(`--config ${validConfigPath} --help`)
618
+ // Should show help without errors
619
+ expect(output).toContain('index')
620
+ expect(output).toContain('search')
621
+ expect(output).not.toContain('Error')
622
+ } finally {
623
+ fs.rmSync(tempDir, { recursive: true, force: true })
624
+ }
625
+ })
626
+ })
627
+ })
@@ -0,0 +1,54 @@
1
+ /**
2
+ * BACKLINKS Command
3
+ *
4
+ * Show what links to a file (incoming links).
5
+ */
6
+
7
+ import * as path from 'node:path'
8
+ import { Args, Command, Options } from '@effect/cli'
9
+ import { Console, Effect } from 'effect'
10
+ import { getIncomingLinks } from '../../index/indexer.js'
11
+ import { jsonOption, prettyOption } from '../options.js'
12
+ import { formatJson } from '../utils.js'
13
+
14
+ export const backlinksCommand = Command.make(
15
+ 'backlinks',
16
+ {
17
+ file: Args.file({ name: 'file' }).pipe(
18
+ Args.withDescription('Markdown file to find references to'),
19
+ ),
20
+ root: Options.directory('root').pipe(
21
+ Options.withAlias('r'),
22
+ Options.withDescription('Root directory for resolving relative links'),
23
+ Options.withDefault('.'),
24
+ ),
25
+ json: jsonOption,
26
+ pretty: prettyOption,
27
+ },
28
+ ({ file, root, json, pretty }) =>
29
+ Effect.gen(function* () {
30
+ const resolvedRoot = path.resolve(root)
31
+ const resolvedFile = path.resolve(file)
32
+ const relativePath = path.relative(resolvedRoot, resolvedFile)
33
+
34
+ const links = yield* getIncomingLinks(resolvedRoot, resolvedFile)
35
+
36
+ if (json) {
37
+ yield* Console.log(
38
+ formatJson({ file: relativePath, backlinks: links }, pretty),
39
+ )
40
+ } else {
41
+ yield* Console.log(`Incoming links to ${relativePath}:`)
42
+ yield* Console.log('')
43
+ if (links.length === 0) {
44
+ yield* Console.log(' (none)')
45
+ } else {
46
+ for (const link of links) {
47
+ yield* Console.log(` <- ${link}`)
48
+ }
49
+ }
50
+ yield* Console.log('')
51
+ yield* Console.log(`Total: ${links.length} backlinks`)
52
+ }
53
+ }),
54
+ ).pipe(Command.withDescription('What links to this?'))