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,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for wink-bm25-text-search
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
declare module 'wink-bm25-text-search' {
|
|
6
|
+
interface BM25Config {
|
|
7
|
+
fldWeights?: Record<string, number>
|
|
8
|
+
bm25Params?: {
|
|
9
|
+
k1?: number
|
|
10
|
+
b?: number
|
|
11
|
+
}
|
|
12
|
+
ovFldNames?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type PrepTask = (text: string) => string[]
|
|
16
|
+
|
|
17
|
+
interface BM25Engine {
|
|
18
|
+
defineConfig(config: BM25Config): void
|
|
19
|
+
definePrepTasks(tasks: PrepTask[]): void
|
|
20
|
+
addDoc(doc: Record<string, string>, id: number): void
|
|
21
|
+
consolidate(): void
|
|
22
|
+
search(query: string, limit?: number): [number, number][]
|
|
23
|
+
exportJSON(): string
|
|
24
|
+
importJSON(json: string): void
|
|
25
|
+
reset(): void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function bm25(): BM25Engine
|
|
29
|
+
export default bm25
|
|
30
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI Summarizer
|
|
3
|
+
*
|
|
4
|
+
* Uses Claude Code CLI for AI summarization.
|
|
5
|
+
* FREE for users with Claude Code subscriptions.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY: Uses spawn() with argument arrays - NEVER exec() with string interpolation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process'
|
|
11
|
+
import type {
|
|
12
|
+
StreamingSummarizer,
|
|
13
|
+
StreamOptions,
|
|
14
|
+
SummaryResult,
|
|
15
|
+
} from '../types.js'
|
|
16
|
+
import { SummarizationError as SummarizationErrorClass } from '../types.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Claude CLI provider for summarization.
|
|
20
|
+
*
|
|
21
|
+
* Uses the `claude` CLI tool in non-interactive mode with text output.
|
|
22
|
+
* Requires Claude Code installation and authentication.
|
|
23
|
+
*
|
|
24
|
+
* @security Uses spawn() with argument arrays to prevent shell injection.
|
|
25
|
+
* User input is passed as array elements, never interpolated.
|
|
26
|
+
*
|
|
27
|
+
* @cost Free (uses existing Claude subscription)
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const summarizer = new ClaudeCLISummarizer()
|
|
32
|
+
*
|
|
33
|
+
* // Check availability
|
|
34
|
+
* if (await summarizer.isAvailable()) {
|
|
35
|
+
* const result = await summarizer.summarize(searchResults, prompt)
|
|
36
|
+
* console.log(result.summary)
|
|
37
|
+
* // result.estimatedCost is always 0 (free)
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // Streaming output
|
|
41
|
+
* await summarizer.summarizeStream(searchResults, prompt, {
|
|
42
|
+
* onChunk: (chunk) => process.stdout.write(chunk),
|
|
43
|
+
* onComplete: (result) => console.log(`Done in ${result.durationMs}ms`),
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class ClaudeCLISummarizer implements StreamingSummarizer {
|
|
48
|
+
private readonly command = 'claude'
|
|
49
|
+
|
|
50
|
+
async summarize(input: string, prompt: string): Promise<SummaryResult> {
|
|
51
|
+
const startTime = Date.now()
|
|
52
|
+
const fullPrompt = `${prompt}\n\n${input}`
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
// SECURITY: spawn() with argument array - safe from shell injection
|
|
56
|
+
const proc = spawn(
|
|
57
|
+
this.command,
|
|
58
|
+
['-p', fullPrompt, '--output-format', 'text'],
|
|
59
|
+
{
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
let stdout = ''
|
|
65
|
+
let stderr = ''
|
|
66
|
+
|
|
67
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
68
|
+
stdout += data.toString()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
72
|
+
stderr += data.toString()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
proc.on('close', (code: number | null) => {
|
|
76
|
+
const durationMs = Date.now() - startTime
|
|
77
|
+
|
|
78
|
+
if (code !== 0) {
|
|
79
|
+
reject(
|
|
80
|
+
new SummarizationErrorClass(
|
|
81
|
+
`Claude CLI exited with code ${code}: ${stderr}`,
|
|
82
|
+
'CLI_EXECUTION_FAILED',
|
|
83
|
+
'claude',
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
resolve({
|
|
90
|
+
summary: stdout.trim(),
|
|
91
|
+
provider: 'claude',
|
|
92
|
+
mode: 'cli',
|
|
93
|
+
estimatedCost: 0,
|
|
94
|
+
durationMs,
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
proc.on('error', (error: Error) => {
|
|
99
|
+
reject(
|
|
100
|
+
new SummarizationErrorClass(
|
|
101
|
+
`Failed to spawn Claude CLI: ${error.message}`,
|
|
102
|
+
'CLI_EXECUTION_FAILED',
|
|
103
|
+
'claude',
|
|
104
|
+
error,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async summarizeStream(
|
|
112
|
+
input: string,
|
|
113
|
+
prompt: string,
|
|
114
|
+
options: StreamOptions,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const startTime = Date.now()
|
|
117
|
+
const fullPrompt = `${prompt}\n\n${input}`
|
|
118
|
+
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
// SECURITY: spawn() with argument array - safe from shell injection
|
|
121
|
+
const proc = spawn(
|
|
122
|
+
this.command,
|
|
123
|
+
['-p', fullPrompt, '--output-format', 'text'],
|
|
124
|
+
{
|
|
125
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
let fullOutput = ''
|
|
130
|
+
let stderr = ''
|
|
131
|
+
|
|
132
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
133
|
+
const chunk = data.toString()
|
|
134
|
+
fullOutput += chunk
|
|
135
|
+
options.onChunk(chunk)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
139
|
+
stderr += data.toString()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
proc.on('close', (code: number | null) => {
|
|
143
|
+
const durationMs = Date.now() - startTime
|
|
144
|
+
|
|
145
|
+
if (code !== 0) {
|
|
146
|
+
const error = new SummarizationErrorClass(
|
|
147
|
+
`Claude CLI exited with code ${code}: ${stderr}`,
|
|
148
|
+
'CLI_EXECUTION_FAILED',
|
|
149
|
+
'claude',
|
|
150
|
+
)
|
|
151
|
+
options.onError?.(error)
|
|
152
|
+
reject(error)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result: SummaryResult = {
|
|
157
|
+
summary: fullOutput.trim(),
|
|
158
|
+
provider: 'claude',
|
|
159
|
+
mode: 'cli',
|
|
160
|
+
estimatedCost: 0,
|
|
161
|
+
durationMs,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
options.onComplete?.(result)
|
|
165
|
+
resolve()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
proc.on('error', (error: Error) => {
|
|
169
|
+
const sumError = new SummarizationErrorClass(
|
|
170
|
+
`Failed to spawn Claude CLI: ${error.message}`,
|
|
171
|
+
'CLI_EXECUTION_FAILED',
|
|
172
|
+
'claude',
|
|
173
|
+
error,
|
|
174
|
+
)
|
|
175
|
+
options.onError?.(sumError)
|
|
176
|
+
reject(sumError)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
estimateCost(_inputTokens: number): number {
|
|
182
|
+
// CLI providers are free with subscription
|
|
183
|
+
return 0
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async isAvailable(): Promise<boolean> {
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
const checkCommand = process.platform === 'win32' ? 'where' : 'which'
|
|
189
|
+
const proc = spawn(checkCommand, [this.command], {
|
|
190
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
proc.on('close', (code) => {
|
|
194
|
+
resolve(code === 0)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
proc.on('error', () => {
|
|
198
|
+
resolve(false)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI Provider Detection Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type ChildProcess, spawn } from 'node:child_process'
|
|
6
|
+
import { EventEmitter } from 'node:events'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
import type { CLIProviderName } from '../types.js'
|
|
9
|
+
import {
|
|
10
|
+
detectInstalledCLIs,
|
|
11
|
+
getCLIInfo,
|
|
12
|
+
isCLIInstalled,
|
|
13
|
+
KNOWN_CLIS,
|
|
14
|
+
} from './detection.js'
|
|
15
|
+
|
|
16
|
+
vi.mock('node:child_process', () => ({
|
|
17
|
+
spawn: vi.fn(),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const mockSpawn = vi.mocked(spawn)
|
|
21
|
+
|
|
22
|
+
const createMockProcess = (exitCode: number | null, emitError = false) => {
|
|
23
|
+
const proc = new EventEmitter() as ChildProcess
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
if (emitError) {
|
|
26
|
+
proc.emit('error', new Error('spawn ENOENT'))
|
|
27
|
+
} else {
|
|
28
|
+
proc.emit('close', exitCode)
|
|
29
|
+
}
|
|
30
|
+
}, 0)
|
|
31
|
+
return proc
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('KNOWN_CLIS', () => {
|
|
35
|
+
const expectedProviders: CLIProviderName[] = [
|
|
36
|
+
'claude',
|
|
37
|
+
'opencode',
|
|
38
|
+
'copilot',
|
|
39
|
+
'aider',
|
|
40
|
+
'cline',
|
|
41
|
+
'amp',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
it('should contain all expected CLI providers', () => {
|
|
45
|
+
const names = KNOWN_CLIS.map((cli) => cli.name)
|
|
46
|
+
for (const provider of expectedProviders) {
|
|
47
|
+
expect(names).toContain(provider)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should have required fields for each CLI', () => {
|
|
52
|
+
for (const cli of KNOWN_CLIS) {
|
|
53
|
+
expect(cli).toHaveProperty('name')
|
|
54
|
+
expect(cli).toHaveProperty('command')
|
|
55
|
+
expect(cli).toHaveProperty('displayName')
|
|
56
|
+
expect(cli).toHaveProperty('args')
|
|
57
|
+
expect(cli).toHaveProperty('useStdin')
|
|
58
|
+
|
|
59
|
+
expect(typeof cli.name).toBe('string')
|
|
60
|
+
expect(typeof cli.command).toBe('string')
|
|
61
|
+
expect(typeof cli.displayName).toBe('string')
|
|
62
|
+
expect(Array.isArray(cli.args)).toBe(true)
|
|
63
|
+
expect(typeof cli.useStdin).toBe('boolean')
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('individual CLI configurations', () => {
|
|
68
|
+
it('should have correct claude configuration', () => {
|
|
69
|
+
const claude = KNOWN_CLIS.find((cli) => cli.name === 'claude')
|
|
70
|
+
expect(claude).toBeDefined()
|
|
71
|
+
expect(claude!.command).toBe('claude')
|
|
72
|
+
expect(claude!.displayName).toBe('Claude Code')
|
|
73
|
+
expect(claude!.args).toContain('-p')
|
|
74
|
+
expect(claude!.useStdin).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should have correct opencode configuration', () => {
|
|
78
|
+
const opencode = KNOWN_CLIS.find((cli) => cli.name === 'opencode')
|
|
79
|
+
expect(opencode).toBeDefined()
|
|
80
|
+
expect(opencode!.command).toBe('opencode')
|
|
81
|
+
expect(opencode!.displayName).toBe('OpenCode')
|
|
82
|
+
expect(opencode!.useStdin).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should have correct copilot configuration', () => {
|
|
86
|
+
const copilot = KNOWN_CLIS.find((cli) => cli.name === 'copilot')
|
|
87
|
+
expect(copilot).toBeDefined()
|
|
88
|
+
expect(copilot!.command).toBe('gh')
|
|
89
|
+
expect(copilot!.displayName).toBe('GitHub Copilot CLI')
|
|
90
|
+
expect(copilot!.args).toContain('copilot')
|
|
91
|
+
expect(copilot!.useStdin).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should have correct aider configuration', () => {
|
|
95
|
+
const aider = KNOWN_CLIS.find((cli) => cli.name === 'aider')
|
|
96
|
+
expect(aider).toBeDefined()
|
|
97
|
+
expect(aider!.command).toBe('aider')
|
|
98
|
+
expect(aider!.displayName).toBe('Aider')
|
|
99
|
+
expect(aider!.useStdin).toBe(false)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should have correct cline configuration', () => {
|
|
103
|
+
const cline = KNOWN_CLIS.find((cli) => cli.name === 'cline')
|
|
104
|
+
expect(cline).toBeDefined()
|
|
105
|
+
expect(cline!.command).toBe('cline')
|
|
106
|
+
expect(cline!.displayName).toBe('Cline')
|
|
107
|
+
expect(cline!.useStdin).toBe(false)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should have correct amp configuration', () => {
|
|
111
|
+
const amp = KNOWN_CLIS.find((cli) => cli.name === 'amp')
|
|
112
|
+
expect(amp).toBeDefined()
|
|
113
|
+
expect(amp!.command).toBe('amp')
|
|
114
|
+
expect(amp!.displayName).toBe('Amp')
|
|
115
|
+
expect(amp!.useStdin).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('getCLIInfo', () => {
|
|
121
|
+
it('should return correct info for known providers', () => {
|
|
122
|
+
const claude = getCLIInfo('claude')
|
|
123
|
+
expect(claude).toBeDefined()
|
|
124
|
+
expect(claude!.name).toBe('claude')
|
|
125
|
+
expect(claude!.command).toBe('claude')
|
|
126
|
+
|
|
127
|
+
const copilot = getCLIInfo('copilot')
|
|
128
|
+
expect(copilot).toBeDefined()
|
|
129
|
+
expect(copilot!.name).toBe('copilot')
|
|
130
|
+
expect(copilot!.command).toBe('gh')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should return undefined for unknown provider', () => {
|
|
134
|
+
const unknown = getCLIInfo('unknown' as CLIProviderName)
|
|
135
|
+
expect(unknown).toBeUndefined()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should return all fields for a CLI', () => {
|
|
139
|
+
const cli = getCLIInfo('claude')
|
|
140
|
+
expect(cli).toMatchObject({
|
|
141
|
+
name: 'claude',
|
|
142
|
+
command: 'claude',
|
|
143
|
+
displayName: 'Claude Code',
|
|
144
|
+
args: expect.any(Array),
|
|
145
|
+
useStdin: false,
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('isCLIInstalled', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
vi.clearAllMocks()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
vi.restoreAllMocks()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should return true when CLI exists (exit code 0)', async () => {
|
|
160
|
+
mockSpawn.mockReturnValue(createMockProcess(0))
|
|
161
|
+
|
|
162
|
+
const result = await isCLIInstalled('claude')
|
|
163
|
+
|
|
164
|
+
expect(result).toBe(true)
|
|
165
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
166
|
+
expect.stringMatching(/^(which|where)$/),
|
|
167
|
+
['claude'],
|
|
168
|
+
{ stdio: ['ignore', 'pipe', 'ignore'] },
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should return false when CLI does not exist (exit code 1)', async () => {
|
|
173
|
+
mockSpawn.mockReturnValue(createMockProcess(1))
|
|
174
|
+
|
|
175
|
+
const result = await isCLIInstalled('claude')
|
|
176
|
+
|
|
177
|
+
expect(result).toBe(false)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should return false when spawn emits an error', async () => {
|
|
181
|
+
mockSpawn.mockReturnValue(createMockProcess(null, true))
|
|
182
|
+
|
|
183
|
+
const result = await isCLIInstalled('claude')
|
|
184
|
+
|
|
185
|
+
expect(result).toBe(false)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should return false for unknown provider', async () => {
|
|
189
|
+
const result = await isCLIInstalled('unknown' as CLIProviderName)
|
|
190
|
+
|
|
191
|
+
expect(result).toBe(false)
|
|
192
|
+
expect(mockSpawn).not.toHaveBeenCalled()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should check the correct command for copilot (gh)', async () => {
|
|
196
|
+
mockSpawn.mockReturnValue(createMockProcess(0))
|
|
197
|
+
|
|
198
|
+
await isCLIInstalled('copilot')
|
|
199
|
+
|
|
200
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
201
|
+
expect.stringMatching(/^(which|where)$/),
|
|
202
|
+
['gh'],
|
|
203
|
+
expect.any(Object),
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('detectInstalledCLIs', () => {
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
vi.clearAllMocks()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
afterEach(() => {
|
|
214
|
+
vi.restoreAllMocks()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should return all CLIs when all are installed', async () => {
|
|
218
|
+
mockSpawn.mockReturnValue(createMockProcess(0))
|
|
219
|
+
|
|
220
|
+
const result = await detectInstalledCLIs()
|
|
221
|
+
|
|
222
|
+
expect(result.length).toBe(KNOWN_CLIS.length)
|
|
223
|
+
expect(result.map((cli) => cli.name)).toEqual(
|
|
224
|
+
expect.arrayContaining([
|
|
225
|
+
'claude',
|
|
226
|
+
'opencode',
|
|
227
|
+
'copilot',
|
|
228
|
+
'aider',
|
|
229
|
+
'cline',
|
|
230
|
+
'amp',
|
|
231
|
+
]),
|
|
232
|
+
)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should return empty array when no CLIs are installed', async () => {
|
|
236
|
+
mockSpawn.mockReturnValue(createMockProcess(1))
|
|
237
|
+
|
|
238
|
+
const result = await detectInstalledCLIs()
|
|
239
|
+
|
|
240
|
+
expect(result).toEqual([])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should return only installed CLIs', async () => {
|
|
244
|
+
const installedCommands = new Set(['claude', 'gh'])
|
|
245
|
+
mockSpawn.mockImplementation((_cmd, args) => {
|
|
246
|
+
const command = args[0] as string
|
|
247
|
+
const isInstalled = installedCommands.has(command)
|
|
248
|
+
return createMockProcess(isInstalled ? 0 : 1)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const result = await detectInstalledCLIs()
|
|
252
|
+
|
|
253
|
+
expect(result.length).toBe(2)
|
|
254
|
+
expect(result.map((cli) => cli.name)).toContain('claude')
|
|
255
|
+
expect(result.map((cli) => cli.name)).toContain('copilot')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should handle errors gracefully', async () => {
|
|
259
|
+
mockSpawn.mockReturnValue(createMockProcess(null, true))
|
|
260
|
+
|
|
261
|
+
const result = await detectInstalledCLIs()
|
|
262
|
+
|
|
263
|
+
expect(result).toEqual([])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should check all known CLIs', async () => {
|
|
267
|
+
mockSpawn.mockReturnValue(createMockProcess(0))
|
|
268
|
+
|
|
269
|
+
await detectInstalledCLIs()
|
|
270
|
+
|
|
271
|
+
expect(mockSpawn).toHaveBeenCalledTimes(KNOWN_CLIS.length)
|
|
272
|
+
})
|
|
273
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Provider Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects installed CLI tools that can be used for AI summarization.
|
|
5
|
+
* Uses spawn() with argument arrays for security - NEVER exec() with string interpolation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'node:child_process'
|
|
9
|
+
import type { CLIInfo, CLIProviderName } from '../types.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Known CLI tools with their configuration.
|
|
13
|
+
*
|
|
14
|
+
* SECURITY: All CLI invocations use spawn() with argument arrays.
|
|
15
|
+
* The args array is used directly, never interpolated into strings.
|
|
16
|
+
*/
|
|
17
|
+
export const KNOWN_CLIS: readonly CLIInfo[] = [
|
|
18
|
+
{
|
|
19
|
+
name: 'claude',
|
|
20
|
+
command: 'claude',
|
|
21
|
+
displayName: 'Claude Code',
|
|
22
|
+
args: ['-p', '--output-format', 'text'],
|
|
23
|
+
useStdin: false, // Uses -p flag for prompt, not stdin
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'opencode',
|
|
27
|
+
command: 'opencode',
|
|
28
|
+
displayName: 'OpenCode',
|
|
29
|
+
args: ['run', '--format', 'text'],
|
|
30
|
+
useStdin: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'copilot',
|
|
34
|
+
command: 'gh',
|
|
35
|
+
displayName: 'GitHub Copilot CLI',
|
|
36
|
+
args: ['copilot', 'explain'],
|
|
37
|
+
useStdin: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'aider',
|
|
41
|
+
command: 'aider',
|
|
42
|
+
displayName: 'Aider',
|
|
43
|
+
args: ['--message'],
|
|
44
|
+
useStdin: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'cline',
|
|
48
|
+
command: 'cline',
|
|
49
|
+
displayName: 'Cline',
|
|
50
|
+
args: ['--prompt'],
|
|
51
|
+
useStdin: false,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'amp',
|
|
55
|
+
command: 'amp',
|
|
56
|
+
displayName: 'Amp',
|
|
57
|
+
args: ['--prompt'],
|
|
58
|
+
useStdin: false,
|
|
59
|
+
},
|
|
60
|
+
] as const
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a command exists on the system.
|
|
64
|
+
*
|
|
65
|
+
* SECURITY: Uses spawn() with argument array, not exec() with string interpolation.
|
|
66
|
+
*/
|
|
67
|
+
const commandExists = (command: string): Promise<boolean> => {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
// Use 'which' on Unix, 'where' on Windows
|
|
70
|
+
const checkCommand = process.platform === 'win32' ? 'where' : 'which'
|
|
71
|
+
|
|
72
|
+
const proc = spawn(checkCommand, [command], {
|
|
73
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
proc.on('close', (code) => {
|
|
77
|
+
resolve(code === 0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
proc.on('error', () => {
|
|
81
|
+
resolve(false)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detect all installed CLI tools that can be used for summarization.
|
|
88
|
+
*
|
|
89
|
+
* @returns Array of CLIInfo for installed tools
|
|
90
|
+
*/
|
|
91
|
+
export const detectInstalledCLIs = async (): Promise<CLIInfo[]> => {
|
|
92
|
+
const results = await Promise.all(
|
|
93
|
+
KNOWN_CLIS.map(async (cli) => {
|
|
94
|
+
const exists = await commandExists(cli.command)
|
|
95
|
+
return exists ? cli : null
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return results.filter((cli): cli is CLIInfo => cli !== null)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get CLI info by name.
|
|
104
|
+
*/
|
|
105
|
+
export const getCLIInfo = (name: CLIProviderName): CLIInfo | undefined => {
|
|
106
|
+
return KNOWN_CLIS.find((cli) => cli.name === name)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a specific CLI is installed.
|
|
111
|
+
*/
|
|
112
|
+
export const isCLIInstalled = async (
|
|
113
|
+
name: CLIProviderName,
|
|
114
|
+
): Promise<boolean> => {
|
|
115
|
+
const cli = getCLIInfo(name)
|
|
116
|
+
if (!cli) return false
|
|
117
|
+
return commandExists(cli.command)
|
|
118
|
+
}
|