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,105 @@
1
+ /**
2
+ * Unit tests for typo-suggester
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+ import type { CommandSchema } from './flag-schemas.js'
7
+ import {
8
+ formatValidFlags,
9
+ levenshteinDistance,
10
+ suggestFlag,
11
+ } from './typo-suggester.js'
12
+
13
+ describe('levenshteinDistance', () => {
14
+ it('returns 0 for identical strings', () => {
15
+ expect(levenshteinDistance('test', 'test')).toBe(0)
16
+ })
17
+
18
+ it('returns string length for empty comparison', () => {
19
+ expect(levenshteinDistance('test', '')).toBe(4)
20
+ expect(levenshteinDistance('', 'test')).toBe(4)
21
+ })
22
+
23
+ it('calculates single character difference', () => {
24
+ expect(levenshteinDistance('test', 'tset')).toBe(2) // transposition
25
+ expect(levenshteinDistance('test', 'tests')).toBe(1) // insertion
26
+ expect(levenshteinDistance('test', 'tes')).toBe(1) // deletion
27
+ expect(levenshteinDistance('test', 'tast')).toBe(1) // substitution
28
+ })
29
+
30
+ it('calculates distance for common typos', () => {
31
+ expect(levenshteinDistance('json', 'jsno')).toBe(2)
32
+ expect(levenshteinDistance('limit', 'limt')).toBe(1)
33
+ expect(levenshteinDistance('tokens', 'toekns')).toBe(2)
34
+ })
35
+ })
36
+
37
+ describe('suggestFlag', () => {
38
+ const mockSchema: CommandSchema = {
39
+ name: 'test',
40
+ flags: [
41
+ { name: 'json', type: 'boolean', description: 'Output JSON' },
42
+ { name: 'limit', type: 'string', alias: 'n', description: 'Max results' },
43
+ { name: 'threshold', type: 'string', description: 'Threshold' },
44
+ ],
45
+ }
46
+
47
+ it('suggests correct flag for typo', () => {
48
+ const result = suggestFlag('--jsno', mockSchema)
49
+ expect(result).toBeDefined()
50
+ expect(result?.flag).toBe('--json')
51
+ })
52
+
53
+ it('suggests correct flag for missing letter', () => {
54
+ const result = suggestFlag('--limt', mockSchema)
55
+ expect(result).toBeDefined()
56
+ expect(result?.flag).toBe('--limit')
57
+ })
58
+
59
+ it('returns undefined for no close match', () => {
60
+ const result = suggestFlag('--foobar', mockSchema)
61
+ expect(result).toBeUndefined()
62
+ })
63
+
64
+ it('handles prefix matches', () => {
65
+ const result = suggestFlag('--js', mockSchema)
66
+ expect(result).toBeDefined()
67
+ expect(result?.flag).toBe('--json')
68
+ })
69
+
70
+ it('handles short flag typos', () => {
71
+ const result = suggestFlag('-m', mockSchema)
72
+ expect(result).toBeDefined()
73
+ expect(result?.flag).toBe('--limit') // -m is close to -n
74
+ })
75
+
76
+ it('respects maxDistance parameter', () => {
77
+ const result = suggestFlag('--jsno', mockSchema, 1)
78
+ expect(result).toBeUndefined() // distance is 2, exceeds max
79
+ })
80
+ })
81
+
82
+ describe('formatValidFlags', () => {
83
+ const mockSchema: CommandSchema = {
84
+ name: 'test',
85
+ flags: [
86
+ { name: 'json', type: 'boolean', description: 'Output JSON' },
87
+ { name: 'limit', type: 'string', alias: 'n', description: 'Max results' },
88
+ { name: 'threshold', type: 'string' },
89
+ ],
90
+ }
91
+
92
+ it('formats flags with descriptions', () => {
93
+ const output = formatValidFlags(mockSchema)
94
+ expect(output).toContain('--json')
95
+ expect(output).toContain('Output JSON')
96
+ expect(output).toContain('--limit, -n')
97
+ expect(output).toContain('Max results')
98
+ expect(output).toContain('--threshold')
99
+ })
100
+
101
+ it('includes alias when present', () => {
102
+ const output = formatValidFlags(mockSchema)
103
+ expect(output).toContain(', -n')
104
+ })
105
+ })
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Typo Suggester
3
+ *
4
+ * Uses Levenshtein distance to suggest correct flags when users mistype.
5
+ */
6
+
7
+ import type { CommandSchema } from './flag-schemas.js'
8
+
9
+ /**
10
+ * Calculate Levenshtein distance between two strings
11
+ */
12
+ export const levenshteinDistance = (a: string, b: string): number => {
13
+ const matrix: number[][] = []
14
+
15
+ // Initialize first column
16
+ for (let i = 0; i <= a.length; i++) {
17
+ matrix[i] = [i]
18
+ }
19
+
20
+ // Initialize first row
21
+ for (let j = 0; j <= b.length; j++) {
22
+ matrix[0]![j] = j
23
+ }
24
+
25
+ // Fill in the rest
26
+ for (let i = 1; i <= a.length; i++) {
27
+ for (let j = 1; j <= b.length; j++) {
28
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1
29
+ matrix[i]![j] = Math.min(
30
+ matrix[i - 1]![j]! + 1, // deletion
31
+ matrix[i]![j - 1]! + 1, // insertion
32
+ matrix[i - 1]![j - 1]! + cost, // substitution
33
+ )
34
+ }
35
+ }
36
+
37
+ return matrix[a.length]![b.length]!
38
+ }
39
+
40
+ /**
41
+ * Suggestion result
42
+ */
43
+ export interface Suggestion {
44
+ flag: string
45
+ distance: number
46
+ description: string | undefined
47
+ }
48
+
49
+ /**
50
+ * Find the best flag suggestion for a typo
51
+ *
52
+ * @param typo - The mistyped flag (e.g., '--jsno')
53
+ * @param schema - The command schema to search
54
+ * @param maxDistance - Maximum Levenshtein distance to consider (default: 2)
55
+ * @returns Best matching flag or undefined
56
+ */
57
+ export const suggestFlag = (
58
+ typo: string,
59
+ schema: CommandSchema,
60
+ maxDistance: number = 2,
61
+ ): Suggestion | undefined => {
62
+ // Normalize the typo (remove leading dashes for comparison)
63
+ const normalizedTypo = typo.replace(/^-+/, '')
64
+
65
+ let bestMatch: Suggestion | undefined
66
+ let bestDistance = Infinity
67
+
68
+ for (const spec of schema.flags) {
69
+ // Check against full flag name
70
+ const flagName = spec.name
71
+ const distance = levenshteinDistance(normalizedTypo, flagName)
72
+
73
+ if (distance <= maxDistance && distance < bestDistance) {
74
+ bestDistance = distance
75
+ bestMatch = {
76
+ flag: `--${spec.name}`,
77
+ distance,
78
+ description: spec.description,
79
+ }
80
+ }
81
+
82
+ // Also check against alias if present
83
+ if (spec.alias) {
84
+ const aliasDistance = levenshteinDistance(normalizedTypo, spec.alias)
85
+ if (aliasDistance <= maxDistance && aliasDistance < bestDistance) {
86
+ bestDistance = aliasDistance
87
+ bestMatch = {
88
+ flag: `--${spec.name}`,
89
+ distance: aliasDistance,
90
+ description: spec.description,
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ // Prefer exact prefix matches (e.g., '--js' should suggest '--json')
97
+ if (!bestMatch || bestDistance > 0) {
98
+ for (const spec of schema.flags) {
99
+ if (spec.name.startsWith(normalizedTypo)) {
100
+ // Prefix match - this is likely what they meant
101
+ const prefixDistance = spec.name.length - normalizedTypo.length
102
+ if (prefixDistance <= maxDistance && prefixDistance < bestDistance) {
103
+ bestDistance = prefixDistance
104
+ bestMatch = {
105
+ flag: `--${spec.name}`,
106
+ distance: prefixDistance,
107
+ description: spec.description,
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ return bestMatch
115
+ }
116
+
117
+ /**
118
+ * Format a list of valid flags for a command
119
+ */
120
+ export const formatValidFlags = (schema: CommandSchema): string => {
121
+ const lines: string[] = []
122
+
123
+ for (const spec of schema.flags) {
124
+ const alias = spec.alias ? `, -${spec.alias}` : ''
125
+ const desc = spec.description ? ` ${spec.description}` : ''
126
+ lines.push(` --${spec.name}${alias}${desc}`)
127
+ }
128
+
129
+ return lines.join('\n')
130
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * CLI Utility Functions
3
+ *
4
+ * Shared helper functions used across CLI commands.
5
+ */
6
+
7
+ import * as fsPromises from 'node:fs/promises'
8
+ import * as path from 'node:path'
9
+ import { Effect } from 'effect'
10
+ import { listNamespaces } from '../embeddings/embedding-namespace.js'
11
+ import { DirectoryWalkError } from '../errors/index.js'
12
+
13
+ /**
14
+ * Format object as JSON string
15
+ */
16
+ export const formatJson = (obj: unknown, pretty: boolean): string => {
17
+ return pretty ? JSON.stringify(obj, null, 2) : JSON.stringify(obj)
18
+ }
19
+
20
+ /**
21
+ * Check if filename is a markdown file
22
+ */
23
+ export const isMarkdownFile = (filename: string): boolean => {
24
+ return filename.endsWith('.md') || filename.endsWith('.mdx')
25
+ }
26
+
27
+ /**
28
+ * Recursively walk directory and collect markdown files (async version).
29
+ * @deprecated Use walkDirEffect for typed error handling
30
+ */
31
+ export const walkDir = async (dir: string): Promise<string[]> => {
32
+ const files: string[] = []
33
+ const entries = await fsPromises.readdir(dir, { withFileTypes: true })
34
+
35
+ for (const entry of entries) {
36
+ const fullPath = path.join(dir, entry.name)
37
+
38
+ // Skip hidden directories and node_modules
39
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
40
+ continue
41
+ }
42
+
43
+ if (entry.isDirectory()) {
44
+ const subFiles = await walkDir(fullPath)
45
+ files.push(...subFiles)
46
+ } else if (entry.isFile() && isMarkdownFile(entry.name)) {
47
+ files.push(fullPath)
48
+ }
49
+ }
50
+
51
+ return files
52
+ }
53
+
54
+ /**
55
+ * Recursively walk directory and collect markdown files.
56
+ *
57
+ * @param dir - Directory to walk
58
+ * @returns List of markdown file paths
59
+ *
60
+ * @throws DirectoryWalkError - Cannot read or traverse directory
61
+ */
62
+ export const walkDirEffect = (
63
+ dir: string,
64
+ ): Effect.Effect<readonly string[], DirectoryWalkError> =>
65
+ Effect.gen(function* () {
66
+ const files: string[] = []
67
+
68
+ const entries = yield* Effect.tryPromise({
69
+ try: () => fsPromises.readdir(dir, { withFileTypes: true }),
70
+ catch: (e) =>
71
+ new DirectoryWalkError({
72
+ path: dir,
73
+ message: `Cannot read directory: ${e instanceof Error ? e.message : String(e)}`,
74
+ cause: e,
75
+ }),
76
+ })
77
+
78
+ for (const entry of entries) {
79
+ const fullPath = path.join(dir, entry.name)
80
+
81
+ // Skip hidden directories and node_modules
82
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
83
+ continue
84
+ }
85
+
86
+ if (entry.isDirectory()) {
87
+ const subFiles = yield* walkDirEffect(fullPath)
88
+ files.push(...subFiles)
89
+ } else if (entry.isFile() && isMarkdownFile(entry.name)) {
90
+ files.push(fullPath)
91
+ }
92
+ }
93
+
94
+ return files
95
+ })
96
+
97
+ /**
98
+ * Check if a query looks like a regex pattern
99
+ */
100
+ export const isRegexPattern = (query: string): boolean => {
101
+ // Has regex special characters (excluding simple spaces and common punctuation)
102
+ return /[.*+?^${}()|[\]\\]/.test(query)
103
+ }
104
+
105
+ /**
106
+ * Check if embeddings exist for a directory.
107
+ * Checks for namespaced embeddings in .mdcontext/embeddings/<namespace>/vectors.bin
108
+ */
109
+ export const hasEmbeddings = async (dir: string): Promise<boolean> => {
110
+ try {
111
+ const namespaces = await Effect.runPromise(
112
+ listNamespaces(dir).pipe(Effect.catchAll(() => Effect.succeed([]))),
113
+ )
114
+ return namespaces.length > 0
115
+ } catch {
116
+ return false
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Find the nearest parent directory containing an mdcontext index.
122
+ * Searches from the specified directory up to the filesystem root.
123
+ *
124
+ * @param startDir - Directory to start searching from
125
+ * @returns The directory containing the index, or null if not found
126
+ */
127
+ export const findIndexRoot = async (
128
+ startDir: string,
129
+ ): Promise<string | null> => {
130
+ let currentDir = path.resolve(startDir)
131
+ const root = path.parse(currentDir).root
132
+
133
+ while (currentDir !== root) {
134
+ const sectionsPath = path.join(
135
+ currentDir,
136
+ '.mdcontext',
137
+ 'indexes',
138
+ 'sections.json',
139
+ )
140
+ try {
141
+ await fsPromises.access(sectionsPath)
142
+ return currentDir // Found an index
143
+ } catch {
144
+ // No index here, try parent
145
+ const parent = path.dirname(currentDir)
146
+ if (parent === currentDir) break // Reached root
147
+ currentDir = parent
148
+ }
149
+ }
150
+
151
+ // Also check root
152
+ const rootSectionsPath = path.join(
153
+ root,
154
+ '.mdcontext',
155
+ 'indexes',
156
+ 'sections.json',
157
+ )
158
+ try {
159
+ await fsPromises.access(rootSectionsPath)
160
+ return root
161
+ } catch {
162
+ return null
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Get index information for display
168
+ */
169
+ export interface IndexInfo {
170
+ exists: boolean
171
+ lastUpdated?: string | undefined
172
+ sectionCount?: number | undefined
173
+ embeddingsExist: boolean
174
+ vectorCount?: number | undefined
175
+ /** The actual directory where the index was found (may differ from requested dir) */
176
+ indexRoot?: string | undefined
177
+ }
178
+
179
+ export const getIndexInfo = async (dir: string): Promise<IndexInfo> => {
180
+ // First try the specified directory
181
+ let indexRoot = dir
182
+ let sectionsPath = path.join(dir, '.mdcontext', 'indexes', 'sections.json')
183
+
184
+ let exists = false
185
+ let lastUpdated: string | undefined
186
+ let sectionCount: number | undefined
187
+ let embeddingsExist = false
188
+ let vectorCount: number | undefined
189
+
190
+ // Check sections index in specified directory
191
+ try {
192
+ const stat = await fsPromises.stat(sectionsPath)
193
+ exists = true
194
+ lastUpdated = stat.mtime.toISOString()
195
+
196
+ const content = await fsPromises.readFile(sectionsPath, 'utf-8')
197
+ const sections = JSON.parse(content)
198
+ sectionCount = Object.keys(sections.sections || {}).length
199
+ } catch {
200
+ // Index doesn't exist in specified directory, try to find in parent directories
201
+ const foundRoot = await findIndexRoot(dir)
202
+ if (foundRoot) {
203
+ indexRoot = foundRoot
204
+ sectionsPath = path.join(
205
+ foundRoot,
206
+ '.mdcontext',
207
+ 'indexes',
208
+ 'sections.json',
209
+ )
210
+
211
+ try {
212
+ const stat = await fsPromises.stat(sectionsPath)
213
+ exists = true
214
+ lastUpdated = stat.mtime.toISOString()
215
+
216
+ const content = await fsPromises.readFile(sectionsPath, 'utf-8')
217
+ const sections = JSON.parse(content)
218
+ sectionCount = Object.keys(sections.sections || {}).length
219
+ } catch {
220
+ // Still failed
221
+ }
222
+ }
223
+ }
224
+
225
+ // Check namespaced embeddings
226
+ try {
227
+ const namespaces = await Effect.runPromise(
228
+ listNamespaces(indexRoot).pipe(Effect.catchAll(() => Effect.succeed([]))),
229
+ )
230
+
231
+ if (namespaces.length > 0) {
232
+ embeddingsExist = true
233
+ // Find active namespace or use first one
234
+ const activeNs = namespaces.find((ns) => ns.isActive) ?? namespaces[0]
235
+ if (activeNs) {
236
+ vectorCount = activeNs.vectorCount
237
+ // Use namespace's updatedAt if more recent
238
+ if (activeNs.updatedAt) {
239
+ const nsDate = new Date(activeNs.updatedAt)
240
+ const currentDate = lastUpdated ? new Date(lastUpdated) : new Date(0)
241
+ if (nsDate > currentDate) {
242
+ lastUpdated = activeNs.updatedAt
243
+ }
244
+ }
245
+ }
246
+ }
247
+ } catch {
248
+ // Embeddings don't exist
249
+ }
250
+
251
+ return {
252
+ exists,
253
+ lastUpdated,
254
+ sectionCount,
255
+ embeddingsExist,
256
+ vectorCount,
257
+ indexRoot: exists && indexRoot !== dir ? indexRoot : undefined,
258
+ }
259
+ }