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.
- package/.changeset/README.md +28 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/release.yml +113 -0
- package/.tldrignore +112 -0
- package/AGENTS.md +46 -0
- package/BACKLOG.md +338 -0
- package/README.md +231 -11
- package/biome.json +36 -0
- package/cspell.config.yaml +14 -0
- package/dist/chunk-KRYIFLQR.js +92 -0
- package/dist/chunk-S7E6TFX6.js +742 -0
- package/dist/chunk-VVTGZNBT.js +1519 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +2015 -0
- package/dist/index.d.ts +266 -0
- package/dist/index.js +86 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +376 -0
- package/docs/019-USAGE.md +586 -0
- package/docs/020-current-implementation.md +364 -0
- package/docs/021-DOGFOODING-FINDINGS.md +175 -0
- package/docs/BACKLOG.md +80 -0
- package/docs/DESIGN.md +439 -0
- package/docs/PROJECT.md +88 -0
- package/docs/ROADMAP.md +407 -0
- package/docs/test-links.md +9 -0
- package/package.json +69 -10
- package/pnpm-workspace.yaml +5 -0
- package/research/config-analysis/01-current-implementation.md +470 -0
- package/research/config-analysis/02-strategy-recommendation.md +428 -0
- package/research/config-analysis/03-task-candidates.md +715 -0
- package/research/config-analysis/033-research-configuration-management.md +828 -0
- package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
- package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
- package/research/dogfood/consolidated-tool-evaluation.md +373 -0
- package/research/dogfood/strategy-a/a-synthesis.md +184 -0
- package/research/dogfood/strategy-a/a1-docs.md +226 -0
- package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
- package/research/dogfood/strategy-a/a3-llm.md +164 -0
- package/research/dogfood/strategy-b/b-synthesis.md +228 -0
- package/research/dogfood/strategy-b/b1-architecture.md +207 -0
- package/research/dogfood/strategy-b/b2-gaps.md +258 -0
- package/research/dogfood/strategy-b/b3-workflows.md +250 -0
- package/research/dogfood/strategy-c/c-synthesis.md +451 -0
- package/research/dogfood/strategy-c/c1-explorer.md +192 -0
- package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
- package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
- package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
- package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
- package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
- package/research/effect-cli-error-handling.md +845 -0
- package/research/effect-errors-as-values.md +943 -0
- package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
- package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
- package/research/errors-task-analysis/embeddings-analysis.md +709 -0
- package/research/errors-task-analysis/index-search-analysis.md +812 -0
- package/research/mdcontext-error-analysis.md +521 -0
- package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
- package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
- package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
- package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
- package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
- package/research/semantic-search/002-research-embedding-models.md +490 -0
- package/research/semantic-search/003-research-rag-alternatives.md +523 -0
- package/research/semantic-search/004-research-vector-search.md +841 -0
- package/research/semantic-search/032-research-semantic-search.md +427 -0
- package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
- package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
- package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
- package/research/task-management-2026/03-lightweight-file-based.md +567 -0
- package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
- package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
- package/research/task-management-2026/linear/02-api-integrations.md +930 -0
- package/research/task-management-2026/linear/03-ai-features.md +368 -0
- package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
- package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
- package/scripts/rebuild-hnswlib.js +63 -0
- package/src/cli/argv-preprocessor.test.ts +210 -0
- package/src/cli/argv-preprocessor.ts +202 -0
- package/src/cli/cli.test.ts +430 -0
- package/src/cli/commands/backlinks.ts +54 -0
- package/src/cli/commands/context.ts +197 -0
- package/src/cli/commands/index-cmd.ts +300 -0
- package/src/cli/commands/index.ts +13 -0
- package/src/cli/commands/links.ts +52 -0
- package/src/cli/commands/search.ts +451 -0
- package/src/cli/commands/stats.ts +146 -0
- package/src/cli/commands/tree.ts +107 -0
- package/src/cli/flag-schemas.ts +275 -0
- package/src/cli/help.ts +386 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/main.ts +145 -0
- package/src/cli/options.ts +31 -0
- package/src/cli/typo-suggester.test.ts +105 -0
- package/src/cli/typo-suggester.ts +130 -0
- package/src/cli/utils.ts +126 -0
- package/src/core/index.ts +1 -0
- package/src/core/types.ts +140 -0
- package/src/embeddings/index.ts +8 -0
- package/src/embeddings/openai-provider.ts +165 -0
- package/src/embeddings/semantic-search.ts +583 -0
- package/src/embeddings/types.ts +82 -0
- package/src/embeddings/vector-store.ts +299 -0
- package/src/index/index.ts +4 -0
- package/src/index/indexer.ts +446 -0
- package/src/index/storage.ts +196 -0
- package/src/index/types.ts +109 -0
- package/src/index/watcher.ts +131 -0
- package/src/index.ts +8 -0
- package/src/mcp/server.ts +483 -0
- package/src/parser/index.ts +1 -0
- package/src/parser/parser.test.ts +291 -0
- package/src/parser/parser.ts +395 -0
- package/src/parser/section-filter.ts +270 -0
- package/src/search/query-parser.test.ts +260 -0
- package/src/search/query-parser.ts +319 -0
- package/src/search/searcher.test.ts +182 -0
- package/src/search/searcher.ts +602 -0
- package/src/summarize/budget-bugs.test.ts +620 -0
- package/src/summarize/formatters.ts +419 -0
- package/src/summarize/index.ts +20 -0
- package/src/summarize/summarizer.test.ts +275 -0
- package/src/summarize/summarizer.ts +528 -0
- package/src/summarize/verify-bugs.test.ts +238 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/tokens.test.ts +142 -0
- package/src/utils/tokens.ts +186 -0
- package/tests/fixtures/cli/.mdcontext/config.json +8 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
- package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +233 -0
- package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/vectors.meta.json +1264 -0
- package/tests/fixtures/cli/README.md +9 -0
- package/tests/fixtures/cli/api-reference.md +11 -0
- package/tests/fixtures/cli/getting-started.md +11 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +21 -0
- package/vitest.setup.ts +12 -0
package/src/cli/main.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/utils.ts
ADDED
|
@@ -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
|
+
})
|