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,260 @@
1
+ /**
2
+ * Index storage operations
3
+ */
4
+
5
+ import * as crypto from 'node:crypto'
6
+ import * as fs from 'node:fs/promises'
7
+ import * as path from 'node:path'
8
+ import { Effect } from 'effect'
9
+
10
+ import {
11
+ DirectoryCreateError,
12
+ FileReadError,
13
+ FileWriteError,
14
+ IndexCorruptedError,
15
+ } from '../errors/index.js'
16
+ import type {
17
+ DocumentIndex,
18
+ IndexConfig,
19
+ LinkIndex,
20
+ SectionIndex,
21
+ } from './types.js'
22
+ import { getIndexPaths, INDEX_VERSION } from './types.js'
23
+
24
+ // ============================================================================
25
+ // File System Helpers
26
+ // ============================================================================
27
+
28
+ const ensureDir = (
29
+ dirPath: string,
30
+ ): Effect.Effect<void, DirectoryCreateError> =>
31
+ Effect.tryPromise({
32
+ try: () => fs.mkdir(dirPath, { recursive: true }),
33
+ catch: (e) =>
34
+ new DirectoryCreateError({
35
+ path: dirPath,
36
+ message: e instanceof Error ? e.message : String(e),
37
+ cause: e,
38
+ }),
39
+ }).pipe(Effect.map(() => undefined))
40
+
41
+ const readJsonFile = <T>(
42
+ filePath: string,
43
+ ): Effect.Effect<T | null, FileReadError | IndexCorruptedError> =>
44
+ Effect.gen(function* () {
45
+ // Try to read file content
46
+ const contentResult = yield* Effect.tryPromise({
47
+ try: () => fs.readFile(filePath, 'utf-8'),
48
+ catch: (e) => {
49
+ // File not found is not an error - return null
50
+ if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
51
+ return { notFound: true as const }
52
+ }
53
+ return new FileReadError({
54
+ path: filePath,
55
+ message: e instanceof Error ? e.message : String(e),
56
+ cause: e,
57
+ })
58
+ },
59
+ }).pipe(
60
+ Effect.map((content) =>
61
+ typeof content === 'string' ? { content } : content,
62
+ ),
63
+ // Note: catchAll here filters out "file not found" as expected case (returns null),
64
+ // while other errors are re-thrown to propagate as typed FileReadError
65
+ Effect.catchAll((e) =>
66
+ e && 'notFound' in e
67
+ ? Effect.succeed({ notFound: true as const })
68
+ : Effect.fail(e),
69
+ ),
70
+ )
71
+
72
+ // Handle not found
73
+ if ('notFound' in contentResult) {
74
+ return null
75
+ }
76
+
77
+ // Parse JSON - corrupted files should fail with IndexCorruptedError
78
+ return yield* Effect.try({
79
+ try: () => JSON.parse(contentResult.content) as T,
80
+ catch: (e) =>
81
+ new IndexCorruptedError({
82
+ path: filePath,
83
+ reason: 'InvalidJson',
84
+ details: e instanceof Error ? e.message : String(e),
85
+ }),
86
+ })
87
+ })
88
+
89
+ const writeJsonFile = <T>(
90
+ filePath: string,
91
+ data: T,
92
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
93
+ Effect.gen(function* () {
94
+ const dir = path.dirname(filePath)
95
+ yield* ensureDir(dir)
96
+ yield* Effect.tryPromise({
97
+ try: () => fs.writeFile(filePath, JSON.stringify(data, null, 2)),
98
+ catch: (e) =>
99
+ new FileWriteError({
100
+ path: filePath,
101
+ message: e instanceof Error ? e.message : String(e),
102
+ cause: e,
103
+ }),
104
+ })
105
+ })
106
+
107
+ // ============================================================================
108
+ // Hash Computation
109
+ // ============================================================================
110
+
111
+ export const computeHash = (content: string): string => {
112
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16)
113
+ }
114
+
115
+ // ============================================================================
116
+ // Index Storage Operations
117
+ // ============================================================================
118
+
119
+ export interface IndexStorage {
120
+ readonly rootPath: string
121
+ readonly paths: ReturnType<typeof getIndexPaths>
122
+ }
123
+
124
+ export const createStorage = (rootPath: string): IndexStorage => ({
125
+ rootPath: path.resolve(rootPath),
126
+ paths: getIndexPaths(path.resolve(rootPath)),
127
+ })
128
+
129
+ export const initializeIndex = (
130
+ storage: IndexStorage,
131
+ ): Effect.Effect<
132
+ void,
133
+ DirectoryCreateError | FileReadError | FileWriteError | IndexCorruptedError
134
+ > =>
135
+ Effect.gen(function* () {
136
+ yield* ensureDir(storage.paths.root)
137
+ yield* ensureDir(storage.paths.parsed)
138
+ yield* ensureDir(path.dirname(storage.paths.documents))
139
+
140
+ // Create default config if it doesn't exist
141
+ const existingConfig = yield* loadConfig(storage)
142
+ if (!existingConfig) {
143
+ const config: IndexConfig = {
144
+ version: INDEX_VERSION,
145
+ rootPath: storage.rootPath,
146
+ include: ['**/*.md', '**/*.mdx'],
147
+ exclude: ['**/node_modules/**', '**/.*/**'],
148
+ createdAt: new Date().toISOString(),
149
+ updatedAt: new Date().toISOString(),
150
+ }
151
+ yield* saveConfig(storage, config)
152
+ }
153
+ })
154
+
155
+ // ============================================================================
156
+ // Config Operations
157
+ // ============================================================================
158
+
159
+ export const loadConfig = (
160
+ storage: IndexStorage,
161
+ ): Effect.Effect<IndexConfig | null, FileReadError | IndexCorruptedError> =>
162
+ readJsonFile<IndexConfig>(storage.paths.config)
163
+
164
+ export const saveConfig = (
165
+ storage: IndexStorage,
166
+ config: IndexConfig,
167
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
168
+ writeJsonFile(storage.paths.config, {
169
+ ...config,
170
+ updatedAt: new Date().toISOString(),
171
+ })
172
+
173
+ // ============================================================================
174
+ // Document Index Operations
175
+ // ============================================================================
176
+
177
+ export const loadDocumentIndex = (
178
+ storage: IndexStorage,
179
+ ): Effect.Effect<DocumentIndex | null, FileReadError | IndexCorruptedError> =>
180
+ readJsonFile<DocumentIndex>(storage.paths.documents)
181
+
182
+ export const saveDocumentIndex = (
183
+ storage: IndexStorage,
184
+ index: DocumentIndex,
185
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
186
+ writeJsonFile(storage.paths.documents, index)
187
+
188
+ export const createEmptyDocumentIndex = (rootPath: string): DocumentIndex => ({
189
+ version: INDEX_VERSION,
190
+ rootPath,
191
+ documents: {},
192
+ })
193
+
194
+ // ============================================================================
195
+ // Section Index Operations
196
+ // ============================================================================
197
+
198
+ export const loadSectionIndex = (
199
+ storage: IndexStorage,
200
+ ): Effect.Effect<SectionIndex | null, FileReadError | IndexCorruptedError> =>
201
+ readJsonFile<SectionIndex>(storage.paths.sections)
202
+
203
+ export const saveSectionIndex = (
204
+ storage: IndexStorage,
205
+ index: SectionIndex,
206
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
207
+ writeJsonFile(storage.paths.sections, index)
208
+
209
+ export const createEmptySectionIndex = (): SectionIndex => ({
210
+ version: INDEX_VERSION,
211
+ sections: {},
212
+ byHeading: Object.create(null),
213
+ byDocument: Object.create(null),
214
+ })
215
+
216
+ // ============================================================================
217
+ // Link Index Operations
218
+ // ============================================================================
219
+
220
+ export const loadLinkIndex = (
221
+ storage: IndexStorage,
222
+ ): Effect.Effect<LinkIndex | null, FileReadError | IndexCorruptedError> =>
223
+ readJsonFile<LinkIndex>(storage.paths.links)
224
+
225
+ export const saveLinkIndex = (
226
+ storage: IndexStorage,
227
+ index: LinkIndex,
228
+ ): Effect.Effect<void, DirectoryCreateError | FileWriteError> =>
229
+ writeJsonFile(storage.paths.links, index)
230
+
231
+ export const createEmptyLinkIndex = (): LinkIndex => ({
232
+ version: INDEX_VERSION,
233
+ forward: Object.create(null),
234
+ backward: Object.create(null),
235
+ broken: [],
236
+ })
237
+
238
+ // ============================================================================
239
+ // Index Existence Check
240
+ // ============================================================================
241
+
242
+ export const indexExists = (
243
+ storage: IndexStorage,
244
+ ): Effect.Effect<boolean, FileReadError> =>
245
+ Effect.tryPromise({
246
+ try: async () => {
247
+ try {
248
+ await fs.access(storage.paths.config)
249
+ return true
250
+ } catch {
251
+ return false
252
+ }
253
+ },
254
+ catch: (e) =>
255
+ new FileReadError({
256
+ path: storage.paths.config,
257
+ message: e instanceof Error ? e.message : String(e),
258
+ cause: e,
259
+ }),
260
+ })
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Index data types for mdcontext
3
+ */
4
+
5
+ // ============================================================================
6
+ // Configuration
7
+ // ============================================================================
8
+
9
+ export interface IndexConfig {
10
+ readonly version: number
11
+ readonly rootPath: string
12
+ readonly include: readonly string[]
13
+ readonly exclude: readonly string[]
14
+ readonly createdAt: string
15
+ readonly updatedAt: string
16
+ }
17
+
18
+ // ============================================================================
19
+ // Document Index
20
+ // ============================================================================
21
+
22
+ export interface DocumentIndex {
23
+ readonly version: number
24
+ readonly rootPath: string
25
+ readonly documents: Record<string, DocumentEntry>
26
+ }
27
+
28
+ export interface DocumentEntry {
29
+ readonly id: string
30
+ readonly path: string
31
+ readonly title: string
32
+ readonly mtime: number
33
+ readonly hash: string
34
+ readonly tokenCount: number
35
+ readonly sectionCount: number
36
+ }
37
+
38
+ // ============================================================================
39
+ // Section Index
40
+ // ============================================================================
41
+
42
+ export interface SectionIndex {
43
+ readonly version: number
44
+ readonly sections: Record<string, SectionEntry>
45
+ readonly byHeading: Record<string, readonly string[]>
46
+ readonly byDocument: Record<string, readonly string[]>
47
+ }
48
+
49
+ export interface SectionEntry {
50
+ readonly id: string
51
+ readonly documentId: string
52
+ readonly documentPath: string
53
+ readonly heading: string
54
+ readonly level: number
55
+ readonly startLine: number
56
+ readonly endLine: number
57
+ readonly tokenCount: number
58
+ readonly hasCode: boolean
59
+ readonly hasList: boolean
60
+ readonly hasTable: boolean
61
+ }
62
+
63
+ // ============================================================================
64
+ // Link Index
65
+ // ============================================================================
66
+
67
+ export interface LinkIndex {
68
+ readonly version: number
69
+ readonly forward: Record<string, readonly string[]>
70
+ readonly backward: Record<string, readonly string[]>
71
+ readonly broken: readonly string[]
72
+ }
73
+
74
+ // ============================================================================
75
+ // Index Result
76
+ // ============================================================================
77
+
78
+ /**
79
+ * Reason why a file was skipped during indexing
80
+ */
81
+ export type SkipReason =
82
+ | 'unchanged' // File hash and mtime unchanged
83
+ | 'excluded' // Matches exclude pattern
84
+ | 'hidden' // Hidden file or directory
85
+ | 'not-markdown' // Not a markdown file
86
+ | 'binary' // Binary file detected
87
+ | 'oversized' // File too large
88
+
89
+ /**
90
+ * Information about a skipped file
91
+ */
92
+ export interface SkippedFile {
93
+ readonly path: string
94
+ readonly reason: SkipReason
95
+ }
96
+
97
+ /**
98
+ * Summary of skipped files by reason
99
+ */
100
+ export interface SkipSummary {
101
+ readonly unchanged: number
102
+ readonly excluded: number
103
+ readonly hidden: number
104
+ readonly total: number
105
+ }
106
+
107
+ export interface IndexResult {
108
+ readonly documentsIndexed: number
109
+ readonly sectionsIndexed: number
110
+ readonly linksIndexed: number
111
+ readonly totalDocuments: number
112
+ readonly totalSections: number
113
+ readonly totalLinks: number
114
+ readonly duration: number
115
+ /** Non-fatal file processing errors (files that couldn't be indexed) */
116
+ readonly errors: readonly FileProcessingError[]
117
+ readonly skipped: SkipSummary
118
+ }
119
+
120
+ /**
121
+ * Non-fatal error during file processing in index build.
122
+ * These are collected and reported but don't stop the build.
123
+ *
124
+ * Note: This is distinct from IndexBuildError in errors/index.ts,
125
+ * which is a TaggedError for fatal build failures.
126
+ */
127
+ export interface FileProcessingError {
128
+ readonly path: string
129
+ readonly message: string
130
+ }
131
+
132
+ // ============================================================================
133
+ // Index Paths
134
+ // ============================================================================
135
+
136
+ export const INDEX_DIR = '.mdcontext'
137
+ export const INDEX_VERSION = 1
138
+
139
+ export const getIndexPaths = (rootPath: string) => ({
140
+ root: `${rootPath}/${INDEX_DIR}`,
141
+ config: `${rootPath}/${INDEX_DIR}/config.json`,
142
+ documents: `${rootPath}/${INDEX_DIR}/indexes/documents.json`,
143
+ sections: `${rootPath}/${INDEX_DIR}/indexes/sections.json`,
144
+ links: `${rootPath}/${INDEX_DIR}/indexes/links.json`,
145
+ cache: `${rootPath}/${INDEX_DIR}/cache`,
146
+ parsed: `${rootPath}/${INDEX_DIR}/cache/parsed`,
147
+ })
@@ -0,0 +1,189 @@
1
+ /**
2
+ * File watcher for automatic re-indexing
3
+ *
4
+ * ## Why Not Effect Streams?
5
+ *
6
+ * We evaluated using Effect Streams (ALP-101) but decided the current approach is better:
7
+ *
8
+ * 1. **chokidar is battle-tested** - Handles OS-specific quirks (FSEvents on macOS,
9
+ * inotify on Linux, ReadDirectoryChangesW on Windows)
10
+ *
11
+ * 2. **Debouncing handles backpressure** - The 300ms debounce already batches rapid
12
+ * changes, so Stream backpressure isn't needed
13
+ *
14
+ * 3. **Simple use case** - File change → rebuild index. No complex transformations
15
+ * or compositions that would benefit from Stream operators
16
+ *
17
+ * 4. **Already Effect-based** - The setup/teardown is wrapped in Effect for proper
18
+ * error handling, and we use typed errors (WatchError, IndexBuildError)
19
+ *
20
+ * If future requirements need more sophisticated event processing (filtering by
21
+ * content type, incremental updates, event replay), reconsider Streams then.
22
+ */
23
+
24
+ import * as path from 'node:path'
25
+ import { watch } from 'chokidar'
26
+ import { Effect } from 'effect'
27
+
28
+ import {
29
+ type DirectoryCreateError,
30
+ type DirectoryWalkError,
31
+ type FileReadError,
32
+ type FileWriteError,
33
+ type IndexCorruptedError,
34
+ WatchError,
35
+ } from '../errors/index.js'
36
+ import { getChokidarIgnorePatterns } from './ignore-patterns.js'
37
+ import { buildIndex, type IndexOptions } from './indexer.js'
38
+ import { createStorage, indexExists } from './storage.js'
39
+
40
+ /**
41
+ * Union of errors that can occur during watch operations
42
+ */
43
+ export type WatchDirectoryError =
44
+ | WatchError
45
+ | DirectoryWalkError
46
+ | DirectoryCreateError
47
+ | FileReadError
48
+ | FileWriteError
49
+ | IndexCorruptedError
50
+
51
+ // ============================================================================
52
+ // Watcher Types
53
+ // ============================================================================
54
+
55
+ export interface WatcherOptions extends IndexOptions {
56
+ readonly debounceMs?: number
57
+ readonly onIndex?: (result: {
58
+ documentsIndexed: number
59
+ duration: number
60
+ }) => void
61
+ readonly onError?: (error: WatchError) => void
62
+ /** Whether to honor .gitignore for file watching (default: true) */
63
+ readonly honorGitignore?: boolean
64
+ /** Whether to honor .mdcontextignore for file watching (default: true) */
65
+ readonly honorMdcontextignore?: boolean
66
+ }
67
+
68
+ export interface Watcher {
69
+ readonly stop: () => void
70
+ }
71
+
72
+ // ============================================================================
73
+ // Watcher Implementation
74
+ // ============================================================================
75
+
76
+ const isMarkdownFile = (filePath: string): boolean =>
77
+ filePath.endsWith('.md') || filePath.endsWith('.mdx')
78
+
79
+ export const watchDirectory = (
80
+ rootPath: string,
81
+ options: WatcherOptions = {},
82
+ ): Effect.Effect<Watcher, WatchDirectoryError> =>
83
+ Effect.gen(function* () {
84
+ const resolvedRoot = path.resolve(rootPath)
85
+ const storage = createStorage(resolvedRoot)
86
+ const debounceMs = options.debounceMs ?? 300
87
+
88
+ // Ensure index exists
89
+ const exists = yield* indexExists(storage)
90
+ if (!exists) {
91
+ // Build initial index
92
+ const result = yield* buildIndex(resolvedRoot, options)
93
+ options.onIndex?.({
94
+ documentsIndexed: result.documentsIndexed,
95
+ duration: result.duration,
96
+ })
97
+ }
98
+
99
+ // Create a debounce queue
100
+ const pendingPaths = new Set<string>()
101
+ let debounceTimer: NodeJS.Timeout | null = null
102
+
103
+ const scheduleReindex = () => {
104
+ if (debounceTimer) {
105
+ clearTimeout(debounceTimer)
106
+ }
107
+
108
+ debounceTimer = setTimeout(async () => {
109
+ if (pendingPaths.size === 0) return
110
+
111
+ pendingPaths.clear()
112
+
113
+ try {
114
+ const result = await Effect.runPromise(
115
+ buildIndex(resolvedRoot, options),
116
+ )
117
+ options.onIndex?.({
118
+ documentsIndexed: result.documentsIndexed,
119
+ duration: result.duration,
120
+ })
121
+ } catch (error) {
122
+ options.onError?.(
123
+ new WatchError({
124
+ path: resolvedRoot,
125
+ message:
126
+ error instanceof Error ? error.message : 'Index rebuild failed',
127
+ cause: error,
128
+ }),
129
+ )
130
+ }
131
+ }, debounceMs)
132
+ }
133
+
134
+ // Build ignore patterns for chokidar
135
+ const ignorePatterns = yield* getChokidarIgnorePatterns({
136
+ rootPath: resolvedRoot,
137
+ cliPatterns: options.exclude,
138
+ honorGitignore: options.honorGitignore ?? true,
139
+ honorMdcontextignore: options.honorMdcontextignore ?? true,
140
+ })
141
+
142
+ // Set up chokidar watcher with dynamic ignore patterns
143
+ const watcher = watch(resolvedRoot, {
144
+ ignored: ignorePatterns,
145
+ persistent: true,
146
+ ignoreInitial: true,
147
+ })
148
+
149
+ watcher.on('add', (filePath) => {
150
+ if (isMarkdownFile(filePath)) {
151
+ pendingPaths.add(filePath)
152
+ scheduleReindex()
153
+ }
154
+ })
155
+
156
+ watcher.on('change', (filePath) => {
157
+ if (isMarkdownFile(filePath)) {
158
+ pendingPaths.add(filePath)
159
+ scheduleReindex()
160
+ }
161
+ })
162
+
163
+ watcher.on('unlink', (filePath) => {
164
+ if (isMarkdownFile(filePath)) {
165
+ pendingPaths.add(filePath)
166
+ scheduleReindex()
167
+ }
168
+ })
169
+
170
+ watcher.on('error', (error: unknown) => {
171
+ options.onError?.(
172
+ new WatchError({
173
+ path: resolvedRoot,
174
+ message:
175
+ error instanceof Error ? error.message : 'File watcher error',
176
+ cause: error,
177
+ }),
178
+ )
179
+ })
180
+
181
+ return {
182
+ stop: () => {
183
+ if (debounceTimer) {
184
+ clearTimeout(debounceTimer)
185
+ }
186
+ watcher.close()
187
+ },
188
+ }
189
+ })
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * mdcontext - Token-efficient markdown analysis for LLMs
3
+ */
4
+
5
+ // Config utilities for user config files
6
+ export type { PartialMdContextConfig } from './config/service.js'
7
+ export * from './core/index.js'
8
+ export * from './index/index.js'
9
+ export * from './parser/index.js'
10
+ export * from './utils/index.js'
11
+
12
+ /**
13
+ * Type-safe configuration helper for mdcontext.config.ts files.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { defineConfig } from 'mdcontext'
18
+ *
19
+ * export default defineConfig({
20
+ * index: {
21
+ * maxDepth: 5,
22
+ * },
23
+ * })
24
+ * ```
25
+ */
26
+ export const defineConfig = <
27
+ T extends import('./config/service.js').PartialMdContextConfig,
28
+ >(
29
+ config: T,
30
+ ): T => config