mdcontext 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/README.md +28 -0
- package/.changeset/config.json +11 -0
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/.github/workflows/release.yml +113 -0
- package/.tldrignore +112 -0
- package/BACKLOG.md +338 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +434 -11
- package/biome.json +36 -0
- package/cspell.config.yaml +14 -0
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +88 -0
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +803 -0
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1629 -0
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +5458 -0
- package/dist/index.d.ts +653 -0
- package/dist/index.js +79 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +472 -0
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +625 -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/CONFIG.md +1123 -0
- package/docs/DESIGN.md +439 -0
- package/docs/ERRORS.md +383 -0
- package/docs/PROJECT.md +88 -0
- package/docs/ROADMAP.md +407 -0
- package/docs/summarization.md +320 -0
- package/docs/test-links.md +9 -0
- package/justfile +40 -0
- package/package.json +74 -9
- package/pnpm-workspace.yaml +5 -0
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -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/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -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/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-error-analysis.md +521 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -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/research-quality-review.md +834 -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/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -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/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +58 -0
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +210 -0
- package/src/cli/argv-preprocessor.ts +202 -0
- package/src/cli/cli.test.ts +627 -0
- package/src/cli/commands/backlinks.ts +54 -0
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +285 -0
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +480 -0
- package/src/cli/commands/index.ts +16 -0
- package/src/cli/commands/links.ts +52 -0
- package/src/cli/commands/search.ts +1281 -0
- package/src/cli/commands/stats.ts +149 -0
- package/src/cli/commands/tree.ts +128 -0
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +341 -0
- package/src/cli/help.ts +588 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/main.ts +435 -0
- package/src/cli/options.ts +41 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/typo-suggester.test.ts +105 -0
- package/src/cli/typo-suggester.ts +130 -0
- package/src/cli/utils.ts +259 -0
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/index.ts +1 -0
- package/src/core/types.ts +113 -0
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +10 -0
- package/src/embeddings/openai-provider.ts +414 -0
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +1270 -0
- package/src/embeddings/types.ts +359 -0
- package/src/embeddings/vector-store.ts +708 -0
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/index.ts +4 -0
- package/src/index/indexer.ts +684 -0
- package/src/index/storage.ts +260 -0
- package/src/index/types.ts +147 -0
- package/src/index/watcher.ts +189 -0
- package/src/index.ts +30 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +612 -0
- package/src/parser/index.ts +1 -0
- package/src/parser/parser.test.ts +291 -0
- package/src/parser/parser.ts +394 -0
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +392 -0
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/query-parser.test.ts +260 -0
- package/src/search/query-parser.ts +319 -0
- package/src/search/searcher.test.ts +280 -0
- package/src/search/searcher.ts +724 -0
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -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 +597 -0
- package/src/summarize/verify-bugs.test.ts +238 -0
- package/src/types/huggingface-transformers.d.ts +66 -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/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/config.json +8 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -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 +247 -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/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +16 -0
- package/vitest.setup.ts +12 -0
|
@@ -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,259 @@
|
|
|
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
|
+
import { Effect } from 'effect'
|
|
10
|
+
import { listNamespaces } from '../embeddings/embedding-namespace.js'
|
|
11
|
+
import { DirectoryWalkError } from '../errors/index.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format object as JSON string
|
|
15
|
+
*/
|
|
16
|
+
export const formatJson = (obj: unknown, pretty: boolean): string => {
|
|
17
|
+
return pretty ? JSON.stringify(obj, null, 2) : JSON.stringify(obj)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if filename is a markdown file
|
|
22
|
+
*/
|
|
23
|
+
export const isMarkdownFile = (filename: string): boolean => {
|
|
24
|
+
return filename.endsWith('.md') || filename.endsWith('.mdx')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Recursively walk directory and collect markdown files (async version).
|
|
29
|
+
* @deprecated Use walkDirEffect for typed error handling
|
|
30
|
+
*/
|
|
31
|
+
export const walkDir = async (dir: string): Promise<string[]> => {
|
|
32
|
+
const files: string[] = []
|
|
33
|
+
const entries = await fsPromises.readdir(dir, { withFileTypes: true })
|
|
34
|
+
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const fullPath = path.join(dir, entry.name)
|
|
37
|
+
|
|
38
|
+
// Skip hidden directories and node_modules
|
|
39
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
const subFiles = await walkDir(fullPath)
|
|
45
|
+
files.push(...subFiles)
|
|
46
|
+
} else if (entry.isFile() && isMarkdownFile(entry.name)) {
|
|
47
|
+
files.push(fullPath)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return files
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Recursively walk directory and collect markdown files.
|
|
56
|
+
*
|
|
57
|
+
* @param dir - Directory to walk
|
|
58
|
+
* @returns List of markdown file paths
|
|
59
|
+
*
|
|
60
|
+
* @throws DirectoryWalkError - Cannot read or traverse directory
|
|
61
|
+
*/
|
|
62
|
+
export const walkDirEffect = (
|
|
63
|
+
dir: string,
|
|
64
|
+
): Effect.Effect<readonly string[], DirectoryWalkError> =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const files: string[] = []
|
|
67
|
+
|
|
68
|
+
const entries = yield* Effect.tryPromise({
|
|
69
|
+
try: () => fsPromises.readdir(dir, { withFileTypes: true }),
|
|
70
|
+
catch: (e) =>
|
|
71
|
+
new DirectoryWalkError({
|
|
72
|
+
path: dir,
|
|
73
|
+
message: `Cannot read directory: ${e instanceof Error ? e.message : String(e)}`,
|
|
74
|
+
cause: e,
|
|
75
|
+
}),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const fullPath = path.join(dir, entry.name)
|
|
80
|
+
|
|
81
|
+
// Skip hidden directories and node_modules
|
|
82
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
const subFiles = yield* walkDirEffect(fullPath)
|
|
88
|
+
files.push(...subFiles)
|
|
89
|
+
} else if (entry.isFile() && isMarkdownFile(entry.name)) {
|
|
90
|
+
files.push(fullPath)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return files
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a query looks like a regex pattern
|
|
99
|
+
*/
|
|
100
|
+
export const isRegexPattern = (query: string): boolean => {
|
|
101
|
+
// Has regex special characters (excluding simple spaces and common punctuation)
|
|
102
|
+
return /[.*+?^${}()|[\]\\]/.test(query)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if embeddings exist for a directory.
|
|
107
|
+
* Checks for namespaced embeddings in .mdcontext/embeddings/<namespace>/vectors.bin
|
|
108
|
+
*/
|
|
109
|
+
export const hasEmbeddings = async (dir: string): Promise<boolean> => {
|
|
110
|
+
try {
|
|
111
|
+
const namespaces = await Effect.runPromise(
|
|
112
|
+
listNamespaces(dir).pipe(Effect.catchAll(() => Effect.succeed([]))),
|
|
113
|
+
)
|
|
114
|
+
return namespaces.length > 0
|
|
115
|
+
} catch {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Find the nearest parent directory containing an mdcontext index.
|
|
122
|
+
* Searches from the specified directory up to the filesystem root.
|
|
123
|
+
*
|
|
124
|
+
* @param startDir - Directory to start searching from
|
|
125
|
+
* @returns The directory containing the index, or null if not found
|
|
126
|
+
*/
|
|
127
|
+
export const findIndexRoot = async (
|
|
128
|
+
startDir: string,
|
|
129
|
+
): Promise<string | null> => {
|
|
130
|
+
let currentDir = path.resolve(startDir)
|
|
131
|
+
const root = path.parse(currentDir).root
|
|
132
|
+
|
|
133
|
+
while (currentDir !== root) {
|
|
134
|
+
const sectionsPath = path.join(
|
|
135
|
+
currentDir,
|
|
136
|
+
'.mdcontext',
|
|
137
|
+
'indexes',
|
|
138
|
+
'sections.json',
|
|
139
|
+
)
|
|
140
|
+
try {
|
|
141
|
+
await fsPromises.access(sectionsPath)
|
|
142
|
+
return currentDir // Found an index
|
|
143
|
+
} catch {
|
|
144
|
+
// No index here, try parent
|
|
145
|
+
const parent = path.dirname(currentDir)
|
|
146
|
+
if (parent === currentDir) break // Reached root
|
|
147
|
+
currentDir = parent
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Also check root
|
|
152
|
+
const rootSectionsPath = path.join(
|
|
153
|
+
root,
|
|
154
|
+
'.mdcontext',
|
|
155
|
+
'indexes',
|
|
156
|
+
'sections.json',
|
|
157
|
+
)
|
|
158
|
+
try {
|
|
159
|
+
await fsPromises.access(rootSectionsPath)
|
|
160
|
+
return root
|
|
161
|
+
} catch {
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get index information for display
|
|
168
|
+
*/
|
|
169
|
+
export interface IndexInfo {
|
|
170
|
+
exists: boolean
|
|
171
|
+
lastUpdated?: string | undefined
|
|
172
|
+
sectionCount?: number | undefined
|
|
173
|
+
embeddingsExist: boolean
|
|
174
|
+
vectorCount?: number | undefined
|
|
175
|
+
/** The actual directory where the index was found (may differ from requested dir) */
|
|
176
|
+
indexRoot?: string | undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const getIndexInfo = async (dir: string): Promise<IndexInfo> => {
|
|
180
|
+
// First try the specified directory
|
|
181
|
+
let indexRoot = dir
|
|
182
|
+
let sectionsPath = path.join(dir, '.mdcontext', 'indexes', 'sections.json')
|
|
183
|
+
|
|
184
|
+
let exists = false
|
|
185
|
+
let lastUpdated: string | undefined
|
|
186
|
+
let sectionCount: number | undefined
|
|
187
|
+
let embeddingsExist = false
|
|
188
|
+
let vectorCount: number | undefined
|
|
189
|
+
|
|
190
|
+
// Check sections index in specified directory
|
|
191
|
+
try {
|
|
192
|
+
const stat = await fsPromises.stat(sectionsPath)
|
|
193
|
+
exists = true
|
|
194
|
+
lastUpdated = stat.mtime.toISOString()
|
|
195
|
+
|
|
196
|
+
const content = await fsPromises.readFile(sectionsPath, 'utf-8')
|
|
197
|
+
const sections = JSON.parse(content)
|
|
198
|
+
sectionCount = Object.keys(sections.sections || {}).length
|
|
199
|
+
} catch {
|
|
200
|
+
// Index doesn't exist in specified directory, try to find in parent directories
|
|
201
|
+
const foundRoot = await findIndexRoot(dir)
|
|
202
|
+
if (foundRoot) {
|
|
203
|
+
indexRoot = foundRoot
|
|
204
|
+
sectionsPath = path.join(
|
|
205
|
+
foundRoot,
|
|
206
|
+
'.mdcontext',
|
|
207
|
+
'indexes',
|
|
208
|
+
'sections.json',
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const stat = await fsPromises.stat(sectionsPath)
|
|
213
|
+
exists = true
|
|
214
|
+
lastUpdated = stat.mtime.toISOString()
|
|
215
|
+
|
|
216
|
+
const content = await fsPromises.readFile(sectionsPath, 'utf-8')
|
|
217
|
+
const sections = JSON.parse(content)
|
|
218
|
+
sectionCount = Object.keys(sections.sections || {}).length
|
|
219
|
+
} catch {
|
|
220
|
+
// Still failed
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check namespaced embeddings
|
|
226
|
+
try {
|
|
227
|
+
const namespaces = await Effect.runPromise(
|
|
228
|
+
listNamespaces(indexRoot).pipe(Effect.catchAll(() => Effect.succeed([]))),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if (namespaces.length > 0) {
|
|
232
|
+
embeddingsExist = true
|
|
233
|
+
// Find active namespace or use first one
|
|
234
|
+
const activeNs = namespaces.find((ns) => ns.isActive) ?? namespaces[0]
|
|
235
|
+
if (activeNs) {
|
|
236
|
+
vectorCount = activeNs.vectorCount
|
|
237
|
+
// Use namespace's updatedAt if more recent
|
|
238
|
+
if (activeNs.updatedAt) {
|
|
239
|
+
const nsDate = new Date(activeNs.updatedAt)
|
|
240
|
+
const currentDate = lastUpdated ? new Date(lastUpdated) : new Date(0)
|
|
241
|
+
if (nsDate > currentDate) {
|
|
242
|
+
lastUpdated = activeNs.updatedAt
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// Embeddings don't exist
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
exists,
|
|
253
|
+
lastUpdated,
|
|
254
|
+
sectionCount,
|
|
255
|
+
embeddingsExist,
|
|
256
|
+
vectorCount,
|
|
257
|
+
indexRoot: exists && indexRoot !== dir ? indexRoot : undefined,
|
|
258
|
+
}
|
|
259
|
+
}
|