sofia-cli 0.1.1
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/.github/agents/copilot-instructions.md +39 -0
- package/.github/agents/speckit.analyze.agent.md +184 -0
- package/.github/agents/speckit.checklist.agent.md +294 -0
- package/.github/agents/speckit.clarify.agent.md +181 -0
- package/.github/agents/speckit.constitution.agent.md +84 -0
- package/.github/agents/speckit.implement.agent.md +135 -0
- package/.github/agents/speckit.plan.agent.md +90 -0
- package/.github/agents/speckit.specify.agent.md +258 -0
- package/.github/agents/speckit.tasks.agent.md +137 -0
- package/.github/agents/speckit.taskstoissues.agent.md +30 -0
- package/.github/copilot-instructions.md +257 -0
- package/.github/prompts/speckit.analyze.prompt.md +3 -0
- package/.github/prompts/speckit.checklist.prompt.md +3 -0
- package/.github/prompts/speckit.clarify.prompt.md +3 -0
- package/.github/prompts/speckit.constitution.prompt.md +3 -0
- package/.github/prompts/speckit.implement.prompt.md +3 -0
- package/.github/prompts/speckit.plan.prompt.md +3 -0
- package/.github/prompts/speckit.specify.prompt.md +3 -0
- package/.github/prompts/speckit.tasks.prompt.md +3 -0
- package/.github/prompts/speckit.taskstoissues.prompt.md +3 -0
- package/.github/workflows/ci.yml +38 -0
- package/.prettierrc +6 -0
- package/.specify/memory/constitution.md +181 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +810 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/constitution-template.md +50 -0
- package/.specify/templates/plan-template.md +113 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/.vscode/mcp.json +42 -0
- package/.vscode/settings.json +19 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +213 -0
- package/dist/src/cli/developCommand.js +240 -0
- package/dist/src/cli/directCommands.js +143 -0
- package/dist/src/cli/envLoader.js +16 -0
- package/dist/src/cli/exportCommand.js +53 -0
- package/dist/src/cli/index.js +203 -0
- package/dist/src/cli/ioContext.js +109 -0
- package/dist/src/cli/preflight.js +57 -0
- package/dist/src/cli/statusCommand.js +110 -0
- package/dist/src/cli/workshopCommand.js +400 -0
- package/dist/src/develop/checkpointState.js +86 -0
- package/dist/src/develop/codeGenerator.js +319 -0
- package/dist/src/develop/dynamicScaffolder.js +226 -0
- package/dist/src/develop/githubMcpAdapter.js +122 -0
- package/dist/src/develop/index.js +15 -0
- package/dist/src/develop/mcpContextEnricher.js +195 -0
- package/dist/src/develop/pocScaffolder.js +542 -0
- package/dist/src/develop/ralphLoop.js +659 -0
- package/dist/src/develop/templateRegistry.js +364 -0
- package/dist/src/develop/testRunner.js +202 -0
- package/dist/src/logging/logger.js +58 -0
- package/dist/src/loop/conversationLoop.js +227 -0
- package/dist/src/loop/phaseSummarizer.js +87 -0
- package/dist/src/mcp/mcpManager.js +267 -0
- package/dist/src/mcp/mcpTransport.js +391 -0
- package/dist/src/mcp/retryPolicy.js +47 -0
- package/dist/src/mcp/webSearch.js +254 -0
- package/dist/src/phases/contextSummarizer.js +101 -0
- package/dist/src/phases/discoveryEnricher.js +156 -0
- package/dist/src/phases/phaseExtractors.js +222 -0
- package/dist/src/phases/phaseHandlers.js +328 -0
- package/dist/src/prompts/design.md +51 -0
- package/dist/src/prompts/develop-boundary.md +51 -0
- package/dist/src/prompts/develop.md +111 -0
- package/dist/src/prompts/discover.md +58 -0
- package/dist/src/prompts/ideate.md +56 -0
- package/dist/src/prompts/plan.md +51 -0
- package/dist/src/prompts/promptLoader.js +167 -0
- package/dist/src/prompts/promptLoader.ts +198 -0
- package/dist/src/prompts/select.md +47 -0
- package/dist/src/prompts/summarize/README.md +8 -0
- package/dist/src/prompts/summarize/design-summary.md +37 -0
- package/dist/src/prompts/summarize/develop-summary.md +25 -0
- package/dist/src/prompts/summarize/ideate-summary.md +27 -0
- package/dist/src/prompts/summarize/plan-summary.md +27 -0
- package/dist/src/prompts/summarize/select-summary.md +21 -0
- package/dist/src/prompts/system.md +28 -0
- package/dist/src/sessions/exportPaths.js +22 -0
- package/dist/src/sessions/exportWriter.js +406 -0
- package/dist/src/sessions/sessionManager.js +81 -0
- package/dist/src/sessions/sessionStore.js +65 -0
- package/dist/src/shared/activitySpinner.js +91 -0
- package/dist/src/shared/copilotClient.js +129 -0
- package/dist/src/shared/data/cards.json +1249 -0
- package/dist/src/shared/data/cardsLoader.js +51 -0
- package/dist/src/shared/errorClassifier.js +120 -0
- package/dist/src/shared/events.js +28 -0
- package/dist/src/shared/markdownRenderer.js +34 -0
- package/dist/src/shared/schemas/session.js +265 -0
- package/dist/src/shared/tableRenderer.js +20 -0
- package/dist/src/vendor/chalk.js +2 -0
- package/dist/src/vendor/cli-table3.js +3 -0
- package/dist/src/vendor/commander.js +2 -0
- package/dist/src/vendor/marked-terminal.js +3 -0
- package/dist/src/vendor/marked.js +2 -0
- package/dist/src/vendor/ora.js +2 -0
- package/dist/src/vendor/pino.js +2 -0
- package/dist/src/vendor/zod.js +2 -0
- package/dist/tests/e2e/developE2e.spec.js +126 -0
- package/dist/tests/e2e/developFailureE2e.spec.js +247 -0
- package/dist/tests/e2e/developPty.spec.js +75 -0
- package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +84 -0
- package/dist/tests/e2e/harness.spec.js +83 -0
- package/dist/tests/e2e/mcpLive.spec.js +120 -0
- package/dist/tests/e2e/newSession.e2e.spec.js +177 -0
- package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +62 -0
- package/dist/tests/e2e/workiqEnrichment.spec.js +56 -0
- package/dist/tests/e2e/zavaSimulation.spec.js +452 -0
- package/dist/tests/fixtures/test-fixture-project/src/add.js +3 -0
- package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +6 -0
- package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +8 -0
- package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +10 -0
- package/dist/tests/fixtures/test-fixture-project/vitest.config.js +6 -0
- package/dist/tests/integration/autoStartConversation.spec.js +138 -0
- package/dist/tests/integration/defaultCommand.spec.js +147 -0
- package/dist/tests/integration/directCommandNonTty.spec.js +224 -0
- package/dist/tests/integration/directCommandTty.spec.js +151 -0
- package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +175 -0
- package/dist/tests/integration/exportArtifacts.spec.js +202 -0
- package/dist/tests/integration/exportFallbackFlow.spec.js +99 -0
- package/dist/tests/integration/mcpDegradationFlow.spec.js +190 -0
- package/dist/tests/integration/mcpTransportFlow.spec.js +139 -0
- package/dist/tests/integration/newSessionFlow.spec.js +343 -0
- package/dist/tests/integration/pocGithubMcp.spec.js +186 -0
- package/dist/tests/integration/pocLocalFallback.spec.js +171 -0
- package/dist/tests/integration/pocScaffold.spec.js +163 -0
- package/dist/tests/integration/ralphLoopFlow.spec.js +359 -0
- package/dist/tests/integration/ralphLoopPartial.spec.js +368 -0
- package/dist/tests/integration/resumeAndBacktrack.spec.js +247 -0
- package/dist/tests/integration/spinnerLifecycle.spec.js +220 -0
- package/dist/tests/integration/summarizationFlow.spec.js +115 -0
- package/dist/tests/integration/testRunnerReal.spec.js +52 -0
- package/dist/tests/integration/webSearchAgent.spec.js +128 -0
- package/dist/tests/live/copilotSdkLive.spec.js +107 -0
- package/dist/tests/live/zavaFullWorkshop.spec.js +392 -0
- package/dist/tests/setup/loadEnv.js +3 -0
- package/dist/tests/unit/cli/developCommand.spec.js +567 -0
- package/dist/tests/unit/cli/directCommands.spec.js +279 -0
- package/dist/tests/unit/cli/envLoader.spec.js +58 -0
- package/dist/tests/unit/cli/ioContext.spec.js +119 -0
- package/dist/tests/unit/cli/preflight.spec.js +108 -0
- package/dist/tests/unit/cli/statusCommand.spec.js +111 -0
- package/dist/tests/unit/cli/workshopClientFallback.spec.js +80 -0
- package/dist/tests/unit/cli/workshopCommand.spec.js +329 -0
- package/dist/tests/unit/config/vitestEnvSetup.spec.js +13 -0
- package/dist/tests/unit/develop/checkpointState.spec.js +315 -0
- package/dist/tests/unit/develop/codeGenerator.spec.js +355 -0
- package/dist/tests/unit/develop/githubMcpAdapter.spec.js +231 -0
- package/dist/tests/unit/develop/mcpContextEnricher.spec.js +433 -0
- package/dist/tests/unit/develop/outputValidator.spec.js +119 -0
- package/dist/tests/unit/develop/pocScaffolder.spec.js +353 -0
- package/dist/tests/unit/develop/ralphLoop.spec.js +1248 -0
- package/dist/tests/unit/develop/templateRegistry.spec.js +85 -0
- package/dist/tests/unit/develop/testRunner.spec.js +249 -0
- package/dist/tests/unit/infraBicep.spec.js +92 -0
- package/dist/tests/unit/infraDeploy.spec.js +82 -0
- package/dist/tests/unit/infraTeardown.spec.js +63 -0
- package/dist/tests/unit/logging/logger.spec.js +43 -0
- package/dist/tests/unit/loop/conversationLoop.spec.js +592 -0
- package/dist/tests/unit/loop/phaseSummarizer.spec.js +141 -0
- package/dist/tests/unit/loop/streamingMarkdown.spec.js +147 -0
- package/dist/tests/unit/mcp/mcpManager.spec.js +279 -0
- package/dist/tests/unit/mcp/mcpTransport.spec.js +529 -0
- package/dist/tests/unit/mcp/retryPolicy.spec.js +218 -0
- package/dist/tests/unit/mcp/timeoutValidation.spec.js +46 -0
- package/dist/tests/unit/mcp/webSearch.spec.js +567 -0
- package/dist/tests/unit/phases/contextSummarizer.spec.js +140 -0
- package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +93 -0
- package/dist/tests/unit/phases/discoveryEnricher.spec.js +411 -0
- package/dist/tests/unit/phases/phaseExtractors.spec.js +352 -0
- package/dist/tests/unit/phases/phaseHandlers.spec.js +425 -0
- package/dist/tests/unit/prompts/promptLoader.spec.js +118 -0
- package/dist/tests/unit/schemas/pocSchemas.spec.js +412 -0
- package/dist/tests/unit/schemas/session.spec.js +257 -0
- package/dist/tests/unit/sessions/exportPaths.spec.js +31 -0
- package/dist/tests/unit/sessions/exportWriter.spec.js +655 -0
- package/dist/tests/unit/sessions/sessionManager.spec.js +151 -0
- package/dist/tests/unit/sessions/sessionStore.spec.js +116 -0
- package/dist/tests/unit/shared/activitySpinner.spec.js +175 -0
- package/dist/tests/unit/shared/cardsLoader.spec.js +76 -0
- package/dist/tests/unit/shared/copilotClient.spec.js +155 -0
- package/dist/tests/unit/shared/errorClassifier.spec.js +131 -0
- package/dist/tests/unit/shared/events.spec.js +55 -0
- package/dist/tests/unit/shared/markdownRenderer.spec.js +35 -0
- package/dist/tests/unit/shared/markdownRendererChunks.spec.js +70 -0
- package/dist/tests/unit/shared/tableRenderer.spec.js +34 -0
- package/dist/vitest.config.js +14 -0
- package/dist/vitest.live.config.js +18 -0
- package/docs/README.md +35 -0
- package/docs/architecture.md +169 -0
- package/docs/cli-usage.md +207 -0
- package/docs/environment.md +66 -0
- package/docs/export-format.md +146 -0
- package/docs/session-model.md +113 -0
- package/eslint.config.js +35 -0
- package/infra/deploy.sh +193 -0
- package/infra/gather-env.sh +211 -0
- package/infra/main.bicep +90 -0
- package/infra/main.bicepparam +18 -0
- package/infra/resources.bicep +134 -0
- package/infra/teardown.sh +114 -0
- package/package.json +63 -0
- package/specs/001-cli-workshop-rebuild/checklists/requirements.md +35 -0
- package/specs/001-cli-workshop-rebuild/contracts/cli.md +59 -0
- package/specs/001-cli-workshop-rebuild/contracts/export-summary-json.md +23 -0
- package/specs/001-cli-workshop-rebuild/contracts/session-json.md +30 -0
- package/specs/001-cli-workshop-rebuild/data-model.md +210 -0
- package/specs/001-cli-workshop-rebuild/plan.md +361 -0
- package/specs/001-cli-workshop-rebuild/quickstart.md +83 -0
- package/specs/001-cli-workshop-rebuild/research.md +116 -0
- package/specs/001-cli-workshop-rebuild/spec.md +240 -0
- package/specs/001-cli-workshop-rebuild/tasks.md +476 -0
- package/specs/002-poc-generation/contracts/poc-output.md +172 -0
- package/specs/002-poc-generation/contracts/ralph-loop.md +113 -0
- package/specs/002-poc-generation/data-model.md +172 -0
- package/specs/002-poc-generation/plan.md +109 -0
- package/specs/002-poc-generation/quickstart.md +97 -0
- package/specs/002-poc-generation/research.md +786 -0
- package/specs/002-poc-generation/spec.md +81 -0
- package/specs/002-poc-generation/tasks-fix.md +198 -0
- package/specs/002-poc-generation/tasks.md +252 -0
- package/specs/003-mcp-transport-integration/checklists/requirements.md +37 -0
- package/specs/003-mcp-transport-integration/contracts/context-enricher.md +220 -0
- package/specs/003-mcp-transport-integration/contracts/discovery-enricher.md +267 -0
- package/specs/003-mcp-transport-integration/contracts/github-adapter.md +149 -0
- package/specs/003-mcp-transport-integration/contracts/mcp-transport.md +288 -0
- package/specs/003-mcp-transport-integration/data-model.md +326 -0
- package/specs/003-mcp-transport-integration/plan.md +114 -0
- package/specs/003-mcp-transport-integration/quickstart.md +311 -0
- package/specs/003-mcp-transport-integration/research.md +395 -0
- package/specs/003-mcp-transport-integration/spec.md +234 -0
- package/specs/003-mcp-transport-integration/tasks.md +324 -0
- package/specs/003-next-spec-gaps.md +150 -0
- package/specs/004-dev-resume-hardening/checklists/requirements.md +37 -0
- package/specs/004-dev-resume-hardening/contracts/cli.md +160 -0
- package/specs/004-dev-resume-hardening/data-model.md +321 -0
- package/specs/004-dev-resume-hardening/plan.md +107 -0
- package/specs/004-dev-resume-hardening/quickstart.md +115 -0
- package/specs/004-dev-resume-hardening/research.md +142 -0
- package/specs/004-dev-resume-hardening/spec.md +221 -0
- package/specs/004-dev-resume-hardening/tasks.md +333 -0
- package/specs/005-ai-search-deploy/checklists/requirements.md +39 -0
- package/specs/005-ai-search-deploy/contracts/web-search-tool.md +241 -0
- package/specs/005-ai-search-deploy/data-model.md +130 -0
- package/specs/005-ai-search-deploy/plan.md +93 -0
- package/specs/005-ai-search-deploy/quickstart.md +96 -0
- package/specs/005-ai-search-deploy/research.md +187 -0
- package/specs/005-ai-search-deploy/spec.md +143 -0
- package/specs/005-ai-search-deploy/tasks.md +284 -0
- package/specs/006-workshop-extraction-fixes/checklists/requirements.md +61 -0
- package/specs/006-workshop-extraction-fixes/contracts/summarization-and-export.md +131 -0
- package/specs/006-workshop-extraction-fixes/data-model.md +149 -0
- package/specs/006-workshop-extraction-fixes/plan.md +123 -0
- package/specs/006-workshop-extraction-fixes/quickstart.md +101 -0
- package/specs/006-workshop-extraction-fixes/research.md +143 -0
- package/specs/006-workshop-extraction-fixes/spec.md +210 -0
- package/specs/006-workshop-extraction-fixes/tasks.md +316 -0
- package/src/cli/developCommand.ts +308 -0
- package/src/cli/directCommands.ts +195 -0
- package/src/cli/envLoader.ts +17 -0
- package/src/cli/exportCommand.ts +65 -0
- package/src/cli/index.ts +249 -0
- package/src/cli/ioContext.ts +139 -0
- package/src/cli/preflight.ts +86 -0
- package/src/cli/statusCommand.ts +118 -0
- package/src/cli/workshopCommand.ts +496 -0
- package/src/develop/checkpointState.ts +121 -0
- package/src/develop/codeGenerator.ts +402 -0
- package/src/develop/dynamicScaffolder.ts +284 -0
- package/src/develop/githubMcpAdapter.ts +199 -0
- package/src/develop/index.ts +34 -0
- package/src/develop/mcpContextEnricher.ts +279 -0
- package/src/develop/pocScaffolder.ts +646 -0
- package/src/develop/ralphLoop.ts +1044 -0
- package/src/develop/templateRegistry.ts +427 -0
- package/src/develop/testRunner.ts +276 -0
- package/src/logging/logger.ts +73 -0
- package/src/loop/conversationLoop.ts +355 -0
- package/src/loop/phaseSummarizer.ts +114 -0
- package/src/mcp/mcpManager.ts +365 -0
- package/src/mcp/mcpTransport.ts +562 -0
- package/src/mcp/retryPolicy.ts +87 -0
- package/src/mcp/webSearch.ts +388 -0
- package/src/originalPrompts/design_thinking.md +178 -0
- package/src/originalPrompts/design_thinking_persona.md +76 -0
- package/src/originalPrompts/document_generator_example.md +77 -0
- package/src/originalPrompts/document_generator_persona.md +47 -0
- package/src/originalPrompts/facilitator_persona.md +125 -0
- package/src/originalPrompts/guardrails.md +47 -0
- package/src/phases/contextSummarizer.ts +154 -0
- package/src/phases/discoveryEnricher.ts +223 -0
- package/src/phases/phaseExtractors.ts +247 -0
- package/src/phases/phaseHandlers.ts +450 -0
- package/src/prompts/design.md +51 -0
- package/src/prompts/develop-boundary.md +51 -0
- package/src/prompts/develop.md +111 -0
- package/src/prompts/discover.md +58 -0
- package/src/prompts/ideate.md +56 -0
- package/src/prompts/plan.md +51 -0
- package/src/prompts/promptLoader.ts +198 -0
- package/src/prompts/select.md +47 -0
- package/src/prompts/summarize/README.md +8 -0
- package/src/prompts/summarize/design-summary.md +37 -0
- package/src/prompts/summarize/develop-summary.md +25 -0
- package/src/prompts/summarize/ideate-summary.md +27 -0
- package/src/prompts/summarize/plan-summary.md +27 -0
- package/src/prompts/summarize/select-summary.md +21 -0
- package/src/prompts/system.md +28 -0
- package/src/sessions/exportPaths.ts +28 -0
- package/src/sessions/exportWriter.ts +490 -0
- package/src/sessions/sessionManager.ts +119 -0
- package/src/sessions/sessionStore.ts +69 -0
- package/src/shared/activitySpinner.ts +108 -0
- package/src/shared/copilotClient.ts +291 -0
- package/src/shared/data/cards.json +1249 -0
- package/src/shared/data/cardsLoader.ts +70 -0
- package/src/shared/errorClassifier.ts +160 -0
- package/src/shared/events.ts +103 -0
- package/src/shared/markdownRenderer.ts +44 -0
- package/src/shared/schemas/session.ts +346 -0
- package/src/shared/tableRenderer.ts +28 -0
- package/src/types/marked-terminal.d.ts +5 -0
- package/src/vendor/chalk.ts +2 -0
- package/src/vendor/cli-table3.ts +3 -0
- package/src/vendor/commander.ts +2 -0
- package/src/vendor/marked-terminal.ts +3 -0
- package/src/vendor/marked.ts +2 -0
- package/src/vendor/ora.ts +2 -0
- package/src/vendor/pino.ts +3 -0
- package/src/vendor/zod.ts +3 -0
- package/tests/e2e/developE2e.spec.ts +152 -0
- package/tests/e2e/developFailureE2e.spec.ts +289 -0
- package/tests/e2e/developPty.spec.ts +86 -0
- package/tests/e2e/discoveryWebSearchRelevance.spec.ts +103 -0
- package/tests/e2e/harness.spec.ts +104 -0
- package/tests/e2e/mcpLive.spec.ts +149 -0
- package/tests/e2e/newSession.e2e.spec.ts +245 -0
- package/tests/e2e/ralphLoopEnrichmentComparison.spec.ts +70 -0
- package/tests/e2e/workiqEnrichment.spec.ts +72 -0
- package/tests/e2e/zava-assessment/agent-interaction-script.md +258 -0
- package/tests/e2e/zava-assessment/company-profile.md +98 -0
- package/tests/e2e/zava-assessment/expected-results-checklist.md +454 -0
- package/tests/e2e/zavaSimulation.spec.ts +511 -0
- package/tests/fixtures/completedSession.json +141 -0
- package/tests/fixtures/test-fixture-project/package-lock.json +1585 -0
- package/tests/fixtures/test-fixture-project/package.json +12 -0
- package/tests/fixtures/test-fixture-project/src/add.ts +3 -0
- package/tests/fixtures/test-fixture-project/tests/failing.test.ts +7 -0
- package/tests/fixtures/test-fixture-project/tests/hanging.test.ts +9 -0
- package/tests/fixtures/test-fixture-project/tests/passing.test.ts +13 -0
- package/tests/fixtures/test-fixture-project/vitest.config.ts +7 -0
- package/tests/integration/autoStartConversation.spec.ts +168 -0
- package/tests/integration/defaultCommand.spec.ts +179 -0
- package/tests/integration/directCommandNonTty.spec.ts +260 -0
- package/tests/integration/directCommandTty.spec.ts +185 -0
- package/tests/integration/discoveryEnrichmentFlow.spec.ts +209 -0
- package/tests/integration/exportArtifacts.spec.ts +232 -0
- package/tests/integration/exportFallbackFlow.spec.ts +115 -0
- package/tests/integration/mcpDegradationFlow.spec.ts +231 -0
- package/tests/integration/mcpTransportFlow.spec.ts +178 -0
- package/tests/integration/newSessionFlow.spec.ts +406 -0
- package/tests/integration/pocGithubMcp.spec.ts +224 -0
- package/tests/integration/pocLocalFallback.spec.ts +205 -0
- package/tests/integration/pocScaffold.spec.ts +220 -0
- package/tests/integration/ralphLoopFlow.spec.ts +430 -0
- package/tests/integration/ralphLoopPartial.spec.ts +416 -0
- package/tests/integration/resumeAndBacktrack.spec.ts +278 -0
- package/tests/integration/spinnerLifecycle.spec.ts +270 -0
- package/tests/integration/summarizationFlow.spec.ts +135 -0
- package/tests/integration/testRunnerReal.spec.ts +63 -0
- package/tests/integration/webSearchAgent.spec.ts +155 -0
- package/tests/live/copilotSdkLive.spec.ts +149 -0
- package/tests/live/zavaFullWorkshop.spec.ts +515 -0
- package/tests/setup/loadEnv.ts +5 -0
- package/tests/unit/cli/developCommand.spec.ts +679 -0
- package/tests/unit/cli/directCommands.spec.ts +325 -0
- package/tests/unit/cli/envLoader.spec.ts +73 -0
- package/tests/unit/cli/ioContext.spec.ts +148 -0
- package/tests/unit/cli/preflight.spec.ts +125 -0
- package/tests/unit/cli/statusCommand.spec.ts +134 -0
- package/tests/unit/cli/workshopClientFallback.spec.ts +100 -0
- package/tests/unit/cli/workshopCommand.spec.ts +378 -0
- package/tests/unit/config/vitestEnvSetup.spec.ts +24 -0
- package/tests/unit/develop/checkpointState.spec.ts +378 -0
- package/tests/unit/develop/codeGenerator.spec.ts +447 -0
- package/tests/unit/develop/githubMcpAdapter.spec.ts +283 -0
- package/tests/unit/develop/mcpContextEnricher.spec.ts +564 -0
- package/tests/unit/develop/outputValidator.spec.ts +134 -0
- package/tests/unit/develop/pocScaffolder.spec.ts +451 -0
- package/tests/unit/develop/ralphLoop.spec.ts +1439 -0
- package/tests/unit/develop/templateRegistry.spec.ts +106 -0
- package/tests/unit/develop/testRunner.spec.ts +294 -0
- package/tests/unit/infraBicep.spec.ts +116 -0
- package/tests/unit/infraDeploy.spec.ts +102 -0
- package/tests/unit/infraTeardown.spec.ts +77 -0
- package/tests/unit/logging/logger.spec.ts +50 -0
- package/tests/unit/loop/conversationLoop.spec.ts +719 -0
- package/tests/unit/loop/phaseSummarizer.spec.ts +169 -0
- package/tests/unit/loop/streamingMarkdown.spec.ts +180 -0
- package/tests/unit/mcp/mcpManager.spec.ts +336 -0
- package/tests/unit/mcp/mcpTransport.spec.ts +689 -0
- package/tests/unit/mcp/retryPolicy.spec.ts +278 -0
- package/tests/unit/mcp/timeoutValidation.spec.ts +55 -0
- package/tests/unit/mcp/webSearch.spec.ts +718 -0
- package/tests/unit/phases/contextSummarizer.spec.ts +158 -0
- package/tests/unit/phases/discoveryEnricher.repeatCalls.spec.ts +125 -0
- package/tests/unit/phases/discoveryEnricher.spec.ts +512 -0
- package/tests/unit/phases/phaseExtractors.spec.ts +406 -0
- package/tests/unit/phases/phaseHandlers.spec.ts +483 -0
- package/tests/unit/prompts/promptLoader.spec.ts +144 -0
- package/tests/unit/schemas/pocSchemas.spec.ts +457 -0
- package/tests/unit/schemas/session.spec.ts +328 -0
- package/tests/unit/sessions/exportPaths.spec.ts +38 -0
- package/tests/unit/sessions/exportWriter.spec.ts +737 -0
- package/tests/unit/sessions/sessionManager.spec.ts +174 -0
- package/tests/unit/sessions/sessionStore.spec.ts +136 -0
- package/tests/unit/shared/activitySpinner.spec.ts +211 -0
- package/tests/unit/shared/cardsLoader.spec.ts +89 -0
- package/tests/unit/shared/copilotClient.spec.ts +185 -0
- package/tests/unit/shared/errorClassifier.spec.ts +152 -0
- package/tests/unit/shared/events.spec.ts +71 -0
- package/tests/unit/shared/markdownRenderer.spec.ts +42 -0
- package/tests/unit/shared/markdownRendererChunks.spec.ts +83 -0
- package/tests/unit/shared/tableRenderer.spec.ts +38 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
- package/vitest.live.config.ts +19 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Transport Layer.
|
|
3
|
+
*
|
|
4
|
+
* Provides the `McpTransport` interface and two implementations:
|
|
5
|
+
* - `StdioMcpTransport`: subprocess-based JSON-RPC 2.0 over stdin/stdout
|
|
6
|
+
* - `HttpMcpTransport`: stateless HTTPS JSON-RPC 2.0 via native fetch()
|
|
7
|
+
*
|
|
8
|
+
* These transports handle the *programmatic adapter path* — deterministic tool
|
|
9
|
+
* calls made by application code (GitHub adapter, Context7 enricher, Azure
|
|
10
|
+
* enricher) that bypass the LLM.
|
|
11
|
+
*
|
|
12
|
+
* LLM-initiated tool calls go through the Copilot SDK's native `mcpServers`
|
|
13
|
+
* support. See research.md Topic 1 for the dual-path architecture.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn, execSync } from 'node:child_process';
|
|
17
|
+
import type { ChildProcess } from 'node:child_process';
|
|
18
|
+
import { createInterface } from 'node:readline';
|
|
19
|
+
import type { Logger } from 'pino';
|
|
20
|
+
|
|
21
|
+
import type { McpServerConfig, StdioServerConfig, HttpServerConfig } from './mcpManager.js';
|
|
22
|
+
|
|
23
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Structured response from an MCP tool call.
|
|
27
|
+
*/
|
|
28
|
+
export interface ToolCallResponse {
|
|
29
|
+
/** Parsed tool result content. */
|
|
30
|
+
content: Record<string, unknown> | string;
|
|
31
|
+
/** Raw JSON-RPC response (for debugging). */
|
|
32
|
+
raw?: unknown;
|
|
33
|
+
/** Whether this response came from a retry attempt. */
|
|
34
|
+
wasRetried?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Common interface for all MCP transport implementations.
|
|
39
|
+
* A transport represents a connection to one MCP server.
|
|
40
|
+
*/
|
|
41
|
+
export interface McpTransport {
|
|
42
|
+
/**
|
|
43
|
+
* Invoke a named tool on this MCP server.
|
|
44
|
+
* Throws `McpTransportError` on failure.
|
|
45
|
+
*/
|
|
46
|
+
callTool(
|
|
47
|
+
toolName: string,
|
|
48
|
+
args: Record<string, unknown>,
|
|
49
|
+
timeoutMs: number,
|
|
50
|
+
): Promise<ToolCallResponse>;
|
|
51
|
+
|
|
52
|
+
/** Whether the transport is currently connected. */
|
|
53
|
+
isConnected(): boolean;
|
|
54
|
+
|
|
55
|
+
/** Gracefully disconnect (terminate subprocess or close connection). */
|
|
56
|
+
disconnect(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Error type for MCP transport failures.
|
|
61
|
+
* Callers use `classifyMcpError()` to determine retry eligibility.
|
|
62
|
+
*/
|
|
63
|
+
export class McpTransportError extends Error {
|
|
64
|
+
constructor(
|
|
65
|
+
message: string,
|
|
66
|
+
public readonly serverName: string,
|
|
67
|
+
public readonly toolName: string,
|
|
68
|
+
public readonly cause?: unknown,
|
|
69
|
+
) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = 'McpTransportError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Internal types ───────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
interface PendingRequest {
|
|
78
|
+
resolve: (value: ToolCallResponse) => void;
|
|
79
|
+
reject: (reason: Error) => void;
|
|
80
|
+
timer: ReturnType<typeof setTimeout>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeRpcErrorMessage(message: string): string {
|
|
84
|
+
const trimmed = message.trim();
|
|
85
|
+
|
|
86
|
+
const parseValidationArray = (raw: string): string | null => {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(raw) as Array<{ path?: Array<string | number>; message?: string }>;
|
|
89
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const details = parsed
|
|
94
|
+
.map((item) => {
|
|
95
|
+
const path =
|
|
96
|
+
Array.isArray(item.path) && item.path.length > 0 ? item.path.join('.') : 'input';
|
|
97
|
+
const detail = item.message ?? 'Invalid input';
|
|
98
|
+
return `${path}: ${detail}`;
|
|
99
|
+
})
|
|
100
|
+
.join('; ');
|
|
101
|
+
|
|
102
|
+
return details;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const arraySuffixMatch = trimmed.match(/^(.*?):\s*(\[[\s\S]*\])$/);
|
|
109
|
+
if (arraySuffixMatch) {
|
|
110
|
+
const details = parseValidationArray(arraySuffixMatch[2]);
|
|
111
|
+
if (details) {
|
|
112
|
+
return `${arraySuffixMatch[1]}: ${details}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const wholeArrayDetails = parseValidationArray(trimmed);
|
|
117
|
+
if (wholeArrayDetails) {
|
|
118
|
+
return `Input validation error: ${wholeArrayDetails}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return trimmed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── StdioMcpTransport ────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* MCP transport over stdio subprocess (JSON-RPC 2.0 newline-delimited).
|
|
128
|
+
*
|
|
129
|
+
* Used for: Context7, Azure MCP, WorkIQ, Playwright.
|
|
130
|
+
*/
|
|
131
|
+
export class StdioMcpTransport implements McpTransport {
|
|
132
|
+
private readonly config: StdioServerConfig;
|
|
133
|
+
private readonly logger: Logger;
|
|
134
|
+
private process: ChildProcess | null = null;
|
|
135
|
+
private readonly pendingRequests = new Map<number, PendingRequest>();
|
|
136
|
+
private nextId = 1;
|
|
137
|
+
private connected = false;
|
|
138
|
+
|
|
139
|
+
constructor(config: StdioServerConfig, logger: Logger) {
|
|
140
|
+
this.config = config;
|
|
141
|
+
this.logger = logger;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Spawn the subprocess and perform the JSON-RPC `initialize` handshake.
|
|
146
|
+
* Must be called before `callTool()`.
|
|
147
|
+
*/
|
|
148
|
+
async connect(): Promise<void> {
|
|
149
|
+
const { command, args, env, cwd, name } = this.config;
|
|
150
|
+
|
|
151
|
+
const child = spawn(command, args, {
|
|
152
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
153
|
+
env: { ...process.env, ...env },
|
|
154
|
+
...(cwd ? { cwd } : {}),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.process = child;
|
|
158
|
+
|
|
159
|
+
// Parse stdout lines as JSON-RPC responses
|
|
160
|
+
const rl = createInterface({ input: child.stdout! });
|
|
161
|
+
rl.on('line', (line: string) => {
|
|
162
|
+
try {
|
|
163
|
+
const msg = JSON.parse(line) as {
|
|
164
|
+
id?: number;
|
|
165
|
+
result?: unknown;
|
|
166
|
+
error?: { message: string };
|
|
167
|
+
};
|
|
168
|
+
if (msg.id != null) {
|
|
169
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
170
|
+
if (pending) {
|
|
171
|
+
clearTimeout(pending.timer);
|
|
172
|
+
this.pendingRequests.delete(msg.id);
|
|
173
|
+
|
|
174
|
+
if (msg.error) {
|
|
175
|
+
pending.reject(
|
|
176
|
+
new McpTransportError(
|
|
177
|
+
normalizeRpcErrorMessage(msg.error.message),
|
|
178
|
+
name,
|
|
179
|
+
'rpc-error',
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
pending.resolve({
|
|
184
|
+
content: this.extractContent(msg.result),
|
|
185
|
+
raw: msg,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
this.logger.debug({ line: line.slice(0, 200) }, 'Skipping non-JSON stdout line');
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Handle subprocess exit
|
|
196
|
+
child.on('exit', (code) => {
|
|
197
|
+
this.connected = false;
|
|
198
|
+
// Reject all pending requests
|
|
199
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
200
|
+
clearTimeout(pending.timer);
|
|
201
|
+
this.pendingRequests.delete(id);
|
|
202
|
+
const err = new McpTransportError(
|
|
203
|
+
`MCP subprocess exited with code ${code}`,
|
|
204
|
+
name,
|
|
205
|
+
'subprocess-exit',
|
|
206
|
+
);
|
|
207
|
+
(err as Error & { code?: string }).code = 'ECONNREFUSED';
|
|
208
|
+
pending.reject(err);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Send initialize handshake
|
|
213
|
+
const initId = this.nextId++;
|
|
214
|
+
const initResult = await new Promise<void>((resolve, reject) => {
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
this.pendingRequests.delete(initId);
|
|
217
|
+
child.kill('SIGTERM');
|
|
218
|
+
const err = new McpTransportError(
|
|
219
|
+
`MCP stdio server '${name}' initialization timed out after 5 seconds`,
|
|
220
|
+
name,
|
|
221
|
+
'initialize',
|
|
222
|
+
);
|
|
223
|
+
(err as Error & { code?: string }).code = 'ETIMEDOUT';
|
|
224
|
+
reject(err);
|
|
225
|
+
}, 5000);
|
|
226
|
+
|
|
227
|
+
this.pendingRequests.set(initId, {
|
|
228
|
+
resolve: () => {
|
|
229
|
+
resolve();
|
|
230
|
+
},
|
|
231
|
+
reject,
|
|
232
|
+
timer,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const initRequest = JSON.stringify({
|
|
236
|
+
jsonrpc: '2.0',
|
|
237
|
+
id: initId,
|
|
238
|
+
method: 'initialize',
|
|
239
|
+
params: {
|
|
240
|
+
protocolVersion: '1.0',
|
|
241
|
+
clientInfo: { name: 'sofIA', version: '0.1.0' },
|
|
242
|
+
capabilities: {},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
child.stdin!.write(initRequest + '\n');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
void initResult;
|
|
250
|
+
this.connected = true;
|
|
251
|
+
this.logger.info({ server: name }, 'MCP stdio server connected');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async callTool(
|
|
255
|
+
toolName: string,
|
|
256
|
+
args: Record<string, unknown>,
|
|
257
|
+
timeoutMs: number,
|
|
258
|
+
): Promise<ToolCallResponse> {
|
|
259
|
+
if (!this.connected || !this.process) {
|
|
260
|
+
throw new McpTransportError(
|
|
261
|
+
`Transport not connected for server '${this.config.name}'`,
|
|
262
|
+
this.config.name,
|
|
263
|
+
toolName,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const id = this.nextId++;
|
|
268
|
+
|
|
269
|
+
return new Promise<ToolCallResponse>((resolve, reject) => {
|
|
270
|
+
const timer = setTimeout(() => {
|
|
271
|
+
this.pendingRequests.delete(id);
|
|
272
|
+
const err = new McpTransportError(
|
|
273
|
+
`MCP tool call timed out after ${timeoutMs}ms: ${this.config.name}.${toolName}`,
|
|
274
|
+
this.config.name,
|
|
275
|
+
toolName,
|
|
276
|
+
);
|
|
277
|
+
(err as Error & { code?: string }).code = 'ETIMEDOUT';
|
|
278
|
+
reject(err);
|
|
279
|
+
}, timeoutMs);
|
|
280
|
+
|
|
281
|
+
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
282
|
+
|
|
283
|
+
const request = JSON.stringify({
|
|
284
|
+
jsonrpc: '2.0',
|
|
285
|
+
id,
|
|
286
|
+
method: 'tools/call',
|
|
287
|
+
params: { name: toolName, arguments: args },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
this.process!.stdin!.write(request + '\n');
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
isConnected(): boolean {
|
|
295
|
+
return this.connected;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async disconnect(): Promise<void> {
|
|
299
|
+
// Reject all pending requests
|
|
300
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
301
|
+
clearTimeout(pending.timer);
|
|
302
|
+
this.pendingRequests.delete(id);
|
|
303
|
+
pending.reject(
|
|
304
|
+
new McpTransportError('Transport disconnected', this.config.name, 'disconnect'),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (this.process) {
|
|
309
|
+
this.process.kill('SIGTERM');
|
|
310
|
+
this.process = null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.connected = false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Extract content from a JSON-RPC result.
|
|
318
|
+
* MCP responses may have `result.content[0].text` or just `result` directly.
|
|
319
|
+
*/
|
|
320
|
+
private extractContent(result: unknown): Record<string, unknown> | string {
|
|
321
|
+
if (result && typeof result === 'object') {
|
|
322
|
+
const r = result as Record<string, unknown>;
|
|
323
|
+
if (Array.isArray(r.content) && r.content.length > 0) {
|
|
324
|
+
const first = r.content[0] as Record<string, unknown>;
|
|
325
|
+
if (typeof first.text === 'string') {
|
|
326
|
+
return first.text;
|
|
327
|
+
}
|
|
328
|
+
return first;
|
|
329
|
+
}
|
|
330
|
+
return r;
|
|
331
|
+
}
|
|
332
|
+
return String(result);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── HttpMcpTransport ─────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Retrieve GitHub token from GitHub CLI if available.
|
|
340
|
+
* @returns Token string or null if GitHub CLI is not installed/authenticated.
|
|
341
|
+
*/
|
|
342
|
+
function getGitHubCliToken(): string | null {
|
|
343
|
+
try {
|
|
344
|
+
const token = execSync('gh auth token', {
|
|
345
|
+
encoding: 'utf8',
|
|
346
|
+
stdio: ['pipe', 'pipe', 'ignore'], // suppress stderr
|
|
347
|
+
timeout: 2000,
|
|
348
|
+
}).trim();
|
|
349
|
+
return token || null;
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* MCP transport over HTTPS (stateless JSON-RPC 2.0 via native fetch).
|
|
357
|
+
*
|
|
358
|
+
* Used for: GitHub MCP, Microsoft Docs MCP.
|
|
359
|
+
*/
|
|
360
|
+
export class HttpMcpTransport implements McpTransport {
|
|
361
|
+
private readonly config: HttpServerConfig;
|
|
362
|
+
private readonly logger: Logger;
|
|
363
|
+
private nextId = 1;
|
|
364
|
+
|
|
365
|
+
constructor(config: HttpServerConfig, logger: Logger) {
|
|
366
|
+
this.config = config;
|
|
367
|
+
this.logger = logger;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async callTool(
|
|
371
|
+
toolName: string,
|
|
372
|
+
args: Record<string, unknown>,
|
|
373
|
+
timeoutMs: number,
|
|
374
|
+
): Promise<ToolCallResponse> {
|
|
375
|
+
const id = this.nextId++;
|
|
376
|
+
const body = {
|
|
377
|
+
jsonrpc: '2.0',
|
|
378
|
+
id,
|
|
379
|
+
method: 'tools/call',
|
|
380
|
+
params: { name: toolName, arguments: args },
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Build headers
|
|
384
|
+
const headers: Record<string, string> = {
|
|
385
|
+
'Content-Type': 'application/json',
|
|
386
|
+
...this.config.headers,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Add auth token if available (GITHUB_TOKEN env var takes precedence, fallback to GitHub CLI)
|
|
390
|
+
const token =
|
|
391
|
+
process.env.GITHUB_TOKEN || (this.config.name === 'github' ? getGitHubCliToken() : null);
|
|
392
|
+
if (token) {
|
|
393
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// AbortController for timeout
|
|
397
|
+
const controller = new AbortController();
|
|
398
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const response = await fetch(this.config.url, {
|
|
402
|
+
method: 'POST',
|
|
403
|
+
headers,
|
|
404
|
+
body: JSON.stringify(body),
|
|
405
|
+
signal: controller.signal,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
clearTimeout(timer);
|
|
409
|
+
|
|
410
|
+
// Handle HTTP error statuses
|
|
411
|
+
if (response.status === 401 || response.status === 403) {
|
|
412
|
+
const err = new McpTransportError(
|
|
413
|
+
`HTTP ${response.status} from ${this.config.name}: authentication failed`,
|
|
414
|
+
this.config.name,
|
|
415
|
+
toolName,
|
|
416
|
+
);
|
|
417
|
+
(err as Error & { code?: string }).code = 'ERR_TLS_CERT_ALTNAME_INVALID';
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (response.status >= 500) {
|
|
422
|
+
throw new McpTransportError(
|
|
423
|
+
`HTTP ${response.status} from ${this.config.name}: server error`,
|
|
424
|
+
this.config.name,
|
|
425
|
+
toolName,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Parse response body (read as text first for better error reporting)
|
|
430
|
+
const bodyText = await response.text();
|
|
431
|
+
const contentType = response.headers.get('content-type') || '';
|
|
432
|
+
|
|
433
|
+
let parsed: Record<string, unknown>;
|
|
434
|
+
|
|
435
|
+
// Handle Server-Sent Events (SSE) format from GitHub Copilot MCP
|
|
436
|
+
if (contentType.includes('text/event-stream')) {
|
|
437
|
+
try {
|
|
438
|
+
// Parse SSE format: "event: message\ndata: {...}\n\n"
|
|
439
|
+
const dataMatch = bodyText.match(/^data:\s*(.+)$/m);
|
|
440
|
+
if (!dataMatch) {
|
|
441
|
+
throw new Error('No data field in SSE response');
|
|
442
|
+
}
|
|
443
|
+
parsed = JSON.parse(dataMatch[1]) as Record<string, unknown>;
|
|
444
|
+
} catch (_sseError) {
|
|
445
|
+
this.logger.error(
|
|
446
|
+
{ status: response.status, contentType, bodyPreview: bodyText.slice(0, 500) },
|
|
447
|
+
`Invalid SSE format from ${this.config.name}`,
|
|
448
|
+
);
|
|
449
|
+
throw new McpTransportError(
|
|
450
|
+
`Invalid SSE format from ${this.config.name}`,
|
|
451
|
+
this.config.name,
|
|
452
|
+
toolName,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
// Regular JSON response
|
|
457
|
+
try {
|
|
458
|
+
parsed = JSON.parse(bodyText) as Record<string, unknown>;
|
|
459
|
+
} catch (_parseError) {
|
|
460
|
+
// Log response details for debugging
|
|
461
|
+
const preview = bodyText.slice(0, 500);
|
|
462
|
+
this.logger.error(
|
|
463
|
+
{ status: response.status, contentType, bodyPreview: preview },
|
|
464
|
+
`Non-JSON response from ${this.config.name}`,
|
|
465
|
+
);
|
|
466
|
+
// Also output to stderr for visibility in tests
|
|
467
|
+
console.error(`[McpTransport] Non-JSON response from ${this.config.name}:`);
|
|
468
|
+
console.error(` Status: ${response.status}`);
|
|
469
|
+
console.error(` Content-Type: ${contentType}`);
|
|
470
|
+
console.error(` Body preview: ${preview}`);
|
|
471
|
+
throw new McpTransportError(
|
|
472
|
+
`Non-JSON response from ${this.config.name} (HTTP ${response.status})`,
|
|
473
|
+
this.config.name,
|
|
474
|
+
toolName,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Handle JSON-RPC error
|
|
480
|
+
if (parsed.error) {
|
|
481
|
+
const rpcError = parsed.error as { message?: string };
|
|
482
|
+
throw new McpTransportError(
|
|
483
|
+
normalizeRpcErrorMessage(rpcError.message ?? 'JSON-RPC error'),
|
|
484
|
+
this.config.name,
|
|
485
|
+
toolName,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
content: this.extractContent(parsed.result),
|
|
491
|
+
raw: parsed,
|
|
492
|
+
};
|
|
493
|
+
} catch (err) {
|
|
494
|
+
clearTimeout(timer);
|
|
495
|
+
|
|
496
|
+
// Re-throw McpTransportError as-is
|
|
497
|
+
if (err instanceof McpTransportError) {
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Handle AbortError (timeout)
|
|
502
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
503
|
+
const timeoutErr = new McpTransportError(
|
|
504
|
+
`MCP HTTP call timed out after ${timeoutMs}ms: ${this.config.name}.${toolName}`,
|
|
505
|
+
this.config.name,
|
|
506
|
+
toolName,
|
|
507
|
+
);
|
|
508
|
+
(timeoutErr as Error & { code?: string }).code = 'ETIMEDOUT';
|
|
509
|
+
throw timeoutErr;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Re-throw other errors as McpTransportError
|
|
513
|
+
throw new McpTransportError(
|
|
514
|
+
err instanceof Error ? err.message : String(err),
|
|
515
|
+
this.config.name,
|
|
516
|
+
toolName,
|
|
517
|
+
err,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
isConnected(): boolean {
|
|
523
|
+
return true; // HTTP is stateless
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async disconnect(): Promise<void> {
|
|
527
|
+
// No-op for HTTP
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Extract content from a JSON-RPC result.
|
|
532
|
+
*/
|
|
533
|
+
private extractContent(result: unknown): Record<string, unknown> | string {
|
|
534
|
+
if (result && typeof result === 'object') {
|
|
535
|
+
const r = result as Record<string, unknown>;
|
|
536
|
+
if (Array.isArray(r.content) && r.content.length > 0) {
|
|
537
|
+
const first = r.content[0] as Record<string, unknown>;
|
|
538
|
+
if (typeof first.text === 'string') {
|
|
539
|
+
return first.text;
|
|
540
|
+
}
|
|
541
|
+
return first;
|
|
542
|
+
}
|
|
543
|
+
return r;
|
|
544
|
+
}
|
|
545
|
+
return String(result);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ── Factory ──────────────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Create the correct transport implementation for a server config.
|
|
553
|
+
*/
|
|
554
|
+
export function createTransport(config: McpServerConfig, logger: Logger): McpTransport {
|
|
555
|
+
if (config.type === 'stdio') {
|
|
556
|
+
return new StdioMcpTransport(config, logger);
|
|
557
|
+
}
|
|
558
|
+
if (config.type === 'http') {
|
|
559
|
+
return new HttpMcpTransport(config, logger);
|
|
560
|
+
}
|
|
561
|
+
throw new Error(`Unsupported MCP transport type: ${(config as McpServerConfig).type}`);
|
|
562
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Retry Policy.
|
|
3
|
+
*
|
|
4
|
+
* Provides `withRetry<T>()` to wrap async MCP operations with a single-retry
|
|
5
|
+
* policy for transient errors (connection-refused, timeout, dns-failure).
|
|
6
|
+
*
|
|
7
|
+
* Auth failures and unknown errors are NOT retried.
|
|
8
|
+
*/
|
|
9
|
+
import type { Logger } from 'pino';
|
|
10
|
+
|
|
11
|
+
import { classifyMcpError } from './mcpManager.js';
|
|
12
|
+
import type { McpErrorClass } from './mcpManager.js';
|
|
13
|
+
|
|
14
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface RetryOptions {
|
|
17
|
+
/** MCP server name (for logging). */
|
|
18
|
+
serverName: string;
|
|
19
|
+
/** Tool name (for logging). */
|
|
20
|
+
toolName: string;
|
|
21
|
+
/** Initial delay in ms before retry. Default: 1000. */
|
|
22
|
+
initialDelayMs?: number;
|
|
23
|
+
/** Jitter fraction (0–1). Default: 0.2 (±20%). */
|
|
24
|
+
jitter?: number;
|
|
25
|
+
/** Logger for retry warnings. */
|
|
26
|
+
logger?: Logger;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Retryable error classes ──────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const RETRYABLE_CLASSES: Set<McpErrorClass> = new Set([
|
|
32
|
+
'connection-refused',
|
|
33
|
+
'timeout',
|
|
34
|
+
'dns-failure',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Determine if an error is transient and should be retried.
|
|
39
|
+
*/
|
|
40
|
+
function isRetryable(err: unknown): boolean {
|
|
41
|
+
return RETRYABLE_CLASSES.has(classifyMcpError(err));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── withRetry ────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wrap an async function with a single-retry policy for transient MCP errors.
|
|
48
|
+
*
|
|
49
|
+
* On transient error: waits `initialDelayMs ± jitter`, then calls fn() once more.
|
|
50
|
+
* If the retry also fails, the second error is thrown (not the first).
|
|
51
|
+
*
|
|
52
|
+
* Non-retryable errors (auth-failure, unknown, validation) are thrown immediately.
|
|
53
|
+
*/
|
|
54
|
+
export async function withRetry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T> {
|
|
55
|
+
const { serverName, toolName, initialDelayMs = 1000, jitter = 0.2, logger } = options;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await fn();
|
|
59
|
+
} catch (firstError) {
|
|
60
|
+
if (!isRetryable(firstError)) {
|
|
61
|
+
throw firstError;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Calculate delay with jitter: initialDelayMs * (1 ± jitter)
|
|
65
|
+
const jitterFactor = 1 + (Math.random() * 2 - 1) * jitter;
|
|
66
|
+
const delayMs = Math.round(initialDelayMs * jitterFactor);
|
|
67
|
+
const errorClass = classifyMcpError(firstError);
|
|
68
|
+
|
|
69
|
+
logger?.warn(
|
|
70
|
+
{ server: serverName, tool: toolName, attempt: 1, delayMs, errorClass },
|
|
71
|
+
`MCP transient error — retrying after ${delayMs}ms`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await sleep(delayMs);
|
|
75
|
+
|
|
76
|
+
// Second attempt — if this fails, its error is thrown
|
|
77
|
+
return await fn();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Internal sleep helper. */
|
|
82
|
+
function sleep(ms: number): Promise<void> {
|
|
83
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Re-export for convenience
|
|
87
|
+
export { classifyMcpError, isRetryable };
|