mdcontext 0.1.0 → 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 (251) hide show
  1. package/.changeset/config.json +9 -9
  2. package/.claude/settings.local.json +25 -0
  3. package/.github/workflows/claude-code-review.yml +44 -0
  4. package/.github/workflows/claude.yml +85 -0
  5. package/CONTRIBUTING.md +186 -0
  6. package/NOTES/NOTES +44 -0
  7. package/README.md +206 -3
  8. package/biome.json +1 -1
  9. package/dist/chunk-23UPXDNL.js +3044 -0
  10. package/dist/chunk-2W7MO2DL.js +1366 -0
  11. package/dist/chunk-3NUAZGMA.js +1689 -0
  12. package/dist/chunk-7TOWB2XB.js +366 -0
  13. package/dist/chunk-7XOTOADQ.js +3065 -0
  14. package/dist/chunk-AH2PDM2K.js +3042 -0
  15. package/dist/chunk-BNXWSZ63.js +3742 -0
  16. package/dist/chunk-BTL5DJVU.js +3222 -0
  17. package/dist/chunk-HDHYG7E4.js +104 -0
  18. package/dist/chunk-HLR4KZBP.js +3234 -0
  19. package/dist/chunk-IP3FRFEB.js +1045 -0
  20. package/dist/chunk-KHU56VDO.js +3042 -0
  21. package/dist/chunk-KRYIFLQR.js +85 -89
  22. package/dist/chunk-LBSDNLEM.js +287 -0
  23. package/dist/chunk-MNTQ7HCP.js +2643 -0
  24. package/dist/chunk-MUJELQQ6.js +1387 -0
  25. package/dist/chunk-MXJGMSLV.js +2199 -0
  26. package/dist/chunk-N6QJGC3Z.js +2636 -0
  27. package/dist/chunk-OBELGBPM.js +1713 -0
  28. package/dist/chunk-OT7R5XTA.js +3192 -0
  29. package/dist/chunk-P7X4RA2T.js +106 -0
  30. package/dist/chunk-PIDUQNC2.js +3185 -0
  31. package/dist/chunk-POGCDIH4.js +3187 -0
  32. package/dist/chunk-PSIEOQGZ.js +3043 -0
  33. package/dist/chunk-PVRT3IHA.js +3238 -0
  34. package/dist/chunk-QNN4TT23.js +1430 -0
  35. package/dist/chunk-RE3R45RJ.js +3042 -0
  36. package/dist/chunk-S7E6TFX6.js +718 -657
  37. package/dist/chunk-SG6GLU4U.js +1378 -0
  38. package/dist/chunk-SJCDV2ST.js +274 -0
  39. package/dist/chunk-SYE5XLF3.js +104 -0
  40. package/dist/chunk-T5VLYBZD.js +103 -0
  41. package/dist/chunk-TOQB7VWU.js +3238 -0
  42. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  43. package/dist/chunk-VVTGZNBT.js +1533 -1423
  44. package/dist/chunk-W7Q4RFEV.js +104 -0
  45. package/dist/chunk-XTYYVRLO.js +3190 -0
  46. package/dist/chunk-Y6MDYVJD.js +3063 -0
  47. package/dist/cli/main.js +4072 -629
  48. package/dist/index.d.ts +420 -33
  49. package/dist/index.js +8 -15
  50. package/dist/mcp/server.js +103 -7
  51. package/dist/schema-BAWSG7KY.js +22 -0
  52. package/dist/schema-E3QUPL26.js +20 -0
  53. package/dist/schema-EHL7WUT6.js +20 -0
  54. package/docs/019-USAGE.md +44 -5
  55. package/docs/020-current-implementation.md +8 -8
  56. package/docs/021-DOGFOODING-FINDINGS.md +1 -1
  57. package/docs/CONFIG.md +1123 -0
  58. package/docs/ERRORS.md +383 -0
  59. package/docs/summarization.md +320 -0
  60. package/justfile +40 -0
  61. package/package.json +39 -33
  62. package/research/INDEX.md +315 -0
  63. package/research/code-review/README.md +90 -0
  64. package/research/code-review/cli-error-handling-review.md +979 -0
  65. package/research/code-review/code-review-validation-report.md +464 -0
  66. package/research/code-review/main-ts-review.md +1128 -0
  67. package/research/config-docs/SUMMARY.md +357 -0
  68. package/research/config-docs/TEST-RESULTS.md +776 -0
  69. package/research/config-docs/TODO.md +542 -0
  70. package/research/config-docs/analysis.md +744 -0
  71. package/research/config-docs/fix-validation.md +502 -0
  72. package/research/config-docs/help-audit.md +264 -0
  73. package/research/config-docs/help-system-analysis.md +890 -0
  74. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  75. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  76. package/research/issue-review.md +603 -0
  77. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  78. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  79. package/research/llm-summarization/anthropic-2026.md +367 -0
  80. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  81. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  82. package/research/llm-summarization/openai-2026.md +473 -0
  83. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  84. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  85. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  86. package/research/llm-summarization/prototype-results.md +56 -0
  87. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  88. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  89. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  90. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  91. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  92. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  93. package/research/mdcontext-pudding/02-search.md +970 -0
  94. package/research/mdcontext-pudding/03-context.md +779 -0
  95. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  96. package/research/mdcontext-pudding/04-tree.md +704 -0
  97. package/research/mdcontext-pudding/05-config.md +1038 -0
  98. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  99. package/research/mdcontext-pudding/06-links.md +679 -0
  100. package/research/mdcontext-pudding/07-stats.md +693 -0
  101. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  102. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  103. package/research/mdcontext-pudding/README.md +168 -0
  104. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  105. package/research/research-quality-review.md +834 -0
  106. package/research/semantic-search/embedding-text-analysis.md +156 -0
  107. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  108. package/research/semantic-search/query-processing-analysis.md +207 -0
  109. package/research/semantic-search/root-cause-and-solution.md +114 -0
  110. package/research/semantic-search/threshold-validation-report.md +69 -0
  111. package/research/semantic-search/vector-search-analysis.md +63 -0
  112. package/research/test-path-issues.md +276 -0
  113. package/review/ALP-76/1-error-type-design.md +962 -0
  114. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  115. package/review/ALP-76/3-error-presentation.md +624 -0
  116. package/review/ALP-76/4-test-coverage.md +625 -0
  117. package/review/ALP-76/5-migration-completeness.md +440 -0
  118. package/review/ALP-76/6-effect-best-practices.md +755 -0
  119. package/scripts/apply-branch-protection.sh +47 -0
  120. package/scripts/branch-protection-templates.json +79 -0
  121. package/scripts/prototype-summarization.ts +346 -0
  122. package/scripts/rebuild-hnswlib.js +32 -37
  123. package/scripts/setup-branch-protection.sh +64 -0
  124. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  125. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  126. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  127. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  128. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  129. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  130. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  131. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  132. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  133. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  134. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  135. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  136. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  137. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  138. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  139. package/src/cli/argv-preprocessor.test.ts +2 -2
  140. package/src/cli/cli.test.ts +230 -33
  141. package/src/cli/commands/config-cmd.ts +642 -0
  142. package/src/cli/commands/context.ts +97 -9
  143. package/src/cli/commands/duplicates.ts +122 -0
  144. package/src/cli/commands/embeddings.ts +529 -0
  145. package/src/cli/commands/index-cmd.ts +210 -30
  146. package/src/cli/commands/index.ts +3 -0
  147. package/src/cli/commands/search.ts +894 -64
  148. package/src/cli/commands/stats.ts +3 -0
  149. package/src/cli/commands/tree.ts +26 -5
  150. package/src/cli/config-layer.ts +176 -0
  151. package/src/cli/error-handler.test.ts +235 -0
  152. package/src/cli/error-handler.ts +655 -0
  153. package/src/cli/flag-schemas.ts +66 -0
  154. package/src/cli/help.ts +209 -7
  155. package/src/cli/main.ts +348 -58
  156. package/src/cli/options.ts +10 -0
  157. package/src/cli/shared-error-handling.ts +199 -0
  158. package/src/cli/utils.ts +150 -17
  159. package/src/config/file-provider.test.ts +320 -0
  160. package/src/config/file-provider.ts +273 -0
  161. package/src/config/index.ts +72 -0
  162. package/src/config/integration.test.ts +667 -0
  163. package/src/config/precedence.test.ts +277 -0
  164. package/src/config/precedence.ts +451 -0
  165. package/src/config/schema.test.ts +414 -0
  166. package/src/config/schema.ts +603 -0
  167. package/src/config/service.test.ts +320 -0
  168. package/src/config/service.ts +243 -0
  169. package/src/config/testing.test.ts +264 -0
  170. package/src/config/testing.ts +110 -0
  171. package/src/core/types.ts +6 -33
  172. package/src/duplicates/detector.test.ts +183 -0
  173. package/src/duplicates/detector.ts +414 -0
  174. package/src/duplicates/index.ts +18 -0
  175. package/src/embeddings/embedding-namespace.test.ts +300 -0
  176. package/src/embeddings/embedding-namespace.ts +947 -0
  177. package/src/embeddings/heading-boost.test.ts +222 -0
  178. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  179. package/src/embeddings/hyde.test.ts +272 -0
  180. package/src/embeddings/hyde.ts +264 -0
  181. package/src/embeddings/index.ts +2 -0
  182. package/src/embeddings/openai-provider.ts +332 -83
  183. package/src/embeddings/pricing.json +22 -0
  184. package/src/embeddings/provider-constants.ts +204 -0
  185. package/src/embeddings/provider-errors.test.ts +967 -0
  186. package/src/embeddings/provider-errors.ts +565 -0
  187. package/src/embeddings/provider-factory.test.ts +240 -0
  188. package/src/embeddings/provider-factory.ts +225 -0
  189. package/src/embeddings/provider-integration.test.ts +788 -0
  190. package/src/embeddings/query-preprocessing.test.ts +187 -0
  191. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  192. package/src/embeddings/semantic-search.ts +780 -93
  193. package/src/embeddings/types.ts +293 -16
  194. package/src/embeddings/vector-store.ts +486 -77
  195. package/src/embeddings/voyage-provider.ts +313 -0
  196. package/src/errors/errors.test.ts +845 -0
  197. package/src/errors/index.ts +533 -0
  198. package/src/index/ignore-patterns.test.ts +354 -0
  199. package/src/index/ignore-patterns.ts +305 -0
  200. package/src/index/indexer.ts +286 -48
  201. package/src/index/storage.ts +94 -30
  202. package/src/index/types.ts +40 -2
  203. package/src/index/watcher.ts +67 -9
  204. package/src/index.ts +22 -0
  205. package/src/integration/search-keyword.test.ts +678 -0
  206. package/src/mcp/server.ts +135 -6
  207. package/src/parser/parser.ts +18 -19
  208. package/src/parser/section-filter.test.ts +277 -0
  209. package/src/parser/section-filter.ts +125 -3
  210. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  211. package/src/search/bm25-store.ts +366 -0
  212. package/src/search/cross-encoder.test.ts +253 -0
  213. package/src/search/cross-encoder.ts +406 -0
  214. package/src/search/fuzzy-search.test.ts +419 -0
  215. package/src/search/fuzzy-search.ts +273 -0
  216. package/src/search/hybrid-search.ts +448 -0
  217. package/src/search/path-matcher.test.ts +276 -0
  218. package/src/search/path-matcher.ts +33 -0
  219. package/src/search/searcher.test.ts +99 -1
  220. package/src/search/searcher.ts +189 -67
  221. package/src/search/wink-bm25.d.ts +30 -0
  222. package/src/summarization/cli-providers/claude.ts +202 -0
  223. package/src/summarization/cli-providers/detection.test.ts +273 -0
  224. package/src/summarization/cli-providers/detection.ts +118 -0
  225. package/src/summarization/cli-providers/index.ts +8 -0
  226. package/src/summarization/cost.test.ts +139 -0
  227. package/src/summarization/cost.ts +102 -0
  228. package/src/summarization/error-handler.test.ts +127 -0
  229. package/src/summarization/error-handler.ts +111 -0
  230. package/src/summarization/index.ts +102 -0
  231. package/src/summarization/pipeline.test.ts +498 -0
  232. package/src/summarization/pipeline.ts +231 -0
  233. package/src/summarization/prompts.test.ts +269 -0
  234. package/src/summarization/prompts.ts +133 -0
  235. package/src/summarization/provider-factory.test.ts +396 -0
  236. package/src/summarization/provider-factory.ts +178 -0
  237. package/src/summarization/types.ts +184 -0
  238. package/src/summarize/summarizer.ts +104 -35
  239. package/src/types/huggingface-transformers.d.ts +66 -0
  240. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  241. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  242. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  243. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
  244. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
  245. package/tests/integration/embed-index.test.ts +712 -0
  246. package/tests/integration/search-context.test.ts +469 -0
  247. package/tests/integration/search-semantic.test.ts +522 -0
  248. package/vitest.config.ts +1 -6
  249. package/AGENTS.md +0 -46
  250. package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
  251. package/tests/fixtures/cli/.mdcontext/vectors.meta.json +0 -1264
@@ -0,0 +1,529 @@
1
+ /**
2
+ * EMBEDDINGS Command
3
+ *
4
+ * Manage embedding providers and namespaces.
5
+ * Allows listing, switching, and removing embedding indexes
6
+ * for different providers/models.
7
+ */
8
+
9
+ import * as path from 'node:path'
10
+ import * as p from '@clack/prompts'
11
+ import { Args, Command, Options } from '@effect/cli'
12
+ import { Console, Effect } from 'effect'
13
+ import {
14
+ type EmbeddingNamespace,
15
+ getActiveNamespace,
16
+ listNamespaces,
17
+ removeNamespace,
18
+ switchNamespace,
19
+ writeActiveProvider,
20
+ } from '../../embeddings/embedding-namespace.js'
21
+ import { jsonOption, prettyOption } from '../options.js'
22
+ import { formatJson } from '../utils.js'
23
+
24
+ // ============================================================================
25
+ // Helpers
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Check if we're running in an interactive TTY
30
+ */
31
+ const isInteractiveTTY = (): boolean =>
32
+ process.stdout.isTTY && process.stdin.isTTY
33
+
34
+ /**
35
+ * Format a namespace for display in the picker
36
+ */
37
+ const formatNamespaceOption = (ns: EmbeddingNamespace): string => {
38
+ const sizeMB = (ns.sizeBytes / 1024 / 1024).toFixed(1)
39
+ const active = ns.isActive ? ' ★' : ''
40
+ return `${ns.provider}/${ns.model}${active} (${ns.vectorCount} vectors, ${sizeMB}MB, $${ns.totalCost.toFixed(4)})`
41
+ }
42
+
43
+ /**
44
+ * Show interactive namespace picker using @clack/prompts
45
+ */
46
+ const showNamespacePicker = async (
47
+ namespaces: EmbeddingNamespace[],
48
+ message: string,
49
+ ): Promise<EmbeddingNamespace | null> => {
50
+ const result = await p.select({
51
+ message,
52
+ options: namespaces.map((ns) => ({
53
+ value: ns.namespace,
54
+ label: formatNamespaceOption(ns),
55
+ hint: ns.namespace,
56
+ })),
57
+ })
58
+
59
+ if (p.isCancel(result)) {
60
+ return null
61
+ }
62
+
63
+ return namespaces.find((ns) => ns.namespace === result) ?? null
64
+ }
65
+
66
+ /**
67
+ * Fuzzy match namespaces against a query
68
+ */
69
+ const fuzzyMatchNamespaces = (
70
+ namespaces: EmbeddingNamespace[],
71
+ query: string,
72
+ ): EmbeddingNamespace[] => {
73
+ const queryLower = query.toLowerCase()
74
+ return namespaces.filter(
75
+ (ns) =>
76
+ ns.namespace.toLowerCase().includes(queryLower) ||
77
+ ns.provider.toLowerCase().includes(queryLower) ||
78
+ ns.model.toLowerCase().includes(queryLower),
79
+ )
80
+ }
81
+
82
+ // ============================================================================
83
+ // List Subcommand
84
+ // ============================================================================
85
+
86
+ const listSubcommand = Command.make(
87
+ 'list',
88
+ {
89
+ path: Args.directory({ name: 'path' }).pipe(
90
+ Args.withDescription('Directory containing embeddings'),
91
+ Args.withDefault('.'),
92
+ ),
93
+ json: jsonOption,
94
+ pretty: prettyOption,
95
+ },
96
+ ({ path: dirPath, json, pretty }) =>
97
+ Effect.gen(function* () {
98
+ const resolvedDir = path.resolve(dirPath)
99
+
100
+ const namespaces = yield* listNamespaces(resolvedDir).pipe(
101
+ Effect.catchAll(() => Effect.succeed([] as EmbeddingNamespace[])),
102
+ )
103
+
104
+ if (namespaces.length === 0) {
105
+ if (json) {
106
+ yield* Console.log(formatJson({ namespaces: [] }, pretty))
107
+ } else {
108
+ yield* Console.log('No embedding namespaces found.')
109
+ yield* Console.log('')
110
+ yield* Console.log('Run: mdcontext index --embed')
111
+ }
112
+ return
113
+ }
114
+
115
+ if (json) {
116
+ yield* Console.log(formatJson({ namespaces }, pretty))
117
+ return
118
+ }
119
+
120
+ // Interactive picker when multiple namespaces exist and TTY is available
121
+ if (namespaces.length > 1 && isInteractiveTTY()) {
122
+ const selected = yield* Effect.promise(() =>
123
+ showNamespacePicker(
124
+ namespaces,
125
+ 'Select a namespace to switch to (or Ctrl+C to cancel):',
126
+ ),
127
+ )
128
+
129
+ if (selected) {
130
+ // Switch to the selected namespace
131
+ const writeResult = yield* writeActiveProvider(resolvedDir, {
132
+ namespace: selected.namespace,
133
+ provider: selected.provider,
134
+ model: selected.model,
135
+ dimensions: selected.dimensions,
136
+ activatedAt: new Date().toISOString(),
137
+ }).pipe(
138
+ Effect.map(() => ({ ok: true as const })),
139
+ Effect.catchAll((e) =>
140
+ Effect.succeed({ ok: false as const, error: e.message }),
141
+ ),
142
+ )
143
+
144
+ if (!writeResult.ok) {
145
+ yield* Console.error(
146
+ `Error: Failed to switch namespace: ${writeResult.error}`,
147
+ )
148
+ process.exit(1)
149
+ return
150
+ }
151
+
152
+ yield* Console.log('')
153
+ yield* Console.log(`Switched to: ${selected.namespace}`)
154
+ yield* Console.log(` Provider: ${selected.provider}`)
155
+ yield* Console.log(` Model: ${selected.model}`)
156
+ yield* Console.log(` Dimensions: ${selected.dimensions}`)
157
+ } else {
158
+ // User cancelled, show plain list
159
+ yield* Console.log('')
160
+ yield* Console.log('Available embedding namespaces:')
161
+ yield* Console.log('')
162
+ for (const ns of namespaces) {
163
+ const active = ns.isActive ? ' (active)' : ''
164
+ const sizeMB = (ns.sizeBytes / 1024 / 1024).toFixed(1)
165
+ yield* Console.log(` ${ns.namespace}${active}`)
166
+ yield* Console.log(` Provider: ${ns.provider}`)
167
+ yield* Console.log(` Model: ${ns.model}`)
168
+ yield* Console.log(` Dimensions: ${ns.dimensions}`)
169
+ yield* Console.log(` Vectors: ${ns.vectorCount}`)
170
+ yield* Console.log(` Size: ${sizeMB} MB`)
171
+ yield* Console.log(` Cost: $${ns.totalCost.toFixed(4)}`)
172
+ yield* Console.log('')
173
+ }
174
+ }
175
+ return
176
+ }
177
+
178
+ // Non-interactive: plain list output
179
+ yield* Console.log('Available embedding namespaces:')
180
+ yield* Console.log('')
181
+
182
+ for (const ns of namespaces) {
183
+ const active = ns.isActive ? ' (active)' : ''
184
+ const sizeMB = (ns.sizeBytes / 1024 / 1024).toFixed(1)
185
+ yield* Console.log(` ${ns.namespace}${active}`)
186
+ yield* Console.log(` Provider: ${ns.provider}`)
187
+ yield* Console.log(` Model: ${ns.model}`)
188
+ yield* Console.log(` Dimensions: ${ns.dimensions}`)
189
+ yield* Console.log(` Vectors: ${ns.vectorCount}`)
190
+ yield* Console.log(` Size: ${sizeMB} MB`)
191
+ yield* Console.log(` Cost: $${ns.totalCost.toFixed(4)}`)
192
+ yield* Console.log('')
193
+ }
194
+ }),
195
+ ).pipe(Command.withDescription('List available embedding namespaces'))
196
+
197
+ // ============================================================================
198
+ // Switch Subcommand
199
+ // ============================================================================
200
+
201
+ const switchSubcommand = Command.make(
202
+ 'switch',
203
+ {
204
+ namespace: Args.text({ name: 'namespace' }).pipe(
205
+ Args.withDescription(
206
+ 'Namespace to switch to (provider name or full namespace)',
207
+ ),
208
+ Args.optional,
209
+ ),
210
+ path: Args.directory({ name: 'path' }).pipe(
211
+ Args.withDescription('Directory containing embeddings'),
212
+ Args.withDefault('.'),
213
+ ),
214
+ json: jsonOption,
215
+ pretty: prettyOption,
216
+ },
217
+ ({ namespace: namespaceOpt, path: dirPath, json, pretty }) =>
218
+ Effect.gen(function* () {
219
+ const resolvedDir = path.resolve(dirPath)
220
+
221
+ // Get all available namespaces first
222
+ const namespaces = yield* listNamespaces(resolvedDir).pipe(
223
+ Effect.catchAll(() => Effect.succeed([] as EmbeddingNamespace[])),
224
+ )
225
+
226
+ if (namespaces.length === 0) {
227
+ if (json) {
228
+ yield* Console.log(
229
+ formatJson(
230
+ {
231
+ error:
232
+ 'No embedding namespaces found. Run "mdcontext index --embed" first.',
233
+ },
234
+ pretty,
235
+ ),
236
+ )
237
+ } else {
238
+ yield* Console.log(
239
+ 'No embedding namespaces found. Run "mdcontext index --embed" first.',
240
+ )
241
+ }
242
+ return
243
+ }
244
+
245
+ // If no namespace argument provided and we're in a TTY, show interactive picker
246
+ if (namespaceOpt._tag === 'None' && isInteractiveTTY()) {
247
+ const selected = yield* Effect.promise(() =>
248
+ showNamespacePicker(namespaces, 'Select a namespace to switch to:'),
249
+ )
250
+
251
+ if (!selected) {
252
+ yield* Console.log('Cancelled.')
253
+ return
254
+ }
255
+
256
+ // Switch to selected namespace
257
+ const writeResult = yield* writeActiveProvider(resolvedDir, {
258
+ namespace: selected.namespace,
259
+ provider: selected.provider,
260
+ model: selected.model,
261
+ dimensions: selected.dimensions,
262
+ activatedAt: new Date().toISOString(),
263
+ }).pipe(
264
+ Effect.map(() => ({ ok: true as const })),
265
+ Effect.catchAll((e) =>
266
+ Effect.succeed({ ok: false as const, error: e.message }),
267
+ ),
268
+ )
269
+
270
+ if (!writeResult.ok) {
271
+ if (json) {
272
+ yield* Console.log(formatJson({ error: writeResult.error }, pretty))
273
+ } else {
274
+ yield* Console.error(
275
+ `Error: Failed to switch namespace: ${writeResult.error}`,
276
+ )
277
+ }
278
+ process.exit(1)
279
+ return
280
+ }
281
+
282
+ if (json) {
283
+ yield* Console.log(formatJson({ switched: selected }, pretty))
284
+ } else {
285
+ yield* Console.log(`Switched to: ${selected.namespace}`)
286
+ yield* Console.log(` Provider: ${selected.provider}`)
287
+ yield* Console.log(` Model: ${selected.model}`)
288
+ yield* Console.log(` Dimensions: ${selected.dimensions}`)
289
+ }
290
+ return
291
+ }
292
+
293
+ // If no namespace argument and not a TTY, error out
294
+ if (namespaceOpt._tag === 'None') {
295
+ if (json) {
296
+ yield* Console.log(
297
+ formatJson({ error: 'Namespace argument required' }, pretty),
298
+ )
299
+ } else {
300
+ yield* Console.log(
301
+ 'Error: Namespace argument required. Available namespaces:',
302
+ )
303
+ for (const ns of namespaces) {
304
+ yield* Console.log(` ${ns.namespace}`)
305
+ }
306
+ }
307
+ process.exit(1)
308
+ return
309
+ }
310
+
311
+ const namespace = namespaceOpt.value
312
+
313
+ // Fuzzy match the namespace query
314
+ const matches = fuzzyMatchNamespaces(namespaces, namespace)
315
+
316
+ if (matches.length === 0) {
317
+ if (json) {
318
+ yield* Console.log(
319
+ formatJson(
320
+ {
321
+ error: `No namespace matching "${namespace}". Available: ${namespaces.map((n) => n.namespace).join(', ')}`,
322
+ },
323
+ pretty,
324
+ ),
325
+ )
326
+ } else {
327
+ yield* Console.log(`Error: No namespace matching "${namespace}".`)
328
+ yield* Console.log('Available namespaces:')
329
+ for (const ns of namespaces) {
330
+ yield* Console.log(` ${ns.namespace}`)
331
+ }
332
+ }
333
+ process.exit(1)
334
+ return
335
+ }
336
+
337
+ // If multiple matches and TTY available, show picker
338
+ if (matches.length > 1 && isInteractiveTTY()) {
339
+ yield* Console.log(
340
+ `Multiple namespaces match "${namespace}". Please select one:`,
341
+ )
342
+ const selected = yield* Effect.promise(() =>
343
+ showNamespacePicker(matches, 'Select a namespace:'),
344
+ )
345
+
346
+ if (!selected) {
347
+ yield* Console.log('Cancelled.')
348
+ return
349
+ }
350
+
351
+ // Switch to selected namespace
352
+ const writeResult = yield* writeActiveProvider(resolvedDir, {
353
+ namespace: selected.namespace,
354
+ provider: selected.provider,
355
+ model: selected.model,
356
+ dimensions: selected.dimensions,
357
+ activatedAt: new Date().toISOString(),
358
+ }).pipe(
359
+ Effect.map(() => ({ ok: true as const })),
360
+ Effect.catchAll((e) =>
361
+ Effect.succeed({ ok: false as const, error: e.message }),
362
+ ),
363
+ )
364
+
365
+ if (!writeResult.ok) {
366
+ if (json) {
367
+ yield* Console.log(formatJson({ error: writeResult.error }, pretty))
368
+ } else {
369
+ yield* Console.error(
370
+ `Error: Failed to switch namespace: ${writeResult.error}`,
371
+ )
372
+ }
373
+ process.exit(1)
374
+ return
375
+ }
376
+
377
+ if (json) {
378
+ yield* Console.log(formatJson({ switched: selected }, pretty))
379
+ } else {
380
+ yield* Console.log(`Switched to: ${selected.namespace}`)
381
+ yield* Console.log(` Provider: ${selected.provider}`)
382
+ yield* Console.log(` Model: ${selected.model}`)
383
+ yield* Console.log(` Dimensions: ${selected.dimensions}`)
384
+ }
385
+ return
386
+ }
387
+
388
+ // Multiple matches but not a TTY - use the existing switchNamespace logic
389
+ const result = yield* switchNamespace(resolvedDir, namespace).pipe(
390
+ Effect.catchAll((e) => {
391
+ if (json) {
392
+ console.log(formatJson({ error: e.message }, pretty))
393
+ } else {
394
+ console.error(`Error: ${e.message}`)
395
+ }
396
+ process.exit(1)
397
+ return Effect.succeed(null)
398
+ }),
399
+ )
400
+
401
+ if (!result) {
402
+ return
403
+ }
404
+
405
+ if (json) {
406
+ yield* Console.log(formatJson({ switched: result }, pretty))
407
+ } else {
408
+ yield* Console.log(`Switched to: ${result.namespace}`)
409
+ yield* Console.log(` Provider: ${result.provider}`)
410
+ yield* Console.log(` Model: ${result.model}`)
411
+ yield* Console.log(` Dimensions: ${result.dimensions}`)
412
+ }
413
+ }),
414
+ ).pipe(Command.withDescription('Switch to a different embedding namespace'))
415
+
416
+ // ============================================================================
417
+ // Remove Subcommand
418
+ // ============================================================================
419
+
420
+ const removeSubcommand = Command.make(
421
+ 'remove',
422
+ {
423
+ namespace: Args.text({ name: 'namespace' }).pipe(
424
+ Args.withDescription('Namespace to remove'),
425
+ ),
426
+ path: Args.directory({ name: 'path' }).pipe(
427
+ Args.withDescription('Directory containing embeddings'),
428
+ Args.withDefault('.'),
429
+ ),
430
+ force: Options.boolean('force').pipe(
431
+ Options.withAlias('f'),
432
+ Options.withDescription('Remove even if this is the active namespace'),
433
+ Options.withDefault(false),
434
+ ),
435
+ json: jsonOption,
436
+ pretty: prettyOption,
437
+ },
438
+ ({ namespace, path: dirPath, force, json, pretty }) =>
439
+ Effect.gen(function* () {
440
+ const resolvedDir = path.resolve(dirPath)
441
+
442
+ const result = yield* removeNamespace(resolvedDir, namespace, {
443
+ force,
444
+ }).pipe(
445
+ Effect.catchAll((e) => {
446
+ if (json) {
447
+ console.log(formatJson({ error: e.message }, pretty))
448
+ } else {
449
+ console.error(`Error: ${e.message}`)
450
+ }
451
+ process.exit(1)
452
+ return Effect.succeed(null)
453
+ }),
454
+ )
455
+
456
+ if (!result) {
457
+ return
458
+ }
459
+
460
+ if (json) {
461
+ yield* Console.log(formatJson({ removed: result }, pretty))
462
+ } else {
463
+ yield* Console.log(`Removed namespace: ${result.removed}`)
464
+ if (result.wasActive) {
465
+ yield* Console.log(
466
+ 'Note: This was the active namespace. Run "mdcontext embeddings list" to see remaining namespaces.',
467
+ )
468
+ }
469
+ }
470
+ }),
471
+ ).pipe(Command.withDescription('Remove an embedding namespace'))
472
+
473
+ // ============================================================================
474
+ // Current Subcommand
475
+ // ============================================================================
476
+
477
+ const currentSubcommand = Command.make(
478
+ 'current',
479
+ {
480
+ path: Args.directory({ name: 'path' }).pipe(
481
+ Args.withDescription('Directory containing embeddings'),
482
+ Args.withDefault('.'),
483
+ ),
484
+ json: jsonOption,
485
+ pretty: prettyOption,
486
+ },
487
+ ({ path: dirPath, json, pretty }) =>
488
+ Effect.gen(function* () {
489
+ const resolvedDir = path.resolve(dirPath)
490
+
491
+ const active = yield* getActiveNamespace(resolvedDir).pipe(
492
+ Effect.catchAll(() => Effect.succeed(null)),
493
+ )
494
+
495
+ if (!active) {
496
+ if (json) {
497
+ yield* Console.log(formatJson({ active: null }, pretty))
498
+ } else {
499
+ yield* Console.log('No active embedding namespace.')
500
+ yield* Console.log('')
501
+ yield* Console.log('Run: mdcontext index --embed')
502
+ }
503
+ return
504
+ }
505
+
506
+ if (json) {
507
+ yield* Console.log(formatJson({ active }, pretty))
508
+ } else {
509
+ yield* Console.log(`Active namespace: ${active.namespace}`)
510
+ yield* Console.log(` Provider: ${active.provider}`)
511
+ yield* Console.log(` Model: ${active.model}`)
512
+ yield* Console.log(` Dimensions: ${active.dimensions}`)
513
+ }
514
+ }),
515
+ ).pipe(Command.withDescription('Show the current active embedding namespace'))
516
+
517
+ // ============================================================================
518
+ // Main Command
519
+ // ============================================================================
520
+
521
+ export const embeddingsCommand = Command.make('embeddings', {}).pipe(
522
+ Command.withDescription('Manage embedding namespaces'),
523
+ Command.withSubcommands([
524
+ listSubcommand,
525
+ switchSubcommand,
526
+ removeSubcommand,
527
+ currentSubcommand,
528
+ ]),
529
+ )