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,240 @@
1
+ /**
2
+ * Provider Factory Unit Tests
3
+ *
4
+ * Tests for the embedding provider factory module ensuring correct
5
+ * provider creation with appropriate baseURL mapping.
6
+ */
7
+
8
+ import { Effect, Option, Redacted } from 'effect'
9
+ import { describe, expect, it } from 'vitest'
10
+ import {
11
+ createEmbeddingProviderDirect,
12
+ getProviderBaseURL,
13
+ PROVIDER_BASE_URLS,
14
+ } from './provider-factory.js'
15
+
16
+ describe('Provider Factory', () => {
17
+ describe('PROVIDER_BASE_URLS constant', () => {
18
+ it('should have undefined for openai (uses SDK default)', () => {
19
+ expect(PROVIDER_BASE_URLS.openai).toBeUndefined()
20
+ })
21
+
22
+ it('should have correct URL for ollama', () => {
23
+ expect(PROVIDER_BASE_URLS.ollama).toBe('http://localhost:11434/v1')
24
+ })
25
+
26
+ it('should have correct URL for lm-studio', () => {
27
+ expect(PROVIDER_BASE_URLS['lm-studio']).toBe('http://localhost:1234/v1')
28
+ })
29
+
30
+ it('should have correct URL for openrouter', () => {
31
+ expect(PROVIDER_BASE_URLS.openrouter).toBe('https://openrouter.ai/api/v1')
32
+ })
33
+
34
+ it('should have all five providers defined', () => {
35
+ const providers = Object.keys(PROVIDER_BASE_URLS)
36
+ expect(providers).toHaveLength(5)
37
+ expect(providers).toContain('openai')
38
+ expect(providers).toContain('ollama')
39
+ expect(providers).toContain('lm-studio')
40
+ expect(providers).toContain('openrouter')
41
+ expect(providers).toContain('voyage')
42
+ })
43
+ })
44
+
45
+ describe('getProviderBaseURL', () => {
46
+ it('should return config baseURL when Option.some is provided', () => {
47
+ const customURL = 'https://custom.api.com/v1'
48
+ const result = getProviderBaseURL('openai', Option.some(customURL))
49
+ expect(result).toBe(customURL)
50
+ })
51
+
52
+ it('should return config baseURL over provider default', () => {
53
+ const customURL = 'https://custom-ollama.example.com/v1'
54
+ const result = getProviderBaseURL('ollama', Option.some(customURL))
55
+ expect(result).toBe(customURL)
56
+ })
57
+
58
+ it('should return provider default when Option.none for ollama', () => {
59
+ const result = getProviderBaseURL('ollama', Option.none())
60
+ expect(result).toBe('http://localhost:11434/v1')
61
+ })
62
+
63
+ it('should return provider default when Option.none for lm-studio', () => {
64
+ const result = getProviderBaseURL('lm-studio', Option.none())
65
+ expect(result).toBe('http://localhost:1234/v1')
66
+ })
67
+
68
+ it('should return provider default when Option.none for openrouter', () => {
69
+ const result = getProviderBaseURL('openrouter', Option.none())
70
+ expect(result).toBe('https://openrouter.ai/api/v1')
71
+ })
72
+
73
+ it('should return undefined for openai when Option.none', () => {
74
+ const result = getProviderBaseURL('openai', Option.none())
75
+ expect(result).toBeUndefined()
76
+ })
77
+ })
78
+
79
+ describe('createEmbeddingProviderDirect', () => {
80
+ it('should create provider with ollama default baseURL', async () => {
81
+ const program = createEmbeddingProviderDirect({
82
+ provider: 'ollama',
83
+ apiKey: 'test-key',
84
+ })
85
+
86
+ const provider = await Effect.runPromise(program)
87
+ expect(provider.name).toContain('ollama')
88
+ })
89
+
90
+ it('should create provider with lm-studio default baseURL', async () => {
91
+ const program = createEmbeddingProviderDirect({
92
+ provider: 'lm-studio',
93
+ apiKey: 'test-key',
94
+ })
95
+
96
+ const provider = await Effect.runPromise(program)
97
+ expect(provider.name).toContain('lm-studio')
98
+ })
99
+
100
+ it('should create provider with openrouter default baseURL', async () => {
101
+ const program = createEmbeddingProviderDirect({
102
+ provider: 'openrouter',
103
+ apiKey: 'test-key',
104
+ })
105
+
106
+ const provider = await Effect.runPromise(program)
107
+ expect(provider.name).toContain('openrouter')
108
+ })
109
+
110
+ it('should create openai provider with SDK default (no baseURL)', async () => {
111
+ const program = createEmbeddingProviderDirect({
112
+ provider: 'openai',
113
+ apiKey: 'test-key',
114
+ })
115
+
116
+ const provider = await Effect.runPromise(program)
117
+ expect(provider.name).toContain('openai')
118
+ })
119
+
120
+ it('should respect custom baseURL override for any provider', async () => {
121
+ const customURL = 'https://my-proxy.example.com/v1'
122
+ const program = createEmbeddingProviderDirect({
123
+ provider: 'openai',
124
+ baseURL: customURL,
125
+ apiKey: 'test-key',
126
+ })
127
+
128
+ const provider = await Effect.runPromise(program)
129
+ expect(provider).toBeDefined()
130
+ })
131
+
132
+ it('should accept baseURL as Option.some', async () => {
133
+ const customURL = 'https://custom.api.com/v1'
134
+ const program = createEmbeddingProviderDirect({
135
+ provider: 'openai',
136
+ baseURL: Option.some(customURL),
137
+ apiKey: 'test-key',
138
+ })
139
+
140
+ const provider = await Effect.runPromise(program)
141
+ expect(provider).toBeDefined()
142
+ })
143
+
144
+ it('should use provider default when baseURL is Option.none', async () => {
145
+ const program = createEmbeddingProviderDirect({
146
+ provider: 'ollama',
147
+ baseURL: Option.none(),
148
+ apiKey: 'test-key',
149
+ })
150
+
151
+ const provider = await Effect.runPromise(program)
152
+ expect(provider.name).toContain('ollama')
153
+ })
154
+
155
+ it('should pass custom model to provider', async () => {
156
+ const program = createEmbeddingProviderDirect({
157
+ provider: 'openai',
158
+ model: 'text-embedding-3-large',
159
+ apiKey: 'test-key',
160
+ })
161
+
162
+ const provider = await Effect.runPromise(program)
163
+ expect(provider.name).toContain('text-embedding-3-large')
164
+ })
165
+
166
+ it('should accept apiKey as Option.some', async () => {
167
+ const program = createEmbeddingProviderDirect({
168
+ provider: 'openai',
169
+ apiKey: Option.some('test-key'),
170
+ })
171
+
172
+ const provider = await Effect.runPromise(program)
173
+ expect(provider).toBeDefined()
174
+ })
175
+
176
+ it('should fail when no API key provided', async () => {
177
+ const originalEnv = process.env.OPENAI_API_KEY
178
+ delete process.env.OPENAI_API_KEY
179
+
180
+ try {
181
+ const program = createEmbeddingProviderDirect({
182
+ provider: 'openai',
183
+ })
184
+
185
+ await expect(Effect.runPromise(program)).rejects.toThrow()
186
+ } finally {
187
+ if (originalEnv) {
188
+ process.env.OPENAI_API_KEY = originalEnv
189
+ }
190
+ }
191
+ })
192
+
193
+ it('should fail when apiKey is Option.none and no env var', async () => {
194
+ const originalEnv = process.env.OPENAI_API_KEY
195
+ delete process.env.OPENAI_API_KEY
196
+
197
+ try {
198
+ const program = createEmbeddingProviderDirect({
199
+ provider: 'openai',
200
+ apiKey: Option.none(),
201
+ })
202
+
203
+ await expect(Effect.runPromise(program)).rejects.toThrow()
204
+ } finally {
205
+ if (originalEnv) {
206
+ process.env.OPENAI_API_KEY = originalEnv
207
+ }
208
+ }
209
+ })
210
+
211
+ it('should accept apiKey as Redacted<string>', async () => {
212
+ const redactedKey = Redacted.make('test-redacted-key')
213
+ const program = createEmbeddingProviderDirect({
214
+ provider: 'openai',
215
+ apiKey: redactedKey,
216
+ })
217
+
218
+ const provider = await Effect.runPromise(program)
219
+ expect(provider).toBeDefined()
220
+ expect(provider.name).toContain('openai')
221
+ })
222
+
223
+ it('should not expose Redacted API key when stringified', () => {
224
+ const redactedKey = Redacted.make('sk-secret-key-12345')
225
+ const stringified = String(redactedKey)
226
+
227
+ // Redacted type shows <redacted> when stringified
228
+ expect(stringified).not.toContain('sk-secret-key-12345')
229
+ expect(stringified).toContain('<redacted>')
230
+ })
231
+
232
+ it('should preserve API key value in Redacted wrapper', () => {
233
+ const secretKey = 'sk-secret-key-12345'
234
+ const redactedKey = Redacted.make(secretKey)
235
+
236
+ // Value can still be extracted when needed
237
+ expect(Redacted.value(redactedKey)).toBe(secretKey)
238
+ })
239
+ })
240
+ })
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Embedding Provider Factory
3
+ *
4
+ * Creates embedding providers based on configuration. Supports multiple
5
+ * providers (OpenAI, Ollama, LM Studio, OpenRouter) with automatic
6
+ * baseURL mapping for OpenAI-compatible APIs.
7
+ */
8
+
9
+ import { Effect, Option, Redacted } from 'effect'
10
+ import { ConfigService, type EmbeddingProvider } from '../config/index.js'
11
+ import type { EmbeddingsConfig } from '../config/schema.js'
12
+ import type { ApiKeyMissingError } from '../errors/index.js'
13
+ import { createOpenAIProvider } from './openai-provider.js'
14
+ import { PROVIDER_BASE_URLS } from './provider-constants.js'
15
+ import type { EmbeddingProvider as EmbeddingProviderInterface } from './types.js'
16
+ import { createVoyageProvider } from './voyage-provider.js'
17
+
18
+ // Re-export provider constants for backward compatibility
19
+ export { PROVIDER_BASE_URLS } from './provider-constants.js'
20
+
21
+ /**
22
+ * Get the base URL for a provider, respecting config override.
23
+ *
24
+ * Precedence:
25
+ * 1. Explicit baseURL from config (highest priority)
26
+ * 2. Provider-specific default from PROVIDER_BASE_URLS
27
+ * 3. OpenAI SDK default (undefined means use SDK default)
28
+ */
29
+ export const getProviderBaseURL = (
30
+ provider: EmbeddingProvider,
31
+ configBaseURL: Option.Option<string>,
32
+ ): string | undefined => {
33
+ // Config baseURL takes precedence
34
+ if (Option.isSome(configBaseURL)) {
35
+ return configBaseURL.value
36
+ }
37
+ // Fall back to provider default
38
+ return PROVIDER_BASE_URLS[provider]
39
+ }
40
+
41
+ // ============================================================================
42
+ // Provider Factory
43
+ // ============================================================================
44
+
45
+ /**
46
+ * Configuration subset needed for provider creation.
47
+ * Extracted from EmbeddingsConfig to allow direct config passing.
48
+ */
49
+ export interface ProviderFactoryConfig {
50
+ readonly provider: EmbeddingProvider
51
+ readonly baseURL?: Option.Option<string> | string | undefined
52
+ readonly model?: string | undefined
53
+ readonly dimensions?: number | undefined
54
+ readonly batchSize?: number | undefined
55
+ /**
56
+ * API key for the provider. Accepts multiple formats:
57
+ * - Plain string: 'sk-...'
58
+ * - Redacted<string>: Redacted.make('sk-...') (recommended for security)
59
+ * - Option<string>: Option.some('sk-...')
60
+ */
61
+ readonly apiKey?:
62
+ | Option.Option<string>
63
+ | string
64
+ | Redacted.Redacted<string>
65
+ | undefined
66
+ /**
67
+ * Request timeout in milliseconds.
68
+ * Default: 30000 (30 seconds)
69
+ */
70
+ readonly timeout?: number | undefined
71
+ }
72
+
73
+ /**
74
+ * Normalize baseURL from various input formats to Option<string>.
75
+ */
76
+ const normalizeBaseURL = (
77
+ baseURL: Option.Option<string> | string | undefined,
78
+ ): Option.Option<string> => {
79
+ if (baseURL === undefined) {
80
+ return Option.none()
81
+ }
82
+ if (typeof baseURL === 'string') {
83
+ return Option.some(baseURL)
84
+ }
85
+ return baseURL
86
+ }
87
+
88
+ /**
89
+ * Normalize apiKey from various input formats to string | Redacted<string> | undefined.
90
+ * Preserves Redacted wrapper for security - don't unwrap until needed.
91
+ */
92
+ const normalizeApiKey = (
93
+ apiKey:
94
+ | Option.Option<string>
95
+ | string
96
+ | Redacted.Redacted<string>
97
+ | undefined,
98
+ ): string | Redacted.Redacted<string> | undefined => {
99
+ if (apiKey === undefined) {
100
+ return undefined
101
+ }
102
+ // Check for Redacted first (keep wrapped for security)
103
+ if (Redacted.isRedacted(apiKey)) {
104
+ return apiKey
105
+ }
106
+ if (typeof apiKey === 'string') {
107
+ return apiKey
108
+ }
109
+ // Handle Option<string>
110
+ return Option.isSome(apiKey) ? apiKey.value : undefined
111
+ }
112
+
113
+ /**
114
+ * Create an embedding provider based on configuration.
115
+ *
116
+ * All supported providers (OpenAI, Ollama, LM Studio, OpenRouter) use
117
+ * OpenAI-compatible APIs, so we use the OpenAI provider with different
118
+ * base URLs.
119
+ *
120
+ * @param config - Optional explicit config (if not provided, reads from ConfigService)
121
+ * @returns Effect yielding the configured EmbeddingProvider
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * // Using ConfigService (reads from environment/config file)
126
+ * const provider = yield* createEmbeddingProvider()
127
+ *
128
+ * // Explicit config override
129
+ * const provider = yield* createEmbeddingProvider({
130
+ * provider: 'ollama',
131
+ * model: 'nomic-embed-text',
132
+ * })
133
+ * ```
134
+ */
135
+ export const createEmbeddingProvider = (
136
+ config?: ProviderFactoryConfig,
137
+ ): Effect.Effect<
138
+ EmbeddingProviderInterface,
139
+ ApiKeyMissingError,
140
+ ConfigService
141
+ > =>
142
+ Effect.gen(function* () {
143
+ // Get embeddings config from service if not provided
144
+ const embeddingsConfig: EmbeddingsConfig = config
145
+ ? ({
146
+ provider: config.provider,
147
+ baseURL: normalizeBaseURL(config.baseURL),
148
+ model: config.model ?? 'text-embedding-3-small',
149
+ dimensions: config.dimensions, // Let provider determine default if not specified
150
+ batchSize: config.batchSize ?? 100,
151
+ maxRetries: 3,
152
+ retryDelayMs: 1000,
153
+ timeoutMs: 30000,
154
+ apiKey: Option.fromNullable(normalizeApiKey(config.apiKey)),
155
+ } as EmbeddingsConfig)
156
+ : (yield* ConfigService).embeddings
157
+
158
+ const provider = embeddingsConfig.provider
159
+ const baseURL = getProviderBaseURL(provider, embeddingsConfig.baseURL)
160
+
161
+ // Extract API key from config if available
162
+ const apiKey = Option.isSome(embeddingsConfig.apiKey)
163
+ ? embeddingsConfig.apiKey.value
164
+ : undefined
165
+
166
+ // Voyage AI uses its own native API, not OpenAI-compatible
167
+ if (provider === 'voyage') {
168
+ return yield* createVoyageProvider({
169
+ model: embeddingsConfig.model,
170
+ batchSize: embeddingsConfig.batchSize,
171
+ apiKey,
172
+ timeout: embeddingsConfig.timeoutMs,
173
+ })
174
+ }
175
+
176
+ // All other providers use OpenAI-compatible API
177
+ return yield* createOpenAIProvider({
178
+ model: embeddingsConfig.model,
179
+ dimensions: embeddingsConfig.dimensions,
180
+ batchSize: embeddingsConfig.batchSize,
181
+ baseURL,
182
+ apiKey,
183
+ timeout: embeddingsConfig.timeoutMs,
184
+ })
185
+ })
186
+
187
+ /**
188
+ * Create an embedding provider without ConfigService dependency.
189
+ *
190
+ * Use this when you need to create a provider outside of the Effect
191
+ * context or when you have explicit config values.
192
+ *
193
+ * @param config - Explicit provider configuration
194
+ * @returns Effect yielding the configured EmbeddingProvider
195
+ */
196
+ export const createEmbeddingProviderDirect = (
197
+ config: ProviderFactoryConfig,
198
+ ): Effect.Effect<EmbeddingProviderInterface, ApiKeyMissingError> =>
199
+ Effect.gen(function* () {
200
+ const provider = config.provider
201
+ const baseURL = getProviderBaseURL(
202
+ provider,
203
+ normalizeBaseURL(config.baseURL),
204
+ )
205
+
206
+ // Voyage AI uses its own native API, not OpenAI-compatible
207
+ if (provider === 'voyage') {
208
+ return yield* createVoyageProvider({
209
+ model: config.model,
210
+ batchSize: config.batchSize,
211
+ apiKey: normalizeApiKey(config.apiKey),
212
+ timeout: config.timeout,
213
+ })
214
+ }
215
+
216
+ // All other providers use OpenAI-compatible API
217
+ return yield* createOpenAIProvider({
218
+ model: config.model,
219
+ dimensions: config.dimensions,
220
+ batchSize: config.batchSize,
221
+ baseURL,
222
+ apiKey: normalizeApiKey(config.apiKey),
223
+ timeout: config.timeout,
224
+ })
225
+ })