mdcontext 0.0.1 → 0.1.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 (140) hide show
  1. package/.changeset/README.md +28 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/ci.yml +83 -0
  4. package/.github/workflows/release.yml +113 -0
  5. package/.tldrignore +112 -0
  6. package/AGENTS.md +46 -0
  7. package/BACKLOG.md +338 -0
  8. package/README.md +231 -11
  9. package/biome.json +36 -0
  10. package/cspell.config.yaml +14 -0
  11. package/dist/chunk-KRYIFLQR.js +92 -0
  12. package/dist/chunk-S7E6TFX6.js +742 -0
  13. package/dist/chunk-VVTGZNBT.js +1519 -0
  14. package/dist/cli/main.d.ts +1 -0
  15. package/dist/cli/main.js +2015 -0
  16. package/dist/index.d.ts +266 -0
  17. package/dist/index.js +86 -0
  18. package/dist/mcp/server.d.ts +1 -0
  19. package/dist/mcp/server.js +376 -0
  20. package/docs/019-USAGE.md +586 -0
  21. package/docs/020-current-implementation.md +364 -0
  22. package/docs/021-DOGFOODING-FINDINGS.md +175 -0
  23. package/docs/BACKLOG.md +80 -0
  24. package/docs/DESIGN.md +439 -0
  25. package/docs/PROJECT.md +88 -0
  26. package/docs/ROADMAP.md +407 -0
  27. package/docs/test-links.md +9 -0
  28. package/package.json +69 -10
  29. package/pnpm-workspace.yaml +5 -0
  30. package/research/config-analysis/01-current-implementation.md +470 -0
  31. package/research/config-analysis/02-strategy-recommendation.md +428 -0
  32. package/research/config-analysis/03-task-candidates.md +715 -0
  33. package/research/config-analysis/033-research-configuration-management.md +828 -0
  34. package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
  35. package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
  36. package/research/dogfood/consolidated-tool-evaluation.md +373 -0
  37. package/research/dogfood/strategy-a/a-synthesis.md +184 -0
  38. package/research/dogfood/strategy-a/a1-docs.md +226 -0
  39. package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
  40. package/research/dogfood/strategy-a/a3-llm.md +164 -0
  41. package/research/dogfood/strategy-b/b-synthesis.md +228 -0
  42. package/research/dogfood/strategy-b/b1-architecture.md +207 -0
  43. package/research/dogfood/strategy-b/b2-gaps.md +258 -0
  44. package/research/dogfood/strategy-b/b3-workflows.md +250 -0
  45. package/research/dogfood/strategy-c/c-synthesis.md +451 -0
  46. package/research/dogfood/strategy-c/c1-explorer.md +192 -0
  47. package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
  48. package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
  49. package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
  50. package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
  51. package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
  52. package/research/effect-cli-error-handling.md +845 -0
  53. package/research/effect-errors-as-values.md +943 -0
  54. package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
  55. package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
  56. package/research/errors-task-analysis/embeddings-analysis.md +709 -0
  57. package/research/errors-task-analysis/index-search-analysis.md +812 -0
  58. package/research/mdcontext-error-analysis.md +521 -0
  59. package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
  60. package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
  61. package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
  62. package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
  63. package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
  64. package/research/semantic-search/002-research-embedding-models.md +490 -0
  65. package/research/semantic-search/003-research-rag-alternatives.md +523 -0
  66. package/research/semantic-search/004-research-vector-search.md +841 -0
  67. package/research/semantic-search/032-research-semantic-search.md +427 -0
  68. package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
  69. package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
  70. package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
  71. package/research/task-management-2026/03-lightweight-file-based.md +567 -0
  72. package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
  73. package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
  74. package/research/task-management-2026/linear/02-api-integrations.md +930 -0
  75. package/research/task-management-2026/linear/03-ai-features.md +368 -0
  76. package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
  77. package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
  78. package/scripts/rebuild-hnswlib.js +63 -0
  79. package/src/cli/argv-preprocessor.test.ts +210 -0
  80. package/src/cli/argv-preprocessor.ts +202 -0
  81. package/src/cli/cli.test.ts +430 -0
  82. package/src/cli/commands/backlinks.ts +54 -0
  83. package/src/cli/commands/context.ts +197 -0
  84. package/src/cli/commands/index-cmd.ts +300 -0
  85. package/src/cli/commands/index.ts +13 -0
  86. package/src/cli/commands/links.ts +52 -0
  87. package/src/cli/commands/search.ts +451 -0
  88. package/src/cli/commands/stats.ts +146 -0
  89. package/src/cli/commands/tree.ts +107 -0
  90. package/src/cli/flag-schemas.ts +275 -0
  91. package/src/cli/help.ts +386 -0
  92. package/src/cli/index.ts +9 -0
  93. package/src/cli/main.ts +145 -0
  94. package/src/cli/options.ts +31 -0
  95. package/src/cli/typo-suggester.test.ts +105 -0
  96. package/src/cli/typo-suggester.ts +130 -0
  97. package/src/cli/utils.ts +126 -0
  98. package/src/core/index.ts +1 -0
  99. package/src/core/types.ts +140 -0
  100. package/src/embeddings/index.ts +8 -0
  101. package/src/embeddings/openai-provider.ts +165 -0
  102. package/src/embeddings/semantic-search.ts +583 -0
  103. package/src/embeddings/types.ts +82 -0
  104. package/src/embeddings/vector-store.ts +299 -0
  105. package/src/index/index.ts +4 -0
  106. package/src/index/indexer.ts +446 -0
  107. package/src/index/storage.ts +196 -0
  108. package/src/index/types.ts +109 -0
  109. package/src/index/watcher.ts +131 -0
  110. package/src/index.ts +8 -0
  111. package/src/mcp/server.ts +483 -0
  112. package/src/parser/index.ts +1 -0
  113. package/src/parser/parser.test.ts +291 -0
  114. package/src/parser/parser.ts +395 -0
  115. package/src/parser/section-filter.ts +270 -0
  116. package/src/search/query-parser.test.ts +260 -0
  117. package/src/search/query-parser.ts +319 -0
  118. package/src/search/searcher.test.ts +182 -0
  119. package/src/search/searcher.ts +602 -0
  120. package/src/summarize/budget-bugs.test.ts +620 -0
  121. package/src/summarize/formatters.ts +419 -0
  122. package/src/summarize/index.ts +20 -0
  123. package/src/summarize/summarizer.test.ts +275 -0
  124. package/src/summarize/summarizer.ts +528 -0
  125. package/src/summarize/verify-bugs.test.ts +238 -0
  126. package/src/utils/index.ts +1 -0
  127. package/src/utils/tokens.test.ts +142 -0
  128. package/src/utils/tokens.ts +186 -0
  129. package/tests/fixtures/cli/.mdcontext/config.json +8 -0
  130. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
  131. package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
  132. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +233 -0
  133. package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
  134. package/tests/fixtures/cli/.mdcontext/vectors.meta.json +1264 -0
  135. package/tests/fixtures/cli/README.md +9 -0
  136. package/tests/fixtures/cli/api-reference.md +11 -0
  137. package/tests/fixtures/cli/getting-started.md +11 -0
  138. package/tsconfig.json +26 -0
  139. package/vitest.config.ts +21 -0
  140. package/vitest.setup.ts +12 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mdcontext CLI - Token-efficient markdown analysis
5
+ *
6
+ * CORE COMMANDS
7
+ * mdcontext index [path] Index markdown files (default: .)
8
+ * mdcontext search <query> [path] Search by meaning or structure
9
+ * mdcontext context <files...> Get LLM-ready summary
10
+ * mdcontext tree [path|file] Show files or document outline
11
+ *
12
+ * LINK ANALYSIS
13
+ * mdcontext links <file> What does this link to?
14
+ * mdcontext backlinks <file> What links to this?
15
+ *
16
+ * INSPECTION
17
+ * mdcontext stats [path] Index statistics
18
+ */
19
+
20
+ import { CliConfig, Command } from '@effect/cli'
21
+ import { NodeContext, NodeRuntime } from '@effect/platform-node'
22
+ import { Effect, Layer } from 'effect'
23
+ import { preprocessArgv } from './argv-preprocessor.js'
24
+ import {
25
+ backlinksCommand,
26
+ contextCommand,
27
+ indexCommand,
28
+ linksCommand,
29
+ searchCommand,
30
+ statsCommand,
31
+ treeCommand,
32
+ } from './commands/index.js'
33
+ import {
34
+ checkSubcommandHelp,
35
+ shouldShowMainHelp,
36
+ showMainHelp,
37
+ } from './help.js'
38
+
39
+ // ============================================================================
40
+ // Main CLI
41
+ // ============================================================================
42
+
43
+ const mainCommand = Command.make('mdcontext').pipe(
44
+ Command.withDescription('Token-efficient markdown analysis for LLMs'),
45
+ Command.withSubcommands([
46
+ indexCommand,
47
+ searchCommand,
48
+ contextCommand,
49
+ treeCommand,
50
+ linksCommand,
51
+ backlinksCommand,
52
+ statsCommand,
53
+ ]),
54
+ )
55
+
56
+ const cli = Command.run(mainCommand, {
57
+ name: 'mdcontext',
58
+ version: '0.1.0',
59
+ })
60
+
61
+ // Clean CLI config: hide built-in options from help
62
+ const cliConfigLayer = CliConfig.layer({
63
+ showBuiltIns: false,
64
+ })
65
+
66
+ // ============================================================================
67
+ // Error Handling
68
+ // ============================================================================
69
+
70
+ // Custom error formatter
71
+ const formatCliError = (error: unknown): string => {
72
+ if (error && typeof error === 'object') {
73
+ // Handle Effect CLI validation errors
74
+ const err = error as Record<string, unknown>
75
+ if (err._tag === 'ValidationError' && err.error) {
76
+ const validationError = err.error as Record<string, unknown>
77
+ // Extract the actual error message
78
+ if (validationError._tag === 'Paragraph' && validationError.value) {
79
+ const paragraph = validationError.value as Record<string, unknown>
80
+ if (paragraph._tag === 'Text' && typeof paragraph.value === 'string') {
81
+ return paragraph.value
82
+ }
83
+ }
84
+ }
85
+ // Handle MissingValue errors
86
+ if (err._tag === 'MissingValue' && err.error) {
87
+ const missingError = err.error as Record<string, unknown>
88
+ if (missingError._tag === 'Paragraph' && missingError.value) {
89
+ const paragraph = missingError.value as Record<string, unknown>
90
+ if (paragraph._tag === 'Text' && typeof paragraph.value === 'string') {
91
+ return paragraph.value
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return String(error)
97
+ }
98
+
99
+ // Check if error is a CLI validation error (should show friendly message)
100
+ const isValidationError = (error: unknown): boolean => {
101
+ if (error && typeof error === 'object') {
102
+ const err = error as Record<string, unknown>
103
+ return (
104
+ err._tag === 'ValidationError' ||
105
+ err._tag === 'MissingValue' ||
106
+ err._tag === 'InvalidValue'
107
+ )
108
+ }
109
+ return false
110
+ }
111
+
112
+ // ============================================================================
113
+ // Custom Help Handling
114
+ // ============================================================================
115
+
116
+ // Check for subcommand help before anything else
117
+ checkSubcommandHelp()
118
+
119
+ // Check if we should show main help
120
+ if (shouldShowMainHelp()) {
121
+ showMainHelp()
122
+ process.exit(0)
123
+ }
124
+
125
+ // Preprocess argv to allow flexible flag positioning
126
+ const processedArgv = preprocessArgv(process.argv)
127
+
128
+ // Run with clean config and friendly errors
129
+ Effect.suspend(() => cli(processedArgv)).pipe(
130
+ Effect.provide(Layer.merge(NodeContext.layer, cliConfigLayer)),
131
+ Effect.catchAll((error) =>
132
+ Effect.sync(() => {
133
+ // Only show friendly error for validation errors
134
+ if (isValidationError(error)) {
135
+ const message = formatCliError(error)
136
+ console.error(`\nError: ${message}`)
137
+ console.error('\nRun "mdcontext --help" for usage information.')
138
+ process.exit(1)
139
+ }
140
+ // Re-throw other errors to be handled normally
141
+ throw error
142
+ }),
143
+ ),
144
+ NodeRuntime.runMain,
145
+ )
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared CLI Options
3
+ *
4
+ * Common options used across multiple commands.
5
+ */
6
+
7
+ import { Options } from '@effect/cli'
8
+
9
+ /**
10
+ * Output as JSON
11
+ */
12
+ export const jsonOption = Options.boolean('json').pipe(
13
+ Options.withDescription('Output as JSON'),
14
+ Options.withDefault(false),
15
+ )
16
+
17
+ /**
18
+ * Pretty-print JSON output
19
+ */
20
+ export const prettyOption = Options.boolean('pretty').pipe(
21
+ Options.withDescription('Pretty-print JSON output'),
22
+ Options.withDefault(true),
23
+ )
24
+
25
+ /**
26
+ * Force full rebuild
27
+ */
28
+ export const forceOption = Options.boolean('force').pipe(
29
+ Options.withDescription('Force full rebuild, ignoring cache'),
30
+ Options.withDefault(false),
31
+ )
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Unit tests for typo-suggester
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+ import type { CommandSchema } from './flag-schemas.js'
7
+ import {
8
+ formatValidFlags,
9
+ levenshteinDistance,
10
+ suggestFlag,
11
+ } from './typo-suggester.js'
12
+
13
+ describe('levenshteinDistance', () => {
14
+ it('returns 0 for identical strings', () => {
15
+ expect(levenshteinDistance('test', 'test')).toBe(0)
16
+ })
17
+
18
+ it('returns string length for empty comparison', () => {
19
+ expect(levenshteinDistance('test', '')).toBe(4)
20
+ expect(levenshteinDistance('', 'test')).toBe(4)
21
+ })
22
+
23
+ it('calculates single character difference', () => {
24
+ expect(levenshteinDistance('test', 'tset')).toBe(2) // transposition
25
+ expect(levenshteinDistance('test', 'tests')).toBe(1) // insertion
26
+ expect(levenshteinDistance('test', 'tes')).toBe(1) // deletion
27
+ expect(levenshteinDistance('test', 'tast')).toBe(1) // substitution
28
+ })
29
+
30
+ it('calculates distance for common typos', () => {
31
+ expect(levenshteinDistance('json', 'jsno')).toBe(2)
32
+ expect(levenshteinDistance('limit', 'limt')).toBe(1)
33
+ expect(levenshteinDistance('tokens', 'toekns')).toBe(2)
34
+ })
35
+ })
36
+
37
+ describe('suggestFlag', () => {
38
+ const mockSchema: CommandSchema = {
39
+ name: 'test',
40
+ flags: [
41
+ { name: 'json', type: 'boolean', description: 'Output JSON' },
42
+ { name: 'limit', type: 'string', alias: 'n', description: 'Max results' },
43
+ { name: 'threshold', type: 'string', description: 'Threshold' },
44
+ ],
45
+ }
46
+
47
+ it('suggests correct flag for typo', () => {
48
+ const result = suggestFlag('--jsno', mockSchema)
49
+ expect(result).toBeDefined()
50
+ expect(result?.flag).toBe('--json')
51
+ })
52
+
53
+ it('suggests correct flag for missing letter', () => {
54
+ const result = suggestFlag('--limt', mockSchema)
55
+ expect(result).toBeDefined()
56
+ expect(result?.flag).toBe('--limit')
57
+ })
58
+
59
+ it('returns undefined for no close match', () => {
60
+ const result = suggestFlag('--foobar', mockSchema)
61
+ expect(result).toBeUndefined()
62
+ })
63
+
64
+ it('handles prefix matches', () => {
65
+ const result = suggestFlag('--js', mockSchema)
66
+ expect(result).toBeDefined()
67
+ expect(result?.flag).toBe('--json')
68
+ })
69
+
70
+ it('handles short flag typos', () => {
71
+ const result = suggestFlag('-m', mockSchema)
72
+ expect(result).toBeDefined()
73
+ expect(result?.flag).toBe('--limit') // -m is close to -n
74
+ })
75
+
76
+ it('respects maxDistance parameter', () => {
77
+ const result = suggestFlag('--jsno', mockSchema, 1)
78
+ expect(result).toBeUndefined() // distance is 2, exceeds max
79
+ })
80
+ })
81
+
82
+ describe('formatValidFlags', () => {
83
+ const mockSchema: CommandSchema = {
84
+ name: 'test',
85
+ flags: [
86
+ { name: 'json', type: 'boolean', description: 'Output JSON' },
87
+ { name: 'limit', type: 'string', alias: 'n', description: 'Max results' },
88
+ { name: 'threshold', type: 'string' },
89
+ ],
90
+ }
91
+
92
+ it('formats flags with descriptions', () => {
93
+ const output = formatValidFlags(mockSchema)
94
+ expect(output).toContain('--json')
95
+ expect(output).toContain('Output JSON')
96
+ expect(output).toContain('--limit, -n')
97
+ expect(output).toContain('Max results')
98
+ expect(output).toContain('--threshold')
99
+ })
100
+
101
+ it('includes alias when present', () => {
102
+ const output = formatValidFlags(mockSchema)
103
+ expect(output).toContain(', -n')
104
+ })
105
+ })
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Typo Suggester
3
+ *
4
+ * Uses Levenshtein distance to suggest correct flags when users mistype.
5
+ */
6
+
7
+ import type { CommandSchema } from './flag-schemas.js'
8
+
9
+ /**
10
+ * Calculate Levenshtein distance between two strings
11
+ */
12
+ export const levenshteinDistance = (a: string, b: string): number => {
13
+ const matrix: number[][] = []
14
+
15
+ // Initialize first column
16
+ for (let i = 0; i <= a.length; i++) {
17
+ matrix[i] = [i]
18
+ }
19
+
20
+ // Initialize first row
21
+ for (let j = 0; j <= b.length; j++) {
22
+ matrix[0]![j] = j
23
+ }
24
+
25
+ // Fill in the rest
26
+ for (let i = 1; i <= a.length; i++) {
27
+ for (let j = 1; j <= b.length; j++) {
28
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1
29
+ matrix[i]![j] = Math.min(
30
+ matrix[i - 1]![j]! + 1, // deletion
31
+ matrix[i]![j - 1]! + 1, // insertion
32
+ matrix[i - 1]![j - 1]! + cost, // substitution
33
+ )
34
+ }
35
+ }
36
+
37
+ return matrix[a.length]![b.length]!
38
+ }
39
+
40
+ /**
41
+ * Suggestion result
42
+ */
43
+ export interface Suggestion {
44
+ flag: string
45
+ distance: number
46
+ description: string | undefined
47
+ }
48
+
49
+ /**
50
+ * Find the best flag suggestion for a typo
51
+ *
52
+ * @param typo - The mistyped flag (e.g., '--jsno')
53
+ * @param schema - The command schema to search
54
+ * @param maxDistance - Maximum Levenshtein distance to consider (default: 2)
55
+ * @returns Best matching flag or undefined
56
+ */
57
+ export const suggestFlag = (
58
+ typo: string,
59
+ schema: CommandSchema,
60
+ maxDistance: number = 2,
61
+ ): Suggestion | undefined => {
62
+ // Normalize the typo (remove leading dashes for comparison)
63
+ const normalizedTypo = typo.replace(/^-+/, '')
64
+
65
+ let bestMatch: Suggestion | undefined
66
+ let bestDistance = Infinity
67
+
68
+ for (const spec of schema.flags) {
69
+ // Check against full flag name
70
+ const flagName = spec.name
71
+ const distance = levenshteinDistance(normalizedTypo, flagName)
72
+
73
+ if (distance <= maxDistance && distance < bestDistance) {
74
+ bestDistance = distance
75
+ bestMatch = {
76
+ flag: `--${spec.name}`,
77
+ distance,
78
+ description: spec.description,
79
+ }
80
+ }
81
+
82
+ // Also check against alias if present
83
+ if (spec.alias) {
84
+ const aliasDistance = levenshteinDistance(normalizedTypo, spec.alias)
85
+ if (aliasDistance <= maxDistance && aliasDistance < bestDistance) {
86
+ bestDistance = aliasDistance
87
+ bestMatch = {
88
+ flag: `--${spec.name}`,
89
+ distance: aliasDistance,
90
+ description: spec.description,
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ // Prefer exact prefix matches (e.g., '--js' should suggest '--json')
97
+ if (!bestMatch || bestDistance > 0) {
98
+ for (const spec of schema.flags) {
99
+ if (spec.name.startsWith(normalizedTypo)) {
100
+ // Prefix match - this is likely what they meant
101
+ const prefixDistance = spec.name.length - normalizedTypo.length
102
+ if (prefixDistance <= maxDistance && prefixDistance < bestDistance) {
103
+ bestDistance = prefixDistance
104
+ bestMatch = {
105
+ flag: `--${spec.name}`,
106
+ distance: prefixDistance,
107
+ description: spec.description,
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ return bestMatch
115
+ }
116
+
117
+ /**
118
+ * Format a list of valid flags for a command
119
+ */
120
+ export const formatValidFlags = (schema: CommandSchema): string => {
121
+ const lines: string[] = []
122
+
123
+ for (const spec of schema.flags) {
124
+ const alias = spec.alias ? `, -${spec.alias}` : ''
125
+ const desc = spec.description ? ` ${spec.description}` : ''
126
+ lines.push(` --${spec.name}${alias}${desc}`)
127
+ }
128
+
129
+ return lines.join('\n')
130
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * CLI Utility Functions
3
+ *
4
+ * Shared helper functions used across CLI commands.
5
+ */
6
+
7
+ import * as fsPromises from 'node:fs/promises'
8
+ import * as path from 'node:path'
9
+
10
+ /**
11
+ * Format object as JSON string
12
+ */
13
+ export const formatJson = (obj: unknown, pretty: boolean): string => {
14
+ return pretty ? JSON.stringify(obj, null, 2) : JSON.stringify(obj)
15
+ }
16
+
17
+ /**
18
+ * Check if filename is a markdown file
19
+ */
20
+ export const isMarkdownFile = (filename: string): boolean => {
21
+ return filename.endsWith('.md') || filename.endsWith('.mdx')
22
+ }
23
+
24
+ /**
25
+ * Recursively walk directory and collect markdown files
26
+ */
27
+ export const walkDir = async (dir: string): Promise<string[]> => {
28
+ const files: string[] = []
29
+ const entries = await fsPromises.readdir(dir, { withFileTypes: true })
30
+
31
+ for (const entry of entries) {
32
+ const fullPath = path.join(dir, entry.name)
33
+
34
+ // Skip hidden directories and node_modules
35
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
36
+ continue
37
+ }
38
+
39
+ if (entry.isDirectory()) {
40
+ const subFiles = await walkDir(fullPath)
41
+ files.push(...subFiles)
42
+ } else if (entry.isFile() && isMarkdownFile(entry.name)) {
43
+ files.push(fullPath)
44
+ }
45
+ }
46
+
47
+ return files
48
+ }
49
+
50
+ /**
51
+ * Check if a query looks like a regex pattern
52
+ */
53
+ export const isRegexPattern = (query: string): boolean => {
54
+ // Has regex special characters (excluding simple spaces and common punctuation)
55
+ return /[.*+?^${}()|[\]\\]/.test(query)
56
+ }
57
+
58
+ /**
59
+ * Check if embeddings exist for a directory
60
+ */
61
+ export const hasEmbeddings = async (dir: string): Promise<boolean> => {
62
+ const vectorsPath = path.join(dir, '.mdcontext', 'vectors.bin')
63
+ try {
64
+ await fsPromises.access(vectorsPath)
65
+ return true
66
+ } catch {
67
+ return false
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get index information for display
73
+ */
74
+ export interface IndexInfo {
75
+ exists: boolean
76
+ lastUpdated?: string | undefined
77
+ sectionCount?: number | undefined
78
+ embeddingsExist: boolean
79
+ vectorCount?: number | undefined
80
+ }
81
+
82
+ export const getIndexInfo = async (dir: string): Promise<IndexInfo> => {
83
+ const sectionsPath = path.join(dir, '.mdcontext', 'indexes', 'sections.json')
84
+ const vectorsMetaPath = path.join(dir, '.mdcontext', 'vectors.meta.json')
85
+
86
+ let exists = false
87
+ let lastUpdated: string | undefined
88
+ let sectionCount: number | undefined
89
+ let embeddingsExist = false
90
+ let vectorCount: number | undefined
91
+
92
+ // Check sections index
93
+ try {
94
+ const stat = await fsPromises.stat(sectionsPath)
95
+ exists = true
96
+ lastUpdated = stat.mtime.toISOString()
97
+
98
+ const content = await fsPromises.readFile(sectionsPath, 'utf-8')
99
+ const sections = JSON.parse(content)
100
+ sectionCount = Object.keys(sections.sections || {}).length
101
+ } catch {
102
+ // Index doesn't exist
103
+ }
104
+
105
+ // Check vectors metadata
106
+ try {
107
+ const content = await fsPromises.readFile(vectorsMetaPath, 'utf-8')
108
+ const meta = JSON.parse(content)
109
+ embeddingsExist = true
110
+ vectorCount = Object.keys(meta.entries || {}).length
111
+ // Use vector meta updatedAt if available
112
+ if (meta.updatedAt) {
113
+ lastUpdated = meta.updatedAt
114
+ }
115
+ } catch {
116
+ // Embeddings don't exist
117
+ }
118
+
119
+ return {
120
+ exists,
121
+ lastUpdated,
122
+ sectionCount,
123
+ embeddingsExist,
124
+ vectorCount,
125
+ }
126
+ }
@@ -0,0 +1 @@
1
+ export * from './types.js'
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Core data types for mdcontext
3
+ * Based on DESIGN.md specifications
4
+ */
5
+
6
+ // ============================================================================
7
+ // Document Types
8
+ // ============================================================================
9
+
10
+ export interface MdDocument {
11
+ readonly id: string
12
+ readonly path: string
13
+ readonly title: string
14
+ readonly frontmatter: Record<string, unknown>
15
+ readonly sections: readonly MdSection[]
16
+ readonly links: readonly MdLink[]
17
+ readonly codeBlocks: readonly MdCodeBlock[]
18
+ readonly metadata: DocumentMetadata
19
+ }
20
+
21
+ export interface DocumentMetadata {
22
+ readonly wordCount: number
23
+ readonly tokenCount: number
24
+ readonly headingCount: number
25
+ readonly linkCount: number
26
+ readonly codeBlockCount: number
27
+ readonly lastModified: Date
28
+ readonly indexedAt: Date
29
+ }
30
+
31
+ // ============================================================================
32
+ // Section Types
33
+ // ============================================================================
34
+
35
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
36
+
37
+ export interface MdSection {
38
+ readonly id: string
39
+ readonly heading: string
40
+ readonly level: HeadingLevel
41
+ readonly content: string
42
+ readonly plainText: string
43
+ readonly startLine: number
44
+ readonly endLine: number
45
+ readonly children: readonly MdSection[]
46
+ readonly metadata: SectionMetadata
47
+ }
48
+
49
+ export interface SectionMetadata {
50
+ readonly wordCount: number
51
+ readonly tokenCount: number
52
+ readonly hasCode: boolean
53
+ readonly hasList: boolean
54
+ readonly hasTable: boolean
55
+ }
56
+
57
+ // ============================================================================
58
+ // Link Types
59
+ // ============================================================================
60
+
61
+ export type LinkType = 'internal' | 'external' | 'image'
62
+
63
+ export interface MdLink {
64
+ readonly type: LinkType
65
+ readonly href: string
66
+ readonly text: string
67
+ readonly sectionId: string
68
+ readonly line: number
69
+ }
70
+
71
+ // ============================================================================
72
+ // Code Block Types
73
+ // ============================================================================
74
+
75
+ export interface MdCodeBlock {
76
+ readonly language: string | null
77
+ readonly content: string
78
+ readonly sectionId: string
79
+ readonly startLine: number
80
+ readonly endLine: number
81
+ }
82
+
83
+ // ============================================================================
84
+ // Error Types
85
+ // ============================================================================
86
+
87
+ export interface ParseError {
88
+ readonly _tag: 'ParseError'
89
+ readonly message: string
90
+ readonly line?: number | undefined
91
+ readonly column?: number | undefined
92
+ }
93
+
94
+ export interface IoError {
95
+ readonly _tag: 'IoError'
96
+ readonly message: string
97
+ readonly path: string
98
+ readonly cause?: unknown
99
+ }
100
+
101
+ export interface IndexError {
102
+ readonly _tag: 'IndexError'
103
+ readonly cause: 'DiskFull' | 'Permission' | 'Corrupted' | 'Unknown'
104
+ readonly message: string
105
+ }
106
+
107
+ // ============================================================================
108
+ // Constructor Functions
109
+ // ============================================================================
110
+
111
+ export const ParseError = (
112
+ message: string,
113
+ line?: number,
114
+ column?: number,
115
+ ): ParseError => ({
116
+ _tag: 'ParseError',
117
+ message,
118
+ line,
119
+ column,
120
+ })
121
+
122
+ export const IoError = (
123
+ message: string,
124
+ path: string,
125
+ cause?: unknown,
126
+ ): IoError => ({
127
+ _tag: 'IoError',
128
+ message,
129
+ path,
130
+ cause,
131
+ })
132
+
133
+ export const IndexError = (
134
+ cause: IndexError['cause'],
135
+ message: string,
136
+ ): IndexError => ({
137
+ _tag: 'IndexError',
138
+ cause,
139
+ message,
140
+ })
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Embeddings module exports
3
+ */
4
+
5
+ export * from './openai-provider.js'
6
+ export * from './semantic-search.js'
7
+ export * from './types.js'
8
+ export * from './vector-store.js'