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,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Provider Factory Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
createSummarizer,
|
|
8
|
+
getBestAvailableSummarizer,
|
|
9
|
+
} from './provider-factory.js'
|
|
10
|
+
import type { AISummarizationConfig } from './types.js'
|
|
11
|
+
import { SummarizationError } from './types.js'
|
|
12
|
+
|
|
13
|
+
vi.mock('./cli-providers/detection.js', () => ({
|
|
14
|
+
isCLIInstalled: vi.fn(),
|
|
15
|
+
getCLIInfo: vi.fn((name: string) => ({
|
|
16
|
+
name,
|
|
17
|
+
displayName: name.charAt(0).toUpperCase() + name.slice(1),
|
|
18
|
+
command: name,
|
|
19
|
+
args: [],
|
|
20
|
+
useStdin: false,
|
|
21
|
+
})),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
vi.mock('./cli-providers/claude.js', () => {
|
|
25
|
+
return {
|
|
26
|
+
ClaudeCLISummarizer: class MockClaudeCLISummarizer {
|
|
27
|
+
summarize = vi.fn()
|
|
28
|
+
isAvailable = vi.fn().mockResolvedValue(true)
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
import { isCLIInstalled } from './cli-providers/detection.js'
|
|
34
|
+
|
|
35
|
+
const mockIsCLIInstalled = vi.mocked(isCLIInstalled)
|
|
36
|
+
|
|
37
|
+
describe('createSummarizer', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.clearAllMocks()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('CLI mode', () => {
|
|
43
|
+
it('should return ClaudeCLISummarizer for CLI mode with claude provider', async () => {
|
|
44
|
+
mockIsCLIInstalled.mockResolvedValue(true)
|
|
45
|
+
|
|
46
|
+
const config: AISummarizationConfig = {
|
|
47
|
+
mode: 'cli',
|
|
48
|
+
provider: 'claude',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const summarizer = await createSummarizer(config)
|
|
52
|
+
|
|
53
|
+
expect(mockIsCLIInstalled).toHaveBeenCalledWith('claude')
|
|
54
|
+
expect(summarizer).toBeDefined()
|
|
55
|
+
expect(summarizer.summarize).toBeDefined()
|
|
56
|
+
expect(summarizer.isAvailable).toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should throw PROVIDER_NOT_FOUND for unimplemented CLI providers', async () => {
|
|
60
|
+
mockIsCLIInstalled.mockResolvedValue(true)
|
|
61
|
+
|
|
62
|
+
const unimplementedProviders = [
|
|
63
|
+
'opencode',
|
|
64
|
+
'copilot',
|
|
65
|
+
'aider',
|
|
66
|
+
'cline',
|
|
67
|
+
'amp',
|
|
68
|
+
] as const
|
|
69
|
+
|
|
70
|
+
for (const provider of unimplementedProviders) {
|
|
71
|
+
const config: AISummarizationConfig = {
|
|
72
|
+
mode: 'cli',
|
|
73
|
+
provider,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await expect(createSummarizer(config)).rejects.toThrow(
|
|
77
|
+
SummarizationError,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await createSummarizer(config)
|
|
82
|
+
} catch (error) {
|
|
83
|
+
expect(error).toBeInstanceOf(SummarizationError)
|
|
84
|
+
expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
|
|
85
|
+
expect((error as SummarizationError).message).toContain(
|
|
86
|
+
'not yet implemented',
|
|
87
|
+
)
|
|
88
|
+
expect((error as SummarizationError).provider).toBe(provider)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should throw PROVIDER_NOT_AVAILABLE if CLI not installed', async () => {
|
|
94
|
+
mockIsCLIInstalled.mockResolvedValue(false)
|
|
95
|
+
|
|
96
|
+
const config: AISummarizationConfig = {
|
|
97
|
+
mode: 'cli',
|
|
98
|
+
provider: 'claude',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await expect(createSummarizer(config)).rejects.toThrow(SummarizationError)
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await createSummarizer(config)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
expect(error).toBeInstanceOf(SummarizationError)
|
|
107
|
+
expect((error as SummarizationError).code).toBe(
|
|
108
|
+
'PROVIDER_NOT_AVAILABLE',
|
|
109
|
+
)
|
|
110
|
+
expect((error as SummarizationError).message).toContain('not installed')
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should throw PROVIDER_NOT_FOUND for invalid CLI provider', async () => {
|
|
115
|
+
const config = {
|
|
116
|
+
mode: 'cli',
|
|
117
|
+
provider: 'invalid-provider',
|
|
118
|
+
} as unknown as AISummarizationConfig
|
|
119
|
+
|
|
120
|
+
await expect(createSummarizer(config)).rejects.toThrow(SummarizationError)
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await createSummarizer(config)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
expect(error).toBeInstanceOf(SummarizationError)
|
|
126
|
+
expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
|
|
127
|
+
expect((error as SummarizationError).message).toContain(
|
|
128
|
+
'Invalid CLI provider',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('API mode', () => {
|
|
135
|
+
it('should throw PROVIDER_NOT_FOUND for API providers (not yet implemented)', async () => {
|
|
136
|
+
const apiProviders = [
|
|
137
|
+
'deepseek',
|
|
138
|
+
'anthropic',
|
|
139
|
+
'openai',
|
|
140
|
+
'gemini',
|
|
141
|
+
'qwen',
|
|
142
|
+
] as const
|
|
143
|
+
|
|
144
|
+
for (const provider of apiProviders) {
|
|
145
|
+
const config: AISummarizationConfig = {
|
|
146
|
+
mode: 'api',
|
|
147
|
+
provider,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await expect(createSummarizer(config)).rejects.toThrow(
|
|
151
|
+
SummarizationError,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await createSummarizer(config)
|
|
156
|
+
} catch (error) {
|
|
157
|
+
expect(error).toBeInstanceOf(SummarizationError)
|
|
158
|
+
expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
|
|
159
|
+
expect((error as SummarizationError).message).toContain(
|
|
160
|
+
'not yet implemented',
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should throw PROVIDER_NOT_FOUND for invalid API provider', async () => {
|
|
167
|
+
const config = {
|
|
168
|
+
mode: 'api',
|
|
169
|
+
provider: 'not-a-real-api',
|
|
170
|
+
} as unknown as AISummarizationConfig
|
|
171
|
+
|
|
172
|
+
await expect(createSummarizer(config)).rejects.toThrow(SummarizationError)
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await createSummarizer(config)
|
|
176
|
+
} catch (error) {
|
|
177
|
+
expect(error).toBeInstanceOf(SummarizationError)
|
|
178
|
+
expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
|
|
179
|
+
expect((error as SummarizationError).message).toContain(
|
|
180
|
+
'Invalid API provider',
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('unknown mode', () => {
|
|
187
|
+
it('should throw PROVIDER_NOT_FOUND for unknown mode', async () => {
|
|
188
|
+
const config = {
|
|
189
|
+
mode: 'unknown',
|
|
190
|
+
provider: 'claude',
|
|
191
|
+
} as unknown as AISummarizationConfig
|
|
192
|
+
|
|
193
|
+
await expect(createSummarizer(config)).rejects.toThrow(SummarizationError)
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await createSummarizer(config)
|
|
197
|
+
} catch (error) {
|
|
198
|
+
expect(error).toBeInstanceOf(SummarizationError)
|
|
199
|
+
expect((error as SummarizationError).code).toBe('PROVIDER_NOT_FOUND')
|
|
200
|
+
expect((error as SummarizationError).message).toContain(
|
|
201
|
+
'Unknown summarization mode',
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('type guards', () => {
|
|
208
|
+
it('isCLIProvider should accept valid CLI providers', async () => {
|
|
209
|
+
mockIsCLIInstalled.mockResolvedValue(true)
|
|
210
|
+
|
|
211
|
+
const validProviders = [
|
|
212
|
+
'claude',
|
|
213
|
+
'copilot',
|
|
214
|
+
'cline',
|
|
215
|
+
'aider',
|
|
216
|
+
'opencode',
|
|
217
|
+
'amp',
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
for (const provider of validProviders) {
|
|
221
|
+
const config: AISummarizationConfig = {
|
|
222
|
+
mode: 'cli',
|
|
223
|
+
provider: provider as AISummarizationConfig['provider'],
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Valid providers should not throw "Invalid CLI provider"
|
|
227
|
+
try {
|
|
228
|
+
await createSummarizer(config)
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// May throw for other reasons (not installed, not implemented)
|
|
231
|
+
// but should NOT throw "Invalid CLI provider"
|
|
232
|
+
expect((error as SummarizationError).message).not.toContain(
|
|
233
|
+
'Invalid CLI provider',
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('isAPIProvider should accept valid API providers', async () => {
|
|
240
|
+
const validProviders = [
|
|
241
|
+
'deepseek',
|
|
242
|
+
'anthropic',
|
|
243
|
+
'openai',
|
|
244
|
+
'gemini',
|
|
245
|
+
'qwen',
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
for (const provider of validProviders) {
|
|
249
|
+
const config: AISummarizationConfig = {
|
|
250
|
+
mode: 'api',
|
|
251
|
+
provider: provider as AISummarizationConfig['provider'],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await createSummarizer(config)
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// May throw "not yet implemented" but should NOT throw "Invalid API provider"
|
|
258
|
+
expect((error as SummarizationError).message).not.toContain(
|
|
259
|
+
'Invalid API provider',
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('isCLIProvider should reject invalid providers', async () => {
|
|
266
|
+
const config = {
|
|
267
|
+
mode: 'cli',
|
|
268
|
+
provider: 'not-valid',
|
|
269
|
+
} as unknown as AISummarizationConfig
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await createSummarizer(config)
|
|
273
|
+
} catch (error) {
|
|
274
|
+
expect((error as SummarizationError).message).toContain(
|
|
275
|
+
'Invalid CLI provider',
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('isAPIProvider should reject invalid providers', async () => {
|
|
281
|
+
const config = {
|
|
282
|
+
mode: 'api',
|
|
283
|
+
provider: 'not-valid',
|
|
284
|
+
} as unknown as AISummarizationConfig
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await createSummarizer(config)
|
|
288
|
+
} catch (error) {
|
|
289
|
+
expect((error as SummarizationError).message).toContain(
|
|
290
|
+
'Invalid API provider',
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('getBestAvailableSummarizer', () => {
|
|
298
|
+
beforeEach(() => {
|
|
299
|
+
vi.clearAllMocks()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should return null when no providers are available', async () => {
|
|
303
|
+
mockIsCLIInstalled.mockResolvedValue(false)
|
|
304
|
+
|
|
305
|
+
const result = await getBestAvailableSummarizer()
|
|
306
|
+
|
|
307
|
+
expect(result).toBeNull()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('should return ClaudeCLISummarizer when claude CLI is available', async () => {
|
|
311
|
+
mockIsCLIInstalled.mockResolvedValue(true)
|
|
312
|
+
|
|
313
|
+
const result = await getBestAvailableSummarizer()
|
|
314
|
+
|
|
315
|
+
expect(result).not.toBeNull()
|
|
316
|
+
expect(result?.config.mode).toBe('cli')
|
|
317
|
+
expect(result?.config.provider).toBe('claude')
|
|
318
|
+
expect(result?.summarizer).toBeDefined()
|
|
319
|
+
expect(result?.summarizer.summarize).toBeDefined()
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('should respect preferredConfig when provider is available', async () => {
|
|
323
|
+
mockIsCLIInstalled.mockResolvedValue(true)
|
|
324
|
+
|
|
325
|
+
const preferredConfig: AISummarizationConfig = {
|
|
326
|
+
mode: 'cli',
|
|
327
|
+
provider: 'claude',
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const result = await getBestAvailableSummarizer(preferredConfig)
|
|
331
|
+
|
|
332
|
+
expect(result).not.toBeNull()
|
|
333
|
+
expect(result?.config.mode).toBe('cli')
|
|
334
|
+
expect(result?.config.provider).toBe('claude')
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('should fall back to auto-detection when preferredConfig fails', async () => {
|
|
338
|
+
// First call for preferred config fails, second call for auto-detection succeeds
|
|
339
|
+
mockIsCLIInstalled
|
|
340
|
+
.mockResolvedValueOnce(false) // preferred config check fails
|
|
341
|
+
.mockResolvedValueOnce(true) // auto-detection finds claude
|
|
342
|
+
|
|
343
|
+
const preferredConfig: AISummarizationConfig = {
|
|
344
|
+
mode: 'cli',
|
|
345
|
+
provider: 'claude',
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = await getBestAvailableSummarizer(preferredConfig)
|
|
349
|
+
|
|
350
|
+
expect(result).not.toBeNull()
|
|
351
|
+
expect(result?.config.provider).toBe('claude')
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should return null when preferredConfig fails and no auto-detection succeeds', async () => {
|
|
355
|
+
mockIsCLIInstalled.mockResolvedValue(false)
|
|
356
|
+
|
|
357
|
+
const preferredConfig: AISummarizationConfig = {
|
|
358
|
+
mode: 'cli',
|
|
359
|
+
provider: 'claude',
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const result = await getBestAvailableSummarizer(preferredConfig)
|
|
363
|
+
|
|
364
|
+
expect(result).toBeNull()
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('should handle partial preferredConfig without mode', async () => {
|
|
368
|
+
mockIsCLIInstalled.mockResolvedValue(true)
|
|
369
|
+
|
|
370
|
+
const partialConfig = {
|
|
371
|
+
provider: 'claude',
|
|
372
|
+
} as Partial<AISummarizationConfig>
|
|
373
|
+
|
|
374
|
+
const result = await getBestAvailableSummarizer(partialConfig)
|
|
375
|
+
|
|
376
|
+
// Should fall through to auto-detection since mode is missing
|
|
377
|
+
expect(result).not.toBeNull()
|
|
378
|
+
expect(result?.config.mode).toBe('cli')
|
|
379
|
+
expect(result?.config.provider).toBe('claude')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('should handle partial preferredConfig without provider', async () => {
|
|
383
|
+
mockIsCLIInstalled.mockResolvedValue(true)
|
|
384
|
+
|
|
385
|
+
const partialConfig = {
|
|
386
|
+
mode: 'cli',
|
|
387
|
+
} as Partial<AISummarizationConfig>
|
|
388
|
+
|
|
389
|
+
const result = await getBestAvailableSummarizer(partialConfig)
|
|
390
|
+
|
|
391
|
+
// Should fall through to auto-detection since provider is missing
|
|
392
|
+
expect(result).not.toBeNull()
|
|
393
|
+
expect(result?.config.mode).toBe('cli')
|
|
394
|
+
expect(result?.config.provider).toBe('claude')
|
|
395
|
+
})
|
|
396
|
+
})
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarization Provider Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates summarizer instances based on configuration.
|
|
5
|
+
* CLI providers are checked first (free), then API providers (paid).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ClaudeCLISummarizer } from './cli-providers/claude.js'
|
|
9
|
+
import { getCLIInfo, isCLIInstalled } from './cli-providers/detection.js'
|
|
10
|
+
import type {
|
|
11
|
+
AISummarizationConfig,
|
|
12
|
+
APIProviderName,
|
|
13
|
+
CLIProviderName,
|
|
14
|
+
Summarizer,
|
|
15
|
+
} from './types.js'
|
|
16
|
+
import { SummarizationError } from './types.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a CLI-based summarizer.
|
|
20
|
+
*/
|
|
21
|
+
const createCLISummarizer = async (
|
|
22
|
+
provider: CLIProviderName,
|
|
23
|
+
): Promise<Summarizer> => {
|
|
24
|
+
// Check if CLI is installed
|
|
25
|
+
const isInstalled = await isCLIInstalled(provider)
|
|
26
|
+
if (!isInstalled) {
|
|
27
|
+
const cliInfo = getCLIInfo(provider)
|
|
28
|
+
throw new SummarizationError(
|
|
29
|
+
`CLI tool '${cliInfo?.displayName ?? provider}' is not installed`,
|
|
30
|
+
'PROVIDER_NOT_AVAILABLE',
|
|
31
|
+
provider,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Return appropriate summarizer based on provider
|
|
36
|
+
switch (provider) {
|
|
37
|
+
case 'claude':
|
|
38
|
+
return new ClaudeCLISummarizer()
|
|
39
|
+
|
|
40
|
+
// TODO: Add other CLI providers as needed
|
|
41
|
+
case 'opencode':
|
|
42
|
+
case 'copilot':
|
|
43
|
+
case 'aider':
|
|
44
|
+
case 'cline':
|
|
45
|
+
case 'amp':
|
|
46
|
+
throw new SummarizationError(
|
|
47
|
+
`CLI provider '${provider}' is not yet implemented`,
|
|
48
|
+
'PROVIDER_NOT_FOUND',
|
|
49
|
+
provider,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
throw new SummarizationError(
|
|
54
|
+
`Unknown CLI provider: ${provider}`,
|
|
55
|
+
'PROVIDER_NOT_FOUND',
|
|
56
|
+
provider,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create an API-based summarizer.
|
|
63
|
+
*
|
|
64
|
+
* Uses Vercel AI SDK for provider abstraction.
|
|
65
|
+
* Requires appropriate API keys to be configured.
|
|
66
|
+
*/
|
|
67
|
+
const createAPISummarizer = async (
|
|
68
|
+
_provider: APIProviderName,
|
|
69
|
+
_config: AISummarizationConfig,
|
|
70
|
+
): Promise<Summarizer> => {
|
|
71
|
+
// TODO: Implement API providers using Vercel AI SDK
|
|
72
|
+
// This will be implemented in a later issue (ALP-220)
|
|
73
|
+
throw new SummarizationError(
|
|
74
|
+
'API providers are not yet implemented. Use CLI providers for now.',
|
|
75
|
+
'PROVIDER_NOT_FOUND',
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a summarizer based on configuration.
|
|
81
|
+
*
|
|
82
|
+
* @param config - Summarization configuration
|
|
83
|
+
* @returns A configured Summarizer instance
|
|
84
|
+
* @throws SummarizationError if provider is not available or configured
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* Type guard to check if a provider is a CLI provider
|
|
88
|
+
*/
|
|
89
|
+
const isCLIProvider = (provider: string): provider is CLIProviderName => {
|
|
90
|
+
return ['claude', 'copilot', 'cline', 'aider', 'opencode', 'amp'].includes(
|
|
91
|
+
provider,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Type guard to check if a provider is an API provider
|
|
97
|
+
*/
|
|
98
|
+
const isAPIProvider = (provider: string): provider is APIProviderName => {
|
|
99
|
+
return ['deepseek', 'anthropic', 'openai', 'gemini', 'qwen'].includes(
|
|
100
|
+
provider,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const createSummarizer = async (
|
|
105
|
+
config: AISummarizationConfig,
|
|
106
|
+
): Promise<Summarizer> => {
|
|
107
|
+
if (config.mode === 'cli') {
|
|
108
|
+
if (!isCLIProvider(config.provider)) {
|
|
109
|
+
throw new SummarizationError(
|
|
110
|
+
`Invalid CLI provider: ${config.provider}`,
|
|
111
|
+
'PROVIDER_NOT_FOUND',
|
|
112
|
+
config.provider,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return createCLISummarizer(config.provider)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (config.mode === 'api') {
|
|
119
|
+
if (!isAPIProvider(config.provider)) {
|
|
120
|
+
throw new SummarizationError(
|
|
121
|
+
`Invalid API provider: ${config.provider}`,
|
|
122
|
+
'PROVIDER_NOT_FOUND',
|
|
123
|
+
config.provider,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
return createAPISummarizer(config.provider, config)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new SummarizationError(
|
|
130
|
+
`Unknown summarization mode: ${config.mode}`,
|
|
131
|
+
'PROVIDER_NOT_FOUND',
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the best available summarizer.
|
|
137
|
+
*
|
|
138
|
+
* Checks CLI providers first (free), then falls back to API providers.
|
|
139
|
+
* Returns null if no providers are available.
|
|
140
|
+
*/
|
|
141
|
+
export const getBestAvailableSummarizer = async (
|
|
142
|
+
preferredConfig?: Partial<AISummarizationConfig>,
|
|
143
|
+
): Promise<{
|
|
144
|
+
summarizer: Summarizer
|
|
145
|
+
config: AISummarizationConfig
|
|
146
|
+
} | null> => {
|
|
147
|
+
// If config specifies a provider, try that first
|
|
148
|
+
if (preferredConfig?.provider && preferredConfig?.mode) {
|
|
149
|
+
try {
|
|
150
|
+
const summarizer = await createSummarizer(
|
|
151
|
+
preferredConfig as AISummarizationConfig,
|
|
152
|
+
)
|
|
153
|
+
return {
|
|
154
|
+
summarizer,
|
|
155
|
+
config: preferredConfig as AISummarizationConfig,
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Fall through to auto-detection
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Try Claude CLI first (most common)
|
|
163
|
+
if (await isCLIInstalled('claude')) {
|
|
164
|
+
const config: AISummarizationConfig = {
|
|
165
|
+
mode: 'cli',
|
|
166
|
+
provider: 'claude',
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
summarizer: new ClaudeCLISummarizer(),
|
|
170
|
+
config,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// TODO: Try other CLI providers
|
|
175
|
+
// TODO: Try API providers with configured keys
|
|
176
|
+
|
|
177
|
+
return null
|
|
178
|
+
}
|