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,30 @@
1
+ /**
2
+ * Type declarations for wink-bm25-text-search
3
+ */
4
+
5
+ declare module 'wink-bm25-text-search' {
6
+ interface BM25Config {
7
+ fldWeights?: Record<string, number>
8
+ bm25Params?: {
9
+ k1?: number
10
+ b?: number
11
+ }
12
+ ovFldNames?: string[]
13
+ }
14
+
15
+ type PrepTask = (text: string) => string[]
16
+
17
+ interface BM25Engine {
18
+ defineConfig(config: BM25Config): void
19
+ definePrepTasks(tasks: PrepTask[]): void
20
+ addDoc(doc: Record<string, string>, id: number): void
21
+ consolidate(): void
22
+ search(query: string, limit?: number): [number, number][]
23
+ exportJSON(): string
24
+ importJSON(json: string): void
25
+ reset(): void
26
+ }
27
+
28
+ function bm25(): BM25Engine
29
+ export default bm25
30
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Claude CLI Summarizer
3
+ *
4
+ * Uses Claude Code CLI for AI summarization.
5
+ * FREE for users with Claude Code subscriptions.
6
+ *
7
+ * SECURITY: Uses spawn() with argument arrays - NEVER exec() with string interpolation.
8
+ */
9
+
10
+ import { spawn } from 'node:child_process'
11
+ import type {
12
+ StreamingSummarizer,
13
+ StreamOptions,
14
+ SummaryResult,
15
+ } from '../types.js'
16
+ import { SummarizationError as SummarizationErrorClass } from '../types.js'
17
+
18
+ /**
19
+ * Claude CLI provider for summarization.
20
+ *
21
+ * Uses the `claude` CLI tool in non-interactive mode with text output.
22
+ * Requires Claude Code installation and authentication.
23
+ *
24
+ * @security Uses spawn() with argument arrays to prevent shell injection.
25
+ * User input is passed as array elements, never interpolated.
26
+ *
27
+ * @cost Free (uses existing Claude subscription)
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const summarizer = new ClaudeCLISummarizer()
32
+ *
33
+ * // Check availability
34
+ * if (await summarizer.isAvailable()) {
35
+ * const result = await summarizer.summarize(searchResults, prompt)
36
+ * console.log(result.summary)
37
+ * // result.estimatedCost is always 0 (free)
38
+ * }
39
+ *
40
+ * // Streaming output
41
+ * await summarizer.summarizeStream(searchResults, prompt, {
42
+ * onChunk: (chunk) => process.stdout.write(chunk),
43
+ * onComplete: (result) => console.log(`Done in ${result.durationMs}ms`),
44
+ * })
45
+ * ```
46
+ */
47
+ export class ClaudeCLISummarizer implements StreamingSummarizer {
48
+ private readonly command = 'claude'
49
+
50
+ async summarize(input: string, prompt: string): Promise<SummaryResult> {
51
+ const startTime = Date.now()
52
+ const fullPrompt = `${prompt}\n\n${input}`
53
+
54
+ return new Promise((resolve, reject) => {
55
+ // SECURITY: spawn() with argument array - safe from shell injection
56
+ const proc = spawn(
57
+ this.command,
58
+ ['-p', fullPrompt, '--output-format', 'text'],
59
+ {
60
+ stdio: ['ignore', 'pipe', 'pipe'],
61
+ },
62
+ )
63
+
64
+ let stdout = ''
65
+ let stderr = ''
66
+
67
+ proc.stdout.on('data', (data: Buffer) => {
68
+ stdout += data.toString()
69
+ })
70
+
71
+ proc.stderr.on('data', (data: Buffer) => {
72
+ stderr += data.toString()
73
+ })
74
+
75
+ proc.on('close', (code: number | null) => {
76
+ const durationMs = Date.now() - startTime
77
+
78
+ if (code !== 0) {
79
+ reject(
80
+ new SummarizationErrorClass(
81
+ `Claude CLI exited with code ${code}: ${stderr}`,
82
+ 'CLI_EXECUTION_FAILED',
83
+ 'claude',
84
+ ),
85
+ )
86
+ return
87
+ }
88
+
89
+ resolve({
90
+ summary: stdout.trim(),
91
+ provider: 'claude',
92
+ mode: 'cli',
93
+ estimatedCost: 0,
94
+ durationMs,
95
+ })
96
+ })
97
+
98
+ proc.on('error', (error: Error) => {
99
+ reject(
100
+ new SummarizationErrorClass(
101
+ `Failed to spawn Claude CLI: ${error.message}`,
102
+ 'CLI_EXECUTION_FAILED',
103
+ 'claude',
104
+ error,
105
+ ),
106
+ )
107
+ })
108
+ })
109
+ }
110
+
111
+ async summarizeStream(
112
+ input: string,
113
+ prompt: string,
114
+ options: StreamOptions,
115
+ ): Promise<void> {
116
+ const startTime = Date.now()
117
+ const fullPrompt = `${prompt}\n\n${input}`
118
+
119
+ return new Promise((resolve, reject) => {
120
+ // SECURITY: spawn() with argument array - safe from shell injection
121
+ const proc = spawn(
122
+ this.command,
123
+ ['-p', fullPrompt, '--output-format', 'text'],
124
+ {
125
+ stdio: ['ignore', 'pipe', 'pipe'],
126
+ },
127
+ )
128
+
129
+ let fullOutput = ''
130
+ let stderr = ''
131
+
132
+ proc.stdout.on('data', (data: Buffer) => {
133
+ const chunk = data.toString()
134
+ fullOutput += chunk
135
+ options.onChunk(chunk)
136
+ })
137
+
138
+ proc.stderr.on('data', (data: Buffer) => {
139
+ stderr += data.toString()
140
+ })
141
+
142
+ proc.on('close', (code: number | null) => {
143
+ const durationMs = Date.now() - startTime
144
+
145
+ if (code !== 0) {
146
+ const error = new SummarizationErrorClass(
147
+ `Claude CLI exited with code ${code}: ${stderr}`,
148
+ 'CLI_EXECUTION_FAILED',
149
+ 'claude',
150
+ )
151
+ options.onError?.(error)
152
+ reject(error)
153
+ return
154
+ }
155
+
156
+ const result: SummaryResult = {
157
+ summary: fullOutput.trim(),
158
+ provider: 'claude',
159
+ mode: 'cli',
160
+ estimatedCost: 0,
161
+ durationMs,
162
+ }
163
+
164
+ options.onComplete?.(result)
165
+ resolve()
166
+ })
167
+
168
+ proc.on('error', (error: Error) => {
169
+ const sumError = new SummarizationErrorClass(
170
+ `Failed to spawn Claude CLI: ${error.message}`,
171
+ 'CLI_EXECUTION_FAILED',
172
+ 'claude',
173
+ error,
174
+ )
175
+ options.onError?.(sumError)
176
+ reject(sumError)
177
+ })
178
+ })
179
+ }
180
+
181
+ estimateCost(_inputTokens: number): number {
182
+ // CLI providers are free with subscription
183
+ return 0
184
+ }
185
+
186
+ async isAvailable(): Promise<boolean> {
187
+ return new Promise((resolve) => {
188
+ const checkCommand = process.platform === 'win32' ? 'where' : 'which'
189
+ const proc = spawn(checkCommand, [this.command], {
190
+ stdio: ['ignore', 'ignore', 'ignore'],
191
+ })
192
+
193
+ proc.on('close', (code) => {
194
+ resolve(code === 0)
195
+ })
196
+
197
+ proc.on('error', () => {
198
+ resolve(false)
199
+ })
200
+ })
201
+ }
202
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Tests for CLI Provider Detection Module
3
+ */
4
+
5
+ import { type ChildProcess, spawn } from 'node:child_process'
6
+ import { EventEmitter } from 'node:events'
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
8
+ import type { CLIProviderName } from '../types.js'
9
+ import {
10
+ detectInstalledCLIs,
11
+ getCLIInfo,
12
+ isCLIInstalled,
13
+ KNOWN_CLIS,
14
+ } from './detection.js'
15
+
16
+ vi.mock('node:child_process', () => ({
17
+ spawn: vi.fn(),
18
+ }))
19
+
20
+ const mockSpawn = vi.mocked(spawn)
21
+
22
+ const createMockProcess = (exitCode: number | null, emitError = false) => {
23
+ const proc = new EventEmitter() as ChildProcess
24
+ setTimeout(() => {
25
+ if (emitError) {
26
+ proc.emit('error', new Error('spawn ENOENT'))
27
+ } else {
28
+ proc.emit('close', exitCode)
29
+ }
30
+ }, 0)
31
+ return proc
32
+ }
33
+
34
+ describe('KNOWN_CLIS', () => {
35
+ const expectedProviders: CLIProviderName[] = [
36
+ 'claude',
37
+ 'opencode',
38
+ 'copilot',
39
+ 'aider',
40
+ 'cline',
41
+ 'amp',
42
+ ]
43
+
44
+ it('should contain all expected CLI providers', () => {
45
+ const names = KNOWN_CLIS.map((cli) => cli.name)
46
+ for (const provider of expectedProviders) {
47
+ expect(names).toContain(provider)
48
+ }
49
+ })
50
+
51
+ it('should have required fields for each CLI', () => {
52
+ for (const cli of KNOWN_CLIS) {
53
+ expect(cli).toHaveProperty('name')
54
+ expect(cli).toHaveProperty('command')
55
+ expect(cli).toHaveProperty('displayName')
56
+ expect(cli).toHaveProperty('args')
57
+ expect(cli).toHaveProperty('useStdin')
58
+
59
+ expect(typeof cli.name).toBe('string')
60
+ expect(typeof cli.command).toBe('string')
61
+ expect(typeof cli.displayName).toBe('string')
62
+ expect(Array.isArray(cli.args)).toBe(true)
63
+ expect(typeof cli.useStdin).toBe('boolean')
64
+ }
65
+ })
66
+
67
+ describe('individual CLI configurations', () => {
68
+ it('should have correct claude configuration', () => {
69
+ const claude = KNOWN_CLIS.find((cli) => cli.name === 'claude')
70
+ expect(claude).toBeDefined()
71
+ expect(claude!.command).toBe('claude')
72
+ expect(claude!.displayName).toBe('Claude Code')
73
+ expect(claude!.args).toContain('-p')
74
+ expect(claude!.useStdin).toBe(false)
75
+ })
76
+
77
+ it('should have correct opencode configuration', () => {
78
+ const opencode = KNOWN_CLIS.find((cli) => cli.name === 'opencode')
79
+ expect(opencode).toBeDefined()
80
+ expect(opencode!.command).toBe('opencode')
81
+ expect(opencode!.displayName).toBe('OpenCode')
82
+ expect(opencode!.useStdin).toBe(true)
83
+ })
84
+
85
+ it('should have correct copilot configuration', () => {
86
+ const copilot = KNOWN_CLIS.find((cli) => cli.name === 'copilot')
87
+ expect(copilot).toBeDefined()
88
+ expect(copilot!.command).toBe('gh')
89
+ expect(copilot!.displayName).toBe('GitHub Copilot CLI')
90
+ expect(copilot!.args).toContain('copilot')
91
+ expect(copilot!.useStdin).toBe(true)
92
+ })
93
+
94
+ it('should have correct aider configuration', () => {
95
+ const aider = KNOWN_CLIS.find((cli) => cli.name === 'aider')
96
+ expect(aider).toBeDefined()
97
+ expect(aider!.command).toBe('aider')
98
+ expect(aider!.displayName).toBe('Aider')
99
+ expect(aider!.useStdin).toBe(false)
100
+ })
101
+
102
+ it('should have correct cline configuration', () => {
103
+ const cline = KNOWN_CLIS.find((cli) => cli.name === 'cline')
104
+ expect(cline).toBeDefined()
105
+ expect(cline!.command).toBe('cline')
106
+ expect(cline!.displayName).toBe('Cline')
107
+ expect(cline!.useStdin).toBe(false)
108
+ })
109
+
110
+ it('should have correct amp configuration', () => {
111
+ const amp = KNOWN_CLIS.find((cli) => cli.name === 'amp')
112
+ expect(amp).toBeDefined()
113
+ expect(amp!.command).toBe('amp')
114
+ expect(amp!.displayName).toBe('Amp')
115
+ expect(amp!.useStdin).toBe(false)
116
+ })
117
+ })
118
+ })
119
+
120
+ describe('getCLIInfo', () => {
121
+ it('should return correct info for known providers', () => {
122
+ const claude = getCLIInfo('claude')
123
+ expect(claude).toBeDefined()
124
+ expect(claude!.name).toBe('claude')
125
+ expect(claude!.command).toBe('claude')
126
+
127
+ const copilot = getCLIInfo('copilot')
128
+ expect(copilot).toBeDefined()
129
+ expect(copilot!.name).toBe('copilot')
130
+ expect(copilot!.command).toBe('gh')
131
+ })
132
+
133
+ it('should return undefined for unknown provider', () => {
134
+ const unknown = getCLIInfo('unknown' as CLIProviderName)
135
+ expect(unknown).toBeUndefined()
136
+ })
137
+
138
+ it('should return all fields for a CLI', () => {
139
+ const cli = getCLIInfo('claude')
140
+ expect(cli).toMatchObject({
141
+ name: 'claude',
142
+ command: 'claude',
143
+ displayName: 'Claude Code',
144
+ args: expect.any(Array),
145
+ useStdin: false,
146
+ })
147
+ })
148
+ })
149
+
150
+ describe('isCLIInstalled', () => {
151
+ beforeEach(() => {
152
+ vi.clearAllMocks()
153
+ })
154
+
155
+ afterEach(() => {
156
+ vi.restoreAllMocks()
157
+ })
158
+
159
+ it('should return true when CLI exists (exit code 0)', async () => {
160
+ mockSpawn.mockReturnValue(createMockProcess(0))
161
+
162
+ const result = await isCLIInstalled('claude')
163
+
164
+ expect(result).toBe(true)
165
+ expect(mockSpawn).toHaveBeenCalledWith(
166
+ expect.stringMatching(/^(which|where)$/),
167
+ ['claude'],
168
+ { stdio: ['ignore', 'pipe', 'ignore'] },
169
+ )
170
+ })
171
+
172
+ it('should return false when CLI does not exist (exit code 1)', async () => {
173
+ mockSpawn.mockReturnValue(createMockProcess(1))
174
+
175
+ const result = await isCLIInstalled('claude')
176
+
177
+ expect(result).toBe(false)
178
+ })
179
+
180
+ it('should return false when spawn emits an error', async () => {
181
+ mockSpawn.mockReturnValue(createMockProcess(null, true))
182
+
183
+ const result = await isCLIInstalled('claude')
184
+
185
+ expect(result).toBe(false)
186
+ })
187
+
188
+ it('should return false for unknown provider', async () => {
189
+ const result = await isCLIInstalled('unknown' as CLIProviderName)
190
+
191
+ expect(result).toBe(false)
192
+ expect(mockSpawn).not.toHaveBeenCalled()
193
+ })
194
+
195
+ it('should check the correct command for copilot (gh)', async () => {
196
+ mockSpawn.mockReturnValue(createMockProcess(0))
197
+
198
+ await isCLIInstalled('copilot')
199
+
200
+ expect(mockSpawn).toHaveBeenCalledWith(
201
+ expect.stringMatching(/^(which|where)$/),
202
+ ['gh'],
203
+ expect.any(Object),
204
+ )
205
+ })
206
+ })
207
+
208
+ describe('detectInstalledCLIs', () => {
209
+ beforeEach(() => {
210
+ vi.clearAllMocks()
211
+ })
212
+
213
+ afterEach(() => {
214
+ vi.restoreAllMocks()
215
+ })
216
+
217
+ it('should return all CLIs when all are installed', async () => {
218
+ mockSpawn.mockReturnValue(createMockProcess(0))
219
+
220
+ const result = await detectInstalledCLIs()
221
+
222
+ expect(result.length).toBe(KNOWN_CLIS.length)
223
+ expect(result.map((cli) => cli.name)).toEqual(
224
+ expect.arrayContaining([
225
+ 'claude',
226
+ 'opencode',
227
+ 'copilot',
228
+ 'aider',
229
+ 'cline',
230
+ 'amp',
231
+ ]),
232
+ )
233
+ })
234
+
235
+ it('should return empty array when no CLIs are installed', async () => {
236
+ mockSpawn.mockReturnValue(createMockProcess(1))
237
+
238
+ const result = await detectInstalledCLIs()
239
+
240
+ expect(result).toEqual([])
241
+ })
242
+
243
+ it('should return only installed CLIs', async () => {
244
+ const installedCommands = new Set(['claude', 'gh'])
245
+ mockSpawn.mockImplementation((_cmd, args) => {
246
+ const command = args[0] as string
247
+ const isInstalled = installedCommands.has(command)
248
+ return createMockProcess(isInstalled ? 0 : 1)
249
+ })
250
+
251
+ const result = await detectInstalledCLIs()
252
+
253
+ expect(result.length).toBe(2)
254
+ expect(result.map((cli) => cli.name)).toContain('claude')
255
+ expect(result.map((cli) => cli.name)).toContain('copilot')
256
+ })
257
+
258
+ it('should handle errors gracefully', async () => {
259
+ mockSpawn.mockReturnValue(createMockProcess(null, true))
260
+
261
+ const result = await detectInstalledCLIs()
262
+
263
+ expect(result).toEqual([])
264
+ })
265
+
266
+ it('should check all known CLIs', async () => {
267
+ mockSpawn.mockReturnValue(createMockProcess(0))
268
+
269
+ await detectInstalledCLIs()
270
+
271
+ expect(mockSpawn).toHaveBeenCalledTimes(KNOWN_CLIS.length)
272
+ })
273
+ })
@@ -0,0 +1,118 @@
1
+ /**
2
+ * CLI Provider Detection
3
+ *
4
+ * Detects installed CLI tools that can be used for AI summarization.
5
+ * Uses spawn() with argument arrays for security - NEVER exec() with string interpolation.
6
+ */
7
+
8
+ import { spawn } from 'node:child_process'
9
+ import type { CLIInfo, CLIProviderName } from '../types.js'
10
+
11
+ /**
12
+ * Known CLI tools with their configuration.
13
+ *
14
+ * SECURITY: All CLI invocations use spawn() with argument arrays.
15
+ * The args array is used directly, never interpolated into strings.
16
+ */
17
+ export const KNOWN_CLIS: readonly CLIInfo[] = [
18
+ {
19
+ name: 'claude',
20
+ command: 'claude',
21
+ displayName: 'Claude Code',
22
+ args: ['-p', '--output-format', 'text'],
23
+ useStdin: false, // Uses -p flag for prompt, not stdin
24
+ },
25
+ {
26
+ name: 'opencode',
27
+ command: 'opencode',
28
+ displayName: 'OpenCode',
29
+ args: ['run', '--format', 'text'],
30
+ useStdin: true,
31
+ },
32
+ {
33
+ name: 'copilot',
34
+ command: 'gh',
35
+ displayName: 'GitHub Copilot CLI',
36
+ args: ['copilot', 'explain'],
37
+ useStdin: true,
38
+ },
39
+ {
40
+ name: 'aider',
41
+ command: 'aider',
42
+ displayName: 'Aider',
43
+ args: ['--message'],
44
+ useStdin: false,
45
+ },
46
+ {
47
+ name: 'cline',
48
+ command: 'cline',
49
+ displayName: 'Cline',
50
+ args: ['--prompt'],
51
+ useStdin: false,
52
+ },
53
+ {
54
+ name: 'amp',
55
+ command: 'amp',
56
+ displayName: 'Amp',
57
+ args: ['--prompt'],
58
+ useStdin: false,
59
+ },
60
+ ] as const
61
+
62
+ /**
63
+ * Check if a command exists on the system.
64
+ *
65
+ * SECURITY: Uses spawn() with argument array, not exec() with string interpolation.
66
+ */
67
+ const commandExists = (command: string): Promise<boolean> => {
68
+ return new Promise((resolve) => {
69
+ // Use 'which' on Unix, 'where' on Windows
70
+ const checkCommand = process.platform === 'win32' ? 'where' : 'which'
71
+
72
+ const proc = spawn(checkCommand, [command], {
73
+ stdio: ['ignore', 'pipe', 'ignore'],
74
+ })
75
+
76
+ proc.on('close', (code) => {
77
+ resolve(code === 0)
78
+ })
79
+
80
+ proc.on('error', () => {
81
+ resolve(false)
82
+ })
83
+ })
84
+ }
85
+
86
+ /**
87
+ * Detect all installed CLI tools that can be used for summarization.
88
+ *
89
+ * @returns Array of CLIInfo for installed tools
90
+ */
91
+ export const detectInstalledCLIs = async (): Promise<CLIInfo[]> => {
92
+ const results = await Promise.all(
93
+ KNOWN_CLIS.map(async (cli) => {
94
+ const exists = await commandExists(cli.command)
95
+ return exists ? cli : null
96
+ }),
97
+ )
98
+
99
+ return results.filter((cli): cli is CLIInfo => cli !== null)
100
+ }
101
+
102
+ /**
103
+ * Get CLI info by name.
104
+ */
105
+ export const getCLIInfo = (name: CLIProviderName): CLIInfo | undefined => {
106
+ return KNOWN_CLIS.find((cli) => cli.name === name)
107
+ }
108
+
109
+ /**
110
+ * Check if a specific CLI is installed.
111
+ */
112
+ export const isCLIInstalled = async (
113
+ name: CLIProviderName,
114
+ ): Promise<boolean> => {
115
+ const cli = getCLIInfo(name)
116
+ if (!cli) return false
117
+ return commandExists(cli.command)
118
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * CLI Providers Module
3
+ *
4
+ * Exports all CLI-based summarizers and detection utilities.
5
+ */
6
+
7
+ export * from './claude.js'
8
+ export * from './detection.js'