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
@@ -7,10 +7,16 @@
7
7
  import * as path from 'node:path'
8
8
  import { Args, Command, Options } from '@effect/cli'
9
9
  import { Console, Effect } from 'effect'
10
+ import {
11
+ CliValidationError,
12
+ FileReadError,
13
+ ParseError,
14
+ } from '../../errors/index.js'
10
15
  import { parseFile } from '../../parser/parser.js'
11
16
  import {
12
17
  buildSectionList,
13
18
  extractSectionContent,
19
+ filterExcludedSections,
14
20
  formatExtractedSections,
15
21
  formatSectionList,
16
22
  } from '../../parser/section-filter.js'
@@ -58,19 +64,39 @@ export const contextCommand = Command.make(
58
64
  Options.withDescription('Exclude nested subsections when filtering'),
59
65
  Options.withDefault(false),
60
66
  ),
67
+ exclude: Options.text('exclude').pipe(
68
+ Options.withAlias('x'),
69
+ Options.withDescription(
70
+ 'Exclude sections matching pattern (can be used multiple times)',
71
+ ),
72
+ Options.repeated,
73
+ ),
61
74
  json: jsonOption,
62
75
  pretty: prettyOption,
63
76
  },
64
- ({ files, tokens, brief, full, section, sections, shallow, json, pretty }) =>
77
+ ({
78
+ files,
79
+ tokens,
80
+ brief,
81
+ full,
82
+ section,
83
+ sections,
84
+ shallow,
85
+ exclude,
86
+ json,
87
+ pretty,
88
+ }) =>
65
89
  Effect.gen(function* () {
66
90
  // Effect CLI Args.repeated returns an array
67
91
  const fileList: string[] = Array.isArray(files) ? files : []
68
92
 
69
93
  if (fileList.length === 0) {
70
94
  yield* Effect.fail(
71
- new Error(
72
- 'At least one file is required. Usage: mdcontext context <file> [files...]',
73
- ),
95
+ new CliValidationError({
96
+ message:
97
+ 'At least one file is required. Usage: mdcontext context <file> [files...]',
98
+ argument: 'files',
99
+ }),
74
100
  )
75
101
  }
76
102
 
@@ -78,20 +104,43 @@ export const contextCommand = Command.make(
78
104
  const sectionSelector =
79
105
  section._tag === 'Some' ? section.value : undefined
80
106
 
107
+ // Extract exclusion patterns (repeated option returns array)
108
+ const excludePatterns: string[] = Array.isArray(exclude) ? exclude : []
109
+
81
110
  // Handle --sections flag: list available sections
82
111
  if (sections) {
83
112
  for (const file of fileList) {
84
113
  const filePath = path.resolve(file)
85
114
  const document = yield* parseFile(filePath).pipe(
86
- Effect.mapError((e) => new Error(`${e._tag}: ${e.message}`)),
115
+ Effect.mapError((e) =>
116
+ e._tag === 'ParseError'
117
+ ? new ParseError({
118
+ message: e.message,
119
+ path: filePath,
120
+ ...(e.line !== undefined && { line: e.line }),
121
+ ...(e.column !== undefined && { column: e.column }),
122
+ })
123
+ : new FileReadError({
124
+ path: e.path,
125
+ message: e.message,
126
+ }),
127
+ ),
87
128
  )
88
129
 
89
- const sectionList = buildSectionList(document)
130
+ let sectionList = buildSectionList(document)
131
+
132
+ // Apply exclusion filter if patterns provided
133
+ if (excludePatterns.length > 0) {
134
+ sectionList = filterExcludedSections(sectionList, excludePatterns)
135
+ }
90
136
 
91
137
  if (json) {
92
138
  const output = {
93
139
  path: filePath,
94
140
  title: document.title,
141
+ ...(excludePatterns.length > 0 && {
142
+ excludePatterns,
143
+ }),
95
144
  sections: sectionList.map((s) => ({
96
145
  number: s.number,
97
146
  heading: s.heading,
@@ -103,6 +152,11 @@ export const contextCommand = Command.make(
103
152
  } else {
104
153
  yield* Console.log(`# ${document.title}`)
105
154
  yield* Console.log(`Path: ${filePath}`)
155
+ if (excludePatterns.length > 0) {
156
+ yield* Console.log(
157
+ `Excluded: ${excludePatterns.map((p) => `"${p}"`).join(', ')}`,
158
+ )
159
+ }
106
160
  yield* Console.log('')
107
161
  yield* Console.log('Available sections:')
108
162
  yield* Console.log(formatSectionList(sectionList))
@@ -116,16 +170,39 @@ export const contextCommand = Command.make(
116
170
  for (const file of fileList) {
117
171
  const filePath = path.resolve(file)
118
172
  const document = yield* parseFile(filePath).pipe(
119
- Effect.mapError((e) => new Error(`${e._tag}: ${e.message}`)),
173
+ Effect.mapError((e) =>
174
+ e._tag === 'ParseError'
175
+ ? new ParseError({
176
+ message: e.message,
177
+ path: filePath,
178
+ ...(e.line !== undefined && { line: e.line }),
179
+ ...(e.column !== undefined && { column: e.column }),
180
+ })
181
+ : new FileReadError({
182
+ path: e.path,
183
+ message: e.message,
184
+ }),
185
+ ),
120
186
  )
121
187
 
122
- const { sections: extractedSections, matchedNumbers } =
123
- extractSectionContent(document, sectionSelector, { shallow })
188
+ const {
189
+ sections: extractedSections,
190
+ matchedNumbers,
191
+ excludedNumbers,
192
+ } = extractSectionContent(document, sectionSelector, {
193
+ shallow,
194
+ exclude: excludePatterns,
195
+ })
124
196
 
125
197
  if (extractedSections.length === 0) {
126
198
  yield* Console.error(
127
199
  `No sections found matching "${sectionSelector}" in ${file}`,
128
200
  )
201
+ if (excludedNumbers.length > 0) {
202
+ yield* Console.error(
203
+ `(${excludedNumbers.length} section(s) were excluded by --exclude patterns)`,
204
+ )
205
+ }
129
206
  yield* Console.error('Use --sections to list available sections.')
130
207
  continue
131
208
  }
@@ -136,7 +213,11 @@ export const contextCommand = Command.make(
136
213
  title: document.title,
137
214
  selector: sectionSelector,
138
215
  shallow,
216
+ ...(excludePatterns.length > 0 && { excludePatterns }),
139
217
  matchedSections: matchedNumbers,
218
+ ...(excludedNumbers.length > 0 && {
219
+ excludedSections: excludedNumbers,
220
+ }),
140
221
  content: formatExtractedSections(extractedSections),
141
222
  sections: extractedSections.map((s) => ({
142
223
  heading: s.heading,
@@ -149,6 +230,11 @@ export const contextCommand = Command.make(
149
230
  yield* Console.log(`# ${document.title}`)
150
231
  yield* Console.log(`Path: ${filePath}`)
151
232
  yield* Console.log(`Sections: ${matchedNumbers.join(', ')}`)
233
+ if (excludedNumbers.length > 0) {
234
+ yield* Console.log(
235
+ `Excluded: ${excludedNumbers.join(', ')} (by --exclude patterns)`,
236
+ )
237
+ }
152
238
  yield* Console.log('')
153
239
  yield* Console.log(formatExtractedSections(extractedSections))
154
240
  }
@@ -169,6 +255,7 @@ export const contextCommand = Command.make(
169
255
  const summary = yield* summarizeFile(filePath, {
170
256
  level: level as 'brief' | 'summary' | 'full',
171
257
  maxTokens: effectiveMaxTokens,
258
+ exclude: excludePatterns.length > 0 ? excludePatterns : undefined,
172
259
  })
173
260
 
174
261
  if (json) {
@@ -185,6 +272,7 @@ export const contextCommand = Command.make(
185
272
  const assembled = yield* assembleContext(root, fileList, {
186
273
  budget: full ? Number.MAX_SAFE_INTEGER : tokens,
187
274
  level: level as 'brief' | 'summary' | 'full',
275
+ exclude: excludePatterns.length > 0 ? excludePatterns : undefined,
188
276
  })
189
277
 
190
278
  if (json) {
@@ -0,0 +1,122 @@
1
+ /**
2
+ * DUPLICATES Command
3
+ *
4
+ * Detect and display duplicate content in markdown files.
5
+ */
6
+
7
+ import * as path from 'node:path'
8
+ import { Args, Command, Options } from '@effect/cli'
9
+ import { Console, Effect, Option } from 'effect'
10
+ import { detectDuplicates } from '../../duplicates/index.js'
11
+ import { jsonOption, prettyOption } from '../options.js'
12
+ import { formatJson, getIndexInfo } from '../utils.js'
13
+
14
+ export const duplicatesCommand = Command.make(
15
+ 'duplicates',
16
+ {
17
+ path: Args.directory({ name: 'path' }).pipe(
18
+ Args.withDescription('Directory to search for duplicates'),
19
+ Args.withDefault('.'),
20
+ ),
21
+ minLength: Options.integer('min-length').pipe(
22
+ Options.withDescription(
23
+ 'Minimum content length (characters) to consider for duplicate detection',
24
+ ),
25
+ Options.withDefault(50),
26
+ ),
27
+ pathPattern: Options.text('path').pipe(
28
+ Options.withAlias('p'),
29
+ Options.withDescription('Filter by document path pattern (glob)'),
30
+ Options.optional,
31
+ ),
32
+ json: jsonOption,
33
+ pretty: prettyOption,
34
+ },
35
+ ({ path: dirPath, minLength, pathPattern, json, pretty }) =>
36
+ Effect.gen(function* () {
37
+ const resolvedDir = path.resolve(dirPath)
38
+
39
+ // Check for index
40
+ const indexInfo = yield* Effect.promise(() => getIndexInfo(resolvedDir))
41
+
42
+ if (!indexInfo.exists && !json) {
43
+ yield* Console.log('No index found.')
44
+ yield* Console.log('')
45
+ yield* Console.log('Run: mdcontext index /path/to/docs')
46
+ return
47
+ }
48
+
49
+ // Determine the actual index root
50
+ const indexRoot = indexInfo.indexRoot ?? resolvedDir
51
+
52
+ // Run duplicate detection
53
+ const result = yield* detectDuplicates(indexRoot, {
54
+ minContentLength: minLength,
55
+ pathPattern: Option.getOrUndefined(pathPattern),
56
+ })
57
+
58
+ if (json) {
59
+ const output = {
60
+ sectionsAnalyzed: result.sectionsAnalyzed,
61
+ duplicatePairs: result.duplicatePairs,
62
+ sectionsWithDuplicates: result.sectionsWithDuplicates,
63
+ groupCount: result.groups.length,
64
+ groups: result.groups.map((g) => ({
65
+ primary: {
66
+ path: g.primary.documentPath,
67
+ heading: g.primary.heading,
68
+ line: g.primary.startLine,
69
+ },
70
+ duplicates: g.duplicates.map((d) => ({
71
+ path: d.documentPath,
72
+ heading: d.heading,
73
+ line: d.startLine,
74
+ })),
75
+ method: g.method,
76
+ similarity: g.similarity,
77
+ })),
78
+ }
79
+ yield* Console.log(formatJson(output, pretty))
80
+ } else {
81
+ yield* Console.log('Duplicate Content Analysis')
82
+ yield* Console.log('')
83
+ yield* Console.log(` Sections analyzed: ${result.sectionsAnalyzed}`)
84
+ yield* Console.log(` Duplicate groups: ${result.groups.length}`)
85
+ yield* Console.log(` Duplicate pairs: ${result.duplicatePairs}`)
86
+ yield* Console.log(
87
+ ` Sections involved: ${result.sectionsWithDuplicates}`,
88
+ )
89
+ yield* Console.log('')
90
+
91
+ if (result.groups.length === 0) {
92
+ yield* Console.log('No duplicates found.')
93
+ } else {
94
+ yield* Console.log('Duplicate Groups:')
95
+ yield* Console.log('')
96
+
97
+ for (let i = 0; i < result.groups.length; i++) {
98
+ const group = result.groups[i]!
99
+ const methodBadge =
100
+ group.method === 'exact'
101
+ ? '[exact]'
102
+ : `[~${Math.round(group.similarity * 100)}%]`
103
+
104
+ yield* Console.log(` Group ${i + 1} ${methodBadge}`)
105
+ yield* Console.log(
106
+ ` ${group.primary.documentPath}:${group.primary.startLine}`,
107
+ )
108
+ yield* Console.log(` ${group.primary.heading}`)
109
+
110
+ for (const dup of group.duplicates) {
111
+ yield* Console.log('')
112
+ yield* Console.log(
113
+ ` Also in: ${dup.documentPath}:${dup.startLine}`,
114
+ )
115
+ yield* Console.log(` ${dup.heading}`)
116
+ }
117
+ yield* Console.log('')
118
+ }
119
+ }
120
+ }
121
+ }),
122
+ ).pipe(Command.withDescription('Detect duplicate content in markdown files'))