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,396 @@
1
+ /**
2
+ * Tests for Provider Factory Module
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
6
+ import {
7
+ createSummarizer,
8
+ getBestAvailableSummarizer,
9
+ } from './provider-factory.js'
10
+ import type { AISummarizationConfig } from './types.js'
11
+ import { SummarizationError } from './types.js'
12
+
13
+ vi.mock('./cli-providers/detection.js', () => ({
14
+ isCLIInstalled: vi.fn(),
15
+ getCLIInfo: vi.fn((name: string) => ({
16
+ name,
17
+ displayName: name.charAt(0).toUpperCase() + name.slice(1),
18
+ command: name,
19
+ args: [],
20
+ useStdin: false,
21
+ })),
22
+ }))
23
+
24
+ vi.mock('./cli-providers/claude.js', () => {
25
+ return {
26
+ ClaudeCLISummarizer: class MockClaudeCLISummarizer {
27
+ summarize = vi.fn()
28
+ isAvailable = vi.fn().mockResolvedValue(true)
29
+ },
30
+ }
31
+ })
32
+
33
+ import { isCLIInstalled } from './cli-providers/detection.js'
34
+
35
+ const mockIsCLIInstalled = vi.mocked(isCLIInstalled)
36
+
37
+ describe('createSummarizer', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks()
40
+ })
41
+
42
+ describe('CLI mode', () => {
43
+ it('should return ClaudeCLISummarizer for CLI mode with claude provider', async () => {
44
+ mockIsCLIInstalled.mockResolvedValue(true)
45
+
46
+ const config: AISummarizationConfig = {
47
+ mode: 'cli',
48
+ provider: 'claude',
49
+ }
50
+
51
+ const summarizer = await createSummarizer(config)
52
+
53
+ expect(mockIsCLIInstalled).toHaveBeenCalledWith('claude')
54
+ expect(summarizer).toBeDefined()
55
+ expect(summarizer.summarize).toBeDefined()
56
+ expect(summarizer.isAvailable).toBeDefined()
57
+ })
58
+
59
+ it('should throw PROVIDER_NOT_FOUND for unimplemented CLI providers', async () => {
60
+ mockIsCLIInstalled.mockResolvedValue(true)
61
+
62
+ const unimplementedProviders = [
63
+ 'opencode',
64
+ 'copilot',
65
+ 'aider',
66
+ 'cline',
67
+ 'amp',
68
+ ] as const
69
+
70
+ for (const provider of unimplementedProviders) {
71
+ const config: AISummarizationConfig = {
72
+ mode: 'cli',
73
+ provider,
74
+ }
75
+
76
+ await expect(createSummarizer(config)).rejects.toThrow(
77
+ SummarizationError,
78
+ )
79
+
80
+ try {
81
+ await createSummarizer(config)
82
+ } catch (error) {
83
+ expect(error).toBeInstanceOf(SummarizationError)
84
+ expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
85
+ expect((error as SummarizationError).message).toContain(
86
+ 'not yet implemented',
87
+ )
88
+ expect((error as SummarizationError).provider).toBe(provider)
89
+ }
90
+ }
91
+ })
92
+
93
+ it('should throw PROVIDER_NOT_AVAILABLE if CLI not installed', async () => {
94
+ mockIsCLIInstalled.mockResolvedValue(false)
95
+
96
+ const config: AISummarizationConfig = {
97
+ mode: 'cli',
98
+ provider: 'claude',
99
+ }
100
+
101
+ await expect(createSummarizer(config)).rejects.toThrow(SummarizationError)
102
+
103
+ try {
104
+ await createSummarizer(config)
105
+ } catch (error) {
106
+ expect(error).toBeInstanceOf(SummarizationError)
107
+ expect((error as SummarizationError).code).toBe(
108
+ 'PROVIDER_NOT_AVAILABLE',
109
+ )
110
+ expect((error as SummarizationError).message).toContain('not installed')
111
+ }
112
+ })
113
+
114
+ it('should throw PROVIDER_NOT_FOUND for invalid CLI provider', async () => {
115
+ const config = {
116
+ mode: 'cli',
117
+ provider: 'invalid-provider',
118
+ } as unknown as AISummarizationConfig
119
+
120
+ await expect(createSummarizer(config)).rejects.toThrow(SummarizationError)
121
+
122
+ try {
123
+ await createSummarizer(config)
124
+ } catch (error) {
125
+ expect(error).toBeInstanceOf(SummarizationError)
126
+ expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
127
+ expect((error as SummarizationError).message).toContain(
128
+ 'Invalid CLI provider',
129
+ )
130
+ }
131
+ })
132
+ })
133
+
134
+ describe('API mode', () => {
135
+ it('should throw PROVIDER_NOT_FOUND for API providers (not yet implemented)', async () => {
136
+ const apiProviders = [
137
+ 'deepseek',
138
+ 'anthropic',
139
+ 'openai',
140
+ 'gemini',
141
+ 'qwen',
142
+ ] as const
143
+
144
+ for (const provider of apiProviders) {
145
+ const config: AISummarizationConfig = {
146
+ mode: 'api',
147
+ provider,
148
+ }
149
+
150
+ await expect(createSummarizer(config)).rejects.toThrow(
151
+ SummarizationError,
152
+ )
153
+
154
+ try {
155
+ await createSummarizer(config)
156
+ } catch (error) {
157
+ expect(error).toBeInstanceOf(SummarizationError)
158
+ expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
159
+ expect((error as SummarizationError).message).toContain(
160
+ 'not yet implemented',
161
+ )
162
+ }
163
+ }
164
+ })
165
+
166
+ it('should throw PROVIDER_NOT_FOUND for invalid API provider', async () => {
167
+ const config = {
168
+ mode: 'api',
169
+ provider: 'not-a-real-api',
170
+ } as unknown as AISummarizationConfig
171
+
172
+ await expect(createSummarizer(config)).rejects.toThrow(SummarizationError)
173
+
174
+ try {
175
+ await createSummarizer(config)
176
+ } catch (error) {
177
+ expect(error).toBeInstanceOf(SummarizationError)
178
+ expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
179
+ expect((error as SummarizationError).message).toContain(
180
+ 'Invalid API provider',
181
+ )
182
+ }
183
+ })
184
+ })
185
+
186
+ describe('unknown mode', () => {
187
+ it('should throw PROVIDER_NOT_FOUND for unknown mode', async () => {
188
+ const config = {
189
+ mode: 'unknown',
190
+ provider: 'claude',
191
+ } as unknown as AISummarizationConfig
192
+
193
+ await expect(createSummarizer(config)).rejects.toThrow(SummarizationError)
194
+
195
+ try {
196
+ await createSummarizer(config)
197
+ } catch (error) {
198
+ expect(error).toBeInstanceOf(SummarizationError)
199
+ expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
200
+ expect((error as SummarizationError).message).toContain(
201
+ 'Unknown summarization mode',
202
+ )
203
+ }
204
+ })
205
+ })
206
+
207
+ describe('type guards', () => {
208
+ it('isCLIProvider should accept valid CLI providers', async () => {
209
+ mockIsCLIInstalled.mockResolvedValue(true)
210
+
211
+ const validProviders = [
212
+ 'claude',
213
+ 'copilot',
214
+ 'cline',
215
+ 'aider',
216
+ 'opencode',
217
+ 'amp',
218
+ ]
219
+
220
+ for (const provider of validProviders) {
221
+ const config: AISummarizationConfig = {
222
+ mode: 'cli',
223
+ provider: provider as AISummarizationConfig['provider'],
224
+ }
225
+
226
+ // Valid providers should not throw "Invalid CLI provider"
227
+ try {
228
+ await createSummarizer(config)
229
+ } catch (error) {
230
+ // May throw for other reasons (not installed, not implemented)
231
+ // but should NOT throw "Invalid CLI provider"
232
+ expect((error as SummarizationError).message).not.toContain(
233
+ 'Invalid CLI provider',
234
+ )
235
+ }
236
+ }
237
+ })
238
+
239
+ it('isAPIProvider should accept valid API providers', async () => {
240
+ const validProviders = [
241
+ 'deepseek',
242
+ 'anthropic',
243
+ 'openai',
244
+ 'gemini',
245
+ 'qwen',
246
+ ]
247
+
248
+ for (const provider of validProviders) {
249
+ const config: AISummarizationConfig = {
250
+ mode: 'api',
251
+ provider: provider as AISummarizationConfig['provider'],
252
+ }
253
+
254
+ try {
255
+ await createSummarizer(config)
256
+ } catch (error) {
257
+ // May throw "not yet implemented" but should NOT throw "Invalid API provider"
258
+ expect((error as SummarizationError).message).not.toContain(
259
+ 'Invalid API provider',
260
+ )
261
+ }
262
+ }
263
+ })
264
+
265
+ it('isCLIProvider should reject invalid providers', async () => {
266
+ const config = {
267
+ mode: 'cli',
268
+ provider: 'not-valid',
269
+ } as unknown as AISummarizationConfig
270
+
271
+ try {
272
+ await createSummarizer(config)
273
+ } catch (error) {
274
+ expect((error as SummarizationError).message).toContain(
275
+ 'Invalid CLI provider',
276
+ )
277
+ }
278
+ })
279
+
280
+ it('isAPIProvider should reject invalid providers', async () => {
281
+ const config = {
282
+ mode: 'api',
283
+ provider: 'not-valid',
284
+ } as unknown as AISummarizationConfig
285
+
286
+ try {
287
+ await createSummarizer(config)
288
+ } catch (error) {
289
+ expect((error as SummarizationError).message).toContain(
290
+ 'Invalid API provider',
291
+ )
292
+ }
293
+ })
294
+ })
295
+ })
296
+
297
+ describe('getBestAvailableSummarizer', () => {
298
+ beforeEach(() => {
299
+ vi.clearAllMocks()
300
+ })
301
+
302
+ it('should return null when no providers are available', async () => {
303
+ mockIsCLIInstalled.mockResolvedValue(false)
304
+
305
+ const result = await getBestAvailableSummarizer()
306
+
307
+ expect(result).toBeNull()
308
+ })
309
+
310
+ it('should return ClaudeCLISummarizer when claude CLI is available', async () => {
311
+ mockIsCLIInstalled.mockResolvedValue(true)
312
+
313
+ const result = await getBestAvailableSummarizer()
314
+
315
+ expect(result).not.toBeNull()
316
+ expect(result?.config.mode).toBe('cli')
317
+ expect(result?.config.provider).toBe('claude')
318
+ expect(result?.summarizer).toBeDefined()
319
+ expect(result?.summarizer.summarize).toBeDefined()
320
+ })
321
+
322
+ it('should respect preferredConfig when provider is available', async () => {
323
+ mockIsCLIInstalled.mockResolvedValue(true)
324
+
325
+ const preferredConfig: AISummarizationConfig = {
326
+ mode: 'cli',
327
+ provider: 'claude',
328
+ }
329
+
330
+ const result = await getBestAvailableSummarizer(preferredConfig)
331
+
332
+ expect(result).not.toBeNull()
333
+ expect(result?.config.mode).toBe('cli')
334
+ expect(result?.config.provider).toBe('claude')
335
+ })
336
+
337
+ it('should fall back to auto-detection when preferredConfig fails', async () => {
338
+ // First call for preferred config fails, second call for auto-detection succeeds
339
+ mockIsCLIInstalled
340
+ .mockResolvedValueOnce(false) // preferred config check fails
341
+ .mockResolvedValueOnce(true) // auto-detection finds claude
342
+
343
+ const preferredConfig: AISummarizationConfig = {
344
+ mode: 'cli',
345
+ provider: 'claude',
346
+ }
347
+
348
+ const result = await getBestAvailableSummarizer(preferredConfig)
349
+
350
+ expect(result).not.toBeNull()
351
+ expect(result?.config.provider).toBe('claude')
352
+ })
353
+
354
+ it('should return null when preferredConfig fails and no auto-detection succeeds', async () => {
355
+ mockIsCLIInstalled.mockResolvedValue(false)
356
+
357
+ const preferredConfig: AISummarizationConfig = {
358
+ mode: 'cli',
359
+ provider: 'claude',
360
+ }
361
+
362
+ const result = await getBestAvailableSummarizer(preferredConfig)
363
+
364
+ expect(result).toBeNull()
365
+ })
366
+
367
+ it('should handle partial preferredConfig without mode', async () => {
368
+ mockIsCLIInstalled.mockResolvedValue(true)
369
+
370
+ const partialConfig = {
371
+ provider: 'claude',
372
+ } as Partial<AISummarizationConfig>
373
+
374
+ const result = await getBestAvailableSummarizer(partialConfig)
375
+
376
+ // Should fall through to auto-detection since mode is missing
377
+ expect(result).not.toBeNull()
378
+ expect(result?.config.mode).toBe('cli')
379
+ expect(result?.config.provider).toBe('claude')
380
+ })
381
+
382
+ it('should handle partial preferredConfig without provider', async () => {
383
+ mockIsCLIInstalled.mockResolvedValue(true)
384
+
385
+ const partialConfig = {
386
+ mode: 'cli',
387
+ } as Partial<AISummarizationConfig>
388
+
389
+ const result = await getBestAvailableSummarizer(partialConfig)
390
+
391
+ // Should fall through to auto-detection since provider is missing
392
+ expect(result).not.toBeNull()
393
+ expect(result?.config.mode).toBe('cli')
394
+ expect(result?.config.provider).toBe('claude')
395
+ })
396
+ })
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Summarization Provider Factory
3
+ *
4
+ * Creates summarizer instances based on configuration.
5
+ * CLI providers are checked first (free), then API providers (paid).
6
+ */
7
+
8
+ import { ClaudeCLISummarizer } from './cli-providers/claude.js'
9
+ import { getCLIInfo, isCLIInstalled } from './cli-providers/detection.js'
10
+ import type {
11
+ AISummarizationConfig,
12
+ APIProviderName,
13
+ CLIProviderName,
14
+ Summarizer,
15
+ } from './types.js'
16
+ import { SummarizationError } from './types.js'
17
+
18
+ /**
19
+ * Create a CLI-based summarizer.
20
+ */
21
+ const createCLISummarizer = async (
22
+ provider: CLIProviderName,
23
+ ): Promise<Summarizer> => {
24
+ // Check if CLI is installed
25
+ const isInstalled = await isCLIInstalled(provider)
26
+ if (!isInstalled) {
27
+ const cliInfo = getCLIInfo(provider)
28
+ throw new SummarizationError(
29
+ `CLI tool '${cliInfo?.displayName ?? provider}' is not installed`,
30
+ 'PROVIDER_NOT_AVAILABLE',
31
+ provider,
32
+ )
33
+ }
34
+
35
+ // Return appropriate summarizer based on provider
36
+ switch (provider) {
37
+ case 'claude':
38
+ return new ClaudeCLISummarizer()
39
+
40
+ // TODO: Add other CLI providers as needed
41
+ case 'opencode':
42
+ case 'copilot':
43
+ case 'aider':
44
+ case 'cline':
45
+ case 'amp':
46
+ throw new SummarizationError(
47
+ `CLI provider '${provider}' is not yet implemented`,
48
+ 'PROVIDER_NOT_FOUND',
49
+ provider,
50
+ )
51
+
52
+ default:
53
+ throw new SummarizationError(
54
+ `Unknown CLI provider: ${provider}`,
55
+ 'PROVIDER_NOT_FOUND',
56
+ provider,
57
+ )
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Create an API-based summarizer.
63
+ *
64
+ * Uses Vercel AI SDK for provider abstraction.
65
+ * Requires appropriate API keys to be configured.
66
+ */
67
+ const createAPISummarizer = async (
68
+ _provider: APIProviderName,
69
+ _config: AISummarizationConfig,
70
+ ): Promise<Summarizer> => {
71
+ // TODO: Implement API providers using Vercel AI SDK
72
+ // This will be implemented in a later issue (ALP-220)
73
+ throw new SummarizationError(
74
+ 'API providers are not yet implemented. Use CLI providers for now.',
75
+ 'PROVIDER_NOT_FOUND',
76
+ )
77
+ }
78
+
79
+ /**
80
+ * Create a summarizer based on configuration.
81
+ *
82
+ * @param config - Summarization configuration
83
+ * @returns A configured Summarizer instance
84
+ * @throws SummarizationError if provider is not available or configured
85
+ */
86
+ /**
87
+ * Type guard to check if a provider is a CLI provider
88
+ */
89
+ const isCLIProvider = (provider: string): provider is CLIProviderName => {
90
+ return ['claude', 'copilot', 'cline', 'aider', 'opencode', 'amp'].includes(
91
+ provider,
92
+ )
93
+ }
94
+
95
+ /**
96
+ * Type guard to check if a provider is an API provider
97
+ */
98
+ const isAPIProvider = (provider: string): provider is APIProviderName => {
99
+ return ['deepseek', 'anthropic', 'openai', 'gemini', 'qwen'].includes(
100
+ provider,
101
+ )
102
+ }
103
+
104
+ export const createSummarizer = async (
105
+ config: AISummarizationConfig,
106
+ ): Promise<Summarizer> => {
107
+ if (config.mode === 'cli') {
108
+ if (!isCLIProvider(config.provider)) {
109
+ throw new SummarizationError(
110
+ `Invalid CLI provider: ${config.provider}`,
111
+ 'PROVIDER_NOT_FOUND',
112
+ config.provider,
113
+ )
114
+ }
115
+ return createCLISummarizer(config.provider)
116
+ }
117
+
118
+ if (config.mode === 'api') {
119
+ if (!isAPIProvider(config.provider)) {
120
+ throw new SummarizationError(
121
+ `Invalid API provider: ${config.provider}`,
122
+ 'PROVIDER_NOT_FOUND',
123
+ config.provider,
124
+ )
125
+ }
126
+ return createAPISummarizer(config.provider, config)
127
+ }
128
+
129
+ throw new SummarizationError(
130
+ `Unknown summarization mode: ${config.mode}`,
131
+ 'PROVIDER_NOT_FOUND',
132
+ )
133
+ }
134
+
135
+ /**
136
+ * Get the best available summarizer.
137
+ *
138
+ * Checks CLI providers first (free), then falls back to API providers.
139
+ * Returns null if no providers are available.
140
+ */
141
+ export const getBestAvailableSummarizer = async (
142
+ preferredConfig?: Partial<AISummarizationConfig>,
143
+ ): Promise<{
144
+ summarizer: Summarizer
145
+ config: AISummarizationConfig
146
+ } | null> => {
147
+ // If config specifies a provider, try that first
148
+ if (preferredConfig?.provider && preferredConfig?.mode) {
149
+ try {
150
+ const summarizer = await createSummarizer(
151
+ preferredConfig as AISummarizationConfig,
152
+ )
153
+ return {
154
+ summarizer,
155
+ config: preferredConfig as AISummarizationConfig,
156
+ }
157
+ } catch {
158
+ // Fall through to auto-detection
159
+ }
160
+ }
161
+
162
+ // Try Claude CLI first (most common)
163
+ if (await isCLIInstalled('claude')) {
164
+ const config: AISummarizationConfig = {
165
+ mode: 'cli',
166
+ provider: 'claude',
167
+ }
168
+ return {
169
+ summarizer: new ClaudeCLISummarizer(),
170
+ config,
171
+ }
172
+ }
173
+
174
+ // TODO: Try other CLI providers
175
+ // TODO: Try API providers with configured keys
176
+
177
+ return null
178
+ }