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,391 @@
|
|
|
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
|
+
import { spawn, execSync } from 'node:child_process';
|
|
16
|
+
import { createInterface } from 'node:readline';
|
|
17
|
+
/**
|
|
18
|
+
* Error type for MCP transport failures.
|
|
19
|
+
* Callers use `classifyMcpError()` to determine retry eligibility.
|
|
20
|
+
*/
|
|
21
|
+
export class McpTransportError extends Error {
|
|
22
|
+
serverName;
|
|
23
|
+
toolName;
|
|
24
|
+
cause;
|
|
25
|
+
constructor(message, serverName, toolName, cause) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.serverName = serverName;
|
|
28
|
+
this.toolName = toolName;
|
|
29
|
+
this.cause = cause;
|
|
30
|
+
this.name = 'McpTransportError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function normalizeRpcErrorMessage(message) {
|
|
34
|
+
const trimmed = message.trim();
|
|
35
|
+
const parseValidationArray = (raw) => {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const details = parsed
|
|
42
|
+
.map((item) => {
|
|
43
|
+
const path = Array.isArray(item.path) && item.path.length > 0 ? item.path.join('.') : 'input';
|
|
44
|
+
const detail = item.message ?? 'Invalid input';
|
|
45
|
+
return `${path}: ${detail}`;
|
|
46
|
+
})
|
|
47
|
+
.join('; ');
|
|
48
|
+
return details;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const arraySuffixMatch = trimmed.match(/^(.*?):\s*(\[[\s\S]*\])$/);
|
|
55
|
+
if (arraySuffixMatch) {
|
|
56
|
+
const details = parseValidationArray(arraySuffixMatch[2]);
|
|
57
|
+
if (details) {
|
|
58
|
+
return `${arraySuffixMatch[1]}: ${details}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const wholeArrayDetails = parseValidationArray(trimmed);
|
|
62
|
+
if (wholeArrayDetails) {
|
|
63
|
+
return `Input validation error: ${wholeArrayDetails}`;
|
|
64
|
+
}
|
|
65
|
+
return trimmed;
|
|
66
|
+
}
|
|
67
|
+
// ── StdioMcpTransport ────────────────────────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* MCP transport over stdio subprocess (JSON-RPC 2.0 newline-delimited).
|
|
70
|
+
*
|
|
71
|
+
* Used for: Context7, Azure MCP, WorkIQ, Playwright.
|
|
72
|
+
*/
|
|
73
|
+
export class StdioMcpTransport {
|
|
74
|
+
config;
|
|
75
|
+
logger;
|
|
76
|
+
process = null;
|
|
77
|
+
pendingRequests = new Map();
|
|
78
|
+
nextId = 1;
|
|
79
|
+
connected = false;
|
|
80
|
+
constructor(config, logger) {
|
|
81
|
+
this.config = config;
|
|
82
|
+
this.logger = logger;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Spawn the subprocess and perform the JSON-RPC `initialize` handshake.
|
|
86
|
+
* Must be called before `callTool()`.
|
|
87
|
+
*/
|
|
88
|
+
async connect() {
|
|
89
|
+
const { command, args, env, cwd, name } = this.config;
|
|
90
|
+
const child = spawn(command, args, {
|
|
91
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
92
|
+
env: { ...process.env, ...env },
|
|
93
|
+
...(cwd ? { cwd } : {}),
|
|
94
|
+
});
|
|
95
|
+
this.process = child;
|
|
96
|
+
// Parse stdout lines as JSON-RPC responses
|
|
97
|
+
const rl = createInterface({ input: child.stdout });
|
|
98
|
+
rl.on('line', (line) => {
|
|
99
|
+
try {
|
|
100
|
+
const msg = JSON.parse(line);
|
|
101
|
+
if (msg.id != null) {
|
|
102
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
103
|
+
if (pending) {
|
|
104
|
+
clearTimeout(pending.timer);
|
|
105
|
+
this.pendingRequests.delete(msg.id);
|
|
106
|
+
if (msg.error) {
|
|
107
|
+
pending.reject(new McpTransportError(normalizeRpcErrorMessage(msg.error.message), name, 'rpc-error'));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
pending.resolve({
|
|
111
|
+
content: this.extractContent(msg.result),
|
|
112
|
+
raw: msg,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
this.logger.debug({ line: line.slice(0, 200) }, 'Skipping non-JSON stdout line');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// Handle subprocess exit
|
|
123
|
+
child.on('exit', (code) => {
|
|
124
|
+
this.connected = false;
|
|
125
|
+
// Reject all pending requests
|
|
126
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
127
|
+
clearTimeout(pending.timer);
|
|
128
|
+
this.pendingRequests.delete(id);
|
|
129
|
+
const err = new McpTransportError(`MCP subprocess exited with code ${code}`, name, 'subprocess-exit');
|
|
130
|
+
err.code = 'ECONNREFUSED';
|
|
131
|
+
pending.reject(err);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Send initialize handshake
|
|
135
|
+
const initId = this.nextId++;
|
|
136
|
+
const initResult = await new Promise((resolve, reject) => {
|
|
137
|
+
const timer = setTimeout(() => {
|
|
138
|
+
this.pendingRequests.delete(initId);
|
|
139
|
+
child.kill('SIGTERM');
|
|
140
|
+
const err = new McpTransportError(`MCP stdio server '${name}' initialization timed out after 5 seconds`, name, 'initialize');
|
|
141
|
+
err.code = 'ETIMEDOUT';
|
|
142
|
+
reject(err);
|
|
143
|
+
}, 5000);
|
|
144
|
+
this.pendingRequests.set(initId, {
|
|
145
|
+
resolve: () => {
|
|
146
|
+
resolve();
|
|
147
|
+
},
|
|
148
|
+
reject,
|
|
149
|
+
timer,
|
|
150
|
+
});
|
|
151
|
+
const initRequest = JSON.stringify({
|
|
152
|
+
jsonrpc: '2.0',
|
|
153
|
+
id: initId,
|
|
154
|
+
method: 'initialize',
|
|
155
|
+
params: {
|
|
156
|
+
protocolVersion: '1.0',
|
|
157
|
+
clientInfo: { name: 'sofIA', version: '0.1.0' },
|
|
158
|
+
capabilities: {},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
child.stdin.write(initRequest + '\n');
|
|
162
|
+
});
|
|
163
|
+
void initResult;
|
|
164
|
+
this.connected = true;
|
|
165
|
+
this.logger.info({ server: name }, 'MCP stdio server connected');
|
|
166
|
+
}
|
|
167
|
+
async callTool(toolName, args, timeoutMs) {
|
|
168
|
+
if (!this.connected || !this.process) {
|
|
169
|
+
throw new McpTransportError(`Transport not connected for server '${this.config.name}'`, this.config.name, toolName);
|
|
170
|
+
}
|
|
171
|
+
const id = this.nextId++;
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
this.pendingRequests.delete(id);
|
|
175
|
+
const err = new McpTransportError(`MCP tool call timed out after ${timeoutMs}ms: ${this.config.name}.${toolName}`, this.config.name, toolName);
|
|
176
|
+
err.code = 'ETIMEDOUT';
|
|
177
|
+
reject(err);
|
|
178
|
+
}, timeoutMs);
|
|
179
|
+
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
180
|
+
const request = JSON.stringify({
|
|
181
|
+
jsonrpc: '2.0',
|
|
182
|
+
id,
|
|
183
|
+
method: 'tools/call',
|
|
184
|
+
params: { name: toolName, arguments: args },
|
|
185
|
+
});
|
|
186
|
+
this.process.stdin.write(request + '\n');
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
isConnected() {
|
|
190
|
+
return this.connected;
|
|
191
|
+
}
|
|
192
|
+
async disconnect() {
|
|
193
|
+
// Reject all pending requests
|
|
194
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
195
|
+
clearTimeout(pending.timer);
|
|
196
|
+
this.pendingRequests.delete(id);
|
|
197
|
+
pending.reject(new McpTransportError('Transport disconnected', this.config.name, 'disconnect'));
|
|
198
|
+
}
|
|
199
|
+
if (this.process) {
|
|
200
|
+
this.process.kill('SIGTERM');
|
|
201
|
+
this.process = null;
|
|
202
|
+
}
|
|
203
|
+
this.connected = false;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Extract content from a JSON-RPC result.
|
|
207
|
+
* MCP responses may have `result.content[0].text` or just `result` directly.
|
|
208
|
+
*/
|
|
209
|
+
extractContent(result) {
|
|
210
|
+
if (result && typeof result === 'object') {
|
|
211
|
+
const r = result;
|
|
212
|
+
if (Array.isArray(r.content) && r.content.length > 0) {
|
|
213
|
+
const first = r.content[0];
|
|
214
|
+
if (typeof first.text === 'string') {
|
|
215
|
+
return first.text;
|
|
216
|
+
}
|
|
217
|
+
return first;
|
|
218
|
+
}
|
|
219
|
+
return r;
|
|
220
|
+
}
|
|
221
|
+
return String(result);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ── HttpMcpTransport ─────────────────────────────────────────────────────────
|
|
225
|
+
/**
|
|
226
|
+
* Retrieve GitHub token from GitHub CLI if available.
|
|
227
|
+
* @returns Token string or null if GitHub CLI is not installed/authenticated.
|
|
228
|
+
*/
|
|
229
|
+
function getGitHubCliToken() {
|
|
230
|
+
try {
|
|
231
|
+
const token = execSync('gh auth token', {
|
|
232
|
+
encoding: 'utf8',
|
|
233
|
+
stdio: ['pipe', 'pipe', 'ignore'], // suppress stderr
|
|
234
|
+
timeout: 2000,
|
|
235
|
+
}).trim();
|
|
236
|
+
return token || null;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* MCP transport over HTTPS (stateless JSON-RPC 2.0 via native fetch).
|
|
244
|
+
*
|
|
245
|
+
* Used for: GitHub MCP, Microsoft Docs MCP.
|
|
246
|
+
*/
|
|
247
|
+
export class HttpMcpTransport {
|
|
248
|
+
config;
|
|
249
|
+
logger;
|
|
250
|
+
nextId = 1;
|
|
251
|
+
constructor(config, logger) {
|
|
252
|
+
this.config = config;
|
|
253
|
+
this.logger = logger;
|
|
254
|
+
}
|
|
255
|
+
async callTool(toolName, args, timeoutMs) {
|
|
256
|
+
const id = this.nextId++;
|
|
257
|
+
const body = {
|
|
258
|
+
jsonrpc: '2.0',
|
|
259
|
+
id,
|
|
260
|
+
method: 'tools/call',
|
|
261
|
+
params: { name: toolName, arguments: args },
|
|
262
|
+
};
|
|
263
|
+
// Build headers
|
|
264
|
+
const headers = {
|
|
265
|
+
'Content-Type': 'application/json',
|
|
266
|
+
...this.config.headers,
|
|
267
|
+
};
|
|
268
|
+
// Add auth token if available (GITHUB_TOKEN env var takes precedence, fallback to GitHub CLI)
|
|
269
|
+
const token = process.env.GITHUB_TOKEN || (this.config.name === 'github' ? getGitHubCliToken() : null);
|
|
270
|
+
if (token) {
|
|
271
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
272
|
+
}
|
|
273
|
+
// AbortController for timeout
|
|
274
|
+
const controller = new AbortController();
|
|
275
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(this.config.url, {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers,
|
|
280
|
+
body: JSON.stringify(body),
|
|
281
|
+
signal: controller.signal,
|
|
282
|
+
});
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
// Handle HTTP error statuses
|
|
285
|
+
if (response.status === 401 || response.status === 403) {
|
|
286
|
+
const err = new McpTransportError(`HTTP ${response.status} from ${this.config.name}: authentication failed`, this.config.name, toolName);
|
|
287
|
+
err.code = 'ERR_TLS_CERT_ALTNAME_INVALID';
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
if (response.status >= 500) {
|
|
291
|
+
throw new McpTransportError(`HTTP ${response.status} from ${this.config.name}: server error`, this.config.name, toolName);
|
|
292
|
+
}
|
|
293
|
+
// Parse response body (read as text first for better error reporting)
|
|
294
|
+
const bodyText = await response.text();
|
|
295
|
+
const contentType = response.headers.get('content-type') || '';
|
|
296
|
+
let parsed;
|
|
297
|
+
// Handle Server-Sent Events (SSE) format from GitHub Copilot MCP
|
|
298
|
+
if (contentType.includes('text/event-stream')) {
|
|
299
|
+
try {
|
|
300
|
+
// Parse SSE format: "event: message\ndata: {...}\n\n"
|
|
301
|
+
const dataMatch = bodyText.match(/^data:\s*(.+)$/m);
|
|
302
|
+
if (!dataMatch) {
|
|
303
|
+
throw new Error('No data field in SSE response');
|
|
304
|
+
}
|
|
305
|
+
parsed = JSON.parse(dataMatch[1]);
|
|
306
|
+
}
|
|
307
|
+
catch (_sseError) {
|
|
308
|
+
this.logger.error({ status: response.status, contentType, bodyPreview: bodyText.slice(0, 500) }, `Invalid SSE format from ${this.config.name}`);
|
|
309
|
+
throw new McpTransportError(`Invalid SSE format from ${this.config.name}`, this.config.name, toolName);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// Regular JSON response
|
|
314
|
+
try {
|
|
315
|
+
parsed = JSON.parse(bodyText);
|
|
316
|
+
}
|
|
317
|
+
catch (_parseError) {
|
|
318
|
+
// Log response details for debugging
|
|
319
|
+
const preview = bodyText.slice(0, 500);
|
|
320
|
+
this.logger.error({ status: response.status, contentType, bodyPreview: preview }, `Non-JSON response from ${this.config.name}`);
|
|
321
|
+
// Also output to stderr for visibility in tests
|
|
322
|
+
console.error(`[McpTransport] Non-JSON response from ${this.config.name}:`);
|
|
323
|
+
console.error(` Status: ${response.status}`);
|
|
324
|
+
console.error(` Content-Type: ${contentType}`);
|
|
325
|
+
console.error(` Body preview: ${preview}`);
|
|
326
|
+
throw new McpTransportError(`Non-JSON response from ${this.config.name} (HTTP ${response.status})`, this.config.name, toolName);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Handle JSON-RPC error
|
|
330
|
+
if (parsed.error) {
|
|
331
|
+
const rpcError = parsed.error;
|
|
332
|
+
throw new McpTransportError(normalizeRpcErrorMessage(rpcError.message ?? 'JSON-RPC error'), this.config.name, toolName);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
content: this.extractContent(parsed.result),
|
|
336
|
+
raw: parsed,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
// Re-throw McpTransportError as-is
|
|
342
|
+
if (err instanceof McpTransportError) {
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
// Handle AbortError (timeout)
|
|
346
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
347
|
+
const timeoutErr = new McpTransportError(`MCP HTTP call timed out after ${timeoutMs}ms: ${this.config.name}.${toolName}`, this.config.name, toolName);
|
|
348
|
+
timeoutErr.code = 'ETIMEDOUT';
|
|
349
|
+
throw timeoutErr;
|
|
350
|
+
}
|
|
351
|
+
// Re-throw other errors as McpTransportError
|
|
352
|
+
throw new McpTransportError(err instanceof Error ? err.message : String(err), this.config.name, toolName, err);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
isConnected() {
|
|
356
|
+
return true; // HTTP is stateless
|
|
357
|
+
}
|
|
358
|
+
async disconnect() {
|
|
359
|
+
// No-op for HTTP
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Extract content from a JSON-RPC result.
|
|
363
|
+
*/
|
|
364
|
+
extractContent(result) {
|
|
365
|
+
if (result && typeof result === 'object') {
|
|
366
|
+
const r = result;
|
|
367
|
+
if (Array.isArray(r.content) && r.content.length > 0) {
|
|
368
|
+
const first = r.content[0];
|
|
369
|
+
if (typeof first.text === 'string') {
|
|
370
|
+
return first.text;
|
|
371
|
+
}
|
|
372
|
+
return first;
|
|
373
|
+
}
|
|
374
|
+
return r;
|
|
375
|
+
}
|
|
376
|
+
return String(result);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// ── Factory ──────────────────────────────────────────────────────────────────
|
|
380
|
+
/**
|
|
381
|
+
* Create the correct transport implementation for a server config.
|
|
382
|
+
*/
|
|
383
|
+
export function createTransport(config, logger) {
|
|
384
|
+
if (config.type === 'stdio') {
|
|
385
|
+
return new StdioMcpTransport(config, logger);
|
|
386
|
+
}
|
|
387
|
+
if (config.type === 'http') {
|
|
388
|
+
return new HttpMcpTransport(config, logger);
|
|
389
|
+
}
|
|
390
|
+
throw new Error(`Unsupported MCP transport type: ${config.type}`);
|
|
391
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { classifyMcpError } from './mcpManager.js';
|
|
2
|
+
// ── Retryable error classes ──────────────────────────────────────────────────
|
|
3
|
+
const RETRYABLE_CLASSES = new Set([
|
|
4
|
+
'connection-refused',
|
|
5
|
+
'timeout',
|
|
6
|
+
'dns-failure',
|
|
7
|
+
]);
|
|
8
|
+
/**
|
|
9
|
+
* Determine if an error is transient and should be retried.
|
|
10
|
+
*/
|
|
11
|
+
function isRetryable(err) {
|
|
12
|
+
return RETRYABLE_CLASSES.has(classifyMcpError(err));
|
|
13
|
+
}
|
|
14
|
+
// ── withRetry ────────────────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Wrap an async function with a single-retry policy for transient MCP errors.
|
|
17
|
+
*
|
|
18
|
+
* On transient error: waits `initialDelayMs ± jitter`, then calls fn() once more.
|
|
19
|
+
* If the retry also fails, the second error is thrown (not the first).
|
|
20
|
+
*
|
|
21
|
+
* Non-retryable errors (auth-failure, unknown, validation) are thrown immediately.
|
|
22
|
+
*/
|
|
23
|
+
export async function withRetry(fn, options) {
|
|
24
|
+
const { serverName, toolName, initialDelayMs = 1000, jitter = 0.2, logger } = options;
|
|
25
|
+
try {
|
|
26
|
+
return await fn();
|
|
27
|
+
}
|
|
28
|
+
catch (firstError) {
|
|
29
|
+
if (!isRetryable(firstError)) {
|
|
30
|
+
throw firstError;
|
|
31
|
+
}
|
|
32
|
+
// Calculate delay with jitter: initialDelayMs * (1 ± jitter)
|
|
33
|
+
const jitterFactor = 1 + (Math.random() * 2 - 1) * jitter;
|
|
34
|
+
const delayMs = Math.round(initialDelayMs * jitterFactor);
|
|
35
|
+
const errorClass = classifyMcpError(firstError);
|
|
36
|
+
logger?.warn({ server: serverName, tool: toolName, attempt: 1, delayMs, errorClass }, `MCP transient error — retrying after ${delayMs}ms`);
|
|
37
|
+
await sleep(delayMs);
|
|
38
|
+
// Second attempt — if this fails, its error is thrown
|
|
39
|
+
return await fn();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Internal sleep helper. */
|
|
43
|
+
function sleep(ms) {
|
|
44
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
}
|
|
46
|
+
// Re-export for convenience
|
|
47
|
+
export { classifyMcpError, isRetryable };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// ── Tool definition ──────────────────────────────────────────────────────────
|
|
2
|
+
export const WEB_SEARCH_TOOL_DEFINITION = {
|
|
3
|
+
name: 'web.search',
|
|
4
|
+
description: 'Search the web for information about companies, industries, technologies, and trends. ' +
|
|
5
|
+
'Returns structured results with title, URL, and snippet.',
|
|
6
|
+
parameters: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
query: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'The search query string.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
required: ['query'],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
// ── Configuration check ──────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Check if web search is configured via environment variables.
|
|
20
|
+
*
|
|
21
|
+
* Uses the new env vars (`FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_DEPLOYMENT_NAME`)
|
|
22
|
+
* instead of legacy vars (`SOFIA_FOUNDRY_AGENT_ENDPOINT`, `SOFIA_FOUNDRY_AGENT_KEY`).
|
|
23
|
+
*/
|
|
24
|
+
export function isWebSearchConfigured() {
|
|
25
|
+
return Boolean(process.env.FOUNDRY_PROJECT_ENDPOINT && process.env.FOUNDRY_MODEL_DEPLOYMENT_NAME);
|
|
26
|
+
}
|
|
27
|
+
// ── Citation extraction ──────────────────────────────────────────────────────
|
|
28
|
+
/**
|
|
29
|
+
* Extract citations from a Foundry agent response.
|
|
30
|
+
*
|
|
31
|
+
* Parses `url_citation` annotations from the response output items and maps
|
|
32
|
+
* them into `WebSearchResultItem[]`. Deduplicates sources by URL.
|
|
33
|
+
*/
|
|
34
|
+
export function extractCitations(output) {
|
|
35
|
+
const results = [];
|
|
36
|
+
const seenUrls = new Set();
|
|
37
|
+
for (const item of output) {
|
|
38
|
+
const messageItem = item;
|
|
39
|
+
if (messageItem.type !== 'message')
|
|
40
|
+
continue;
|
|
41
|
+
const content = messageItem.content;
|
|
42
|
+
if (!Array.isArray(content))
|
|
43
|
+
continue;
|
|
44
|
+
for (const contentBlock of content) {
|
|
45
|
+
const block = contentBlock;
|
|
46
|
+
if (block.type !== 'output_text')
|
|
47
|
+
continue;
|
|
48
|
+
const text = String(block.text ?? '');
|
|
49
|
+
const annotations = block.annotations;
|
|
50
|
+
if (!Array.isArray(annotations))
|
|
51
|
+
continue;
|
|
52
|
+
for (const annotation of annotations) {
|
|
53
|
+
const ann = annotation;
|
|
54
|
+
if (ann.type !== 'url_citation')
|
|
55
|
+
continue;
|
|
56
|
+
const url = String(ann.url ?? '');
|
|
57
|
+
const title = String(ann.title ?? url);
|
|
58
|
+
const startIndex = Number(ann.start_index ?? 0);
|
|
59
|
+
const endIndex = Number(ann.end_index ?? text.length);
|
|
60
|
+
const snippet = text.slice(startIndex, Math.min(endIndex, startIndex + 200)) || title;
|
|
61
|
+
if (url && !seenUrls.has(url)) {
|
|
62
|
+
seenUrls.add(url);
|
|
63
|
+
results.push({ title, url, snippet });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { results, sources: [...seenUrls] };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fallback extraction when a response has no URL citations.
|
|
72
|
+
*
|
|
73
|
+
* Some Foundry responses can contain only plain output text without
|
|
74
|
+
* `url_citation` annotations. This extracts text blocks into lightweight
|
|
75
|
+
* snippets so downstream enrichment still has useful context.
|
|
76
|
+
*/
|
|
77
|
+
export function extractTextSnippets(output) {
|
|
78
|
+
const snippets = [];
|
|
79
|
+
for (const item of output) {
|
|
80
|
+
const messageItem = item;
|
|
81
|
+
if (messageItem.type !== 'message')
|
|
82
|
+
continue;
|
|
83
|
+
const content = messageItem.content;
|
|
84
|
+
if (!Array.isArray(content))
|
|
85
|
+
continue;
|
|
86
|
+
for (let i = 0; i < content.length; i++) {
|
|
87
|
+
const block = content[i];
|
|
88
|
+
if (block.type !== 'output_text')
|
|
89
|
+
continue;
|
|
90
|
+
const text = String(block.text ?? '').trim();
|
|
91
|
+
if (!text)
|
|
92
|
+
continue;
|
|
93
|
+
snippets.push({
|
|
94
|
+
title: 'Foundry response',
|
|
95
|
+
url: `foundry://response/${i + 1}`,
|
|
96
|
+
snippet: text.length > 300 ? `${text.slice(0, 300)}…` : text,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return snippets;
|
|
101
|
+
}
|
|
102
|
+
const MAX_QUERIES_PER_AGENT = 3;
|
|
103
|
+
const MAX_RETRIES = 2;
|
|
104
|
+
const RETRY_DELAY_MS = 2000; // 2 second initial delay
|
|
105
|
+
let sessionState = null;
|
|
106
|
+
let sessionDeps = null;
|
|
107
|
+
/**
|
|
108
|
+
* Create default dependencies using the real Azure SDK.
|
|
109
|
+
*/
|
|
110
|
+
async function createDefaultDeps() {
|
|
111
|
+
const { AIProjectClient } = await import('@azure/ai-projects');
|
|
112
|
+
const { DefaultAzureCredential } = await import('@azure/identity');
|
|
113
|
+
return {
|
|
114
|
+
createClient: (endpoint) => new AIProjectClient(endpoint, new DefaultAzureCredential()),
|
|
115
|
+
getOpenAIClient: async (client) => client.getOpenAIClient(),
|
|
116
|
+
createAgentVersion: async (client, name, options) => {
|
|
117
|
+
const aiClient = client;
|
|
118
|
+
const result = await aiClient.agents.createVersion(name, options);
|
|
119
|
+
return { name: result.name, version: result.version };
|
|
120
|
+
},
|
|
121
|
+
deleteAgentVersion: async (client, name, version) => {
|
|
122
|
+
await client.agents.deleteVersion(name, version);
|
|
123
|
+
},
|
|
124
|
+
createConversation: async (openAIClient) => {
|
|
125
|
+
const oai = openAIClient;
|
|
126
|
+
return oai.conversations.create();
|
|
127
|
+
},
|
|
128
|
+
deleteConversation: async (openAIClient, id) => {
|
|
129
|
+
const oai = openAIClient;
|
|
130
|
+
await oai.conversations.delete(id);
|
|
131
|
+
},
|
|
132
|
+
createResponse: async (openAIClient, conversationId, input, agentName) => {
|
|
133
|
+
const oai = openAIClient;
|
|
134
|
+
return oai.responses.create({ conversation: conversationId, input }, { body: { agent: { name: agentName, type: 'agent_reference' } } });
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// ── Tool factory ─────────────────────────────────────────────────────────────
|
|
139
|
+
async function sleep(ms) {
|
|
140
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
141
|
+
}
|
|
142
|
+
async function cleanupCurrentAgent(keepDeps) {
|
|
143
|
+
if (!sessionState?.initialized || !sessionDeps) {
|
|
144
|
+
sessionState = null;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const { client, agentName, agentVersion } = sessionState;
|
|
148
|
+
sessionState = null;
|
|
149
|
+
try {
|
|
150
|
+
await sessionDeps.deleteAgentVersion(client, agentName, agentVersion);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Best-effort cleanup.
|
|
154
|
+
}
|
|
155
|
+
if (!keepDeps) {
|
|
156
|
+
sessionDeps = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Create a web search function that calls the Azure AI Foundry Agent Service.
|
|
161
|
+
*
|
|
162
|
+
* The returned function:
|
|
163
|
+
* - Lazily creates an ephemeral agent with web_search_preview on first call
|
|
164
|
+
* - Reuses the agent for subsequent calls
|
|
165
|
+
* - Rotates the agent after a few queries to avoid stale response behavior
|
|
166
|
+
* - Uses a fresh conversation per query to keep citation output stable
|
|
167
|
+
* - Returns structured results with URL citations
|
|
168
|
+
* - Degrades gracefully on errors (returns empty results with degraded flag)
|
|
169
|
+
*
|
|
170
|
+
* Pass `deps` for testing to inject mocked SDK clients.
|
|
171
|
+
*/
|
|
172
|
+
export function createWebSearchTool(config, deps) {
|
|
173
|
+
sessionDeps = deps ?? null;
|
|
174
|
+
return async (query) => {
|
|
175
|
+
try {
|
|
176
|
+
const resolvedDeps = sessionDeps ?? (await createDefaultDeps());
|
|
177
|
+
sessionDeps = resolvedDeps;
|
|
178
|
+
if (sessionState?.initialized && sessionState.queryCount >= MAX_QUERIES_PER_AGENT) {
|
|
179
|
+
await cleanupCurrentAgent(true);
|
|
180
|
+
}
|
|
181
|
+
// Lazy initialization
|
|
182
|
+
if (!sessionState?.initialized) {
|
|
183
|
+
const client = resolvedDeps.createClient(config.projectEndpoint);
|
|
184
|
+
const openAIClient = await resolvedDeps.getOpenAIClient(client);
|
|
185
|
+
const agent = await resolvedDeps.createAgentVersion(client, 'sofia-web-search', {
|
|
186
|
+
kind: 'prompt',
|
|
187
|
+
model: config.modelDeploymentName,
|
|
188
|
+
instructions: 'You are a web search assistant. Search the web and return relevant results with citations.',
|
|
189
|
+
tools: [{ type: 'web_search_preview' }],
|
|
190
|
+
});
|
|
191
|
+
sessionState = {
|
|
192
|
+
client,
|
|
193
|
+
openAIClient,
|
|
194
|
+
agentName: agent.name,
|
|
195
|
+
agentVersion: agent.version,
|
|
196
|
+
queryCount: 0,
|
|
197
|
+
initialized: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Execute query in an isolated conversation with retry logic for rate limiting.
|
|
201
|
+
const conversation = await sessionDeps.createConversation(sessionState.openAIClient);
|
|
202
|
+
let response;
|
|
203
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
204
|
+
try {
|
|
205
|
+
response = await sessionDeps.createResponse(sessionState.openAIClient, conversation.id, query, sessionState.agentName);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
210
|
+
// Check for 429 rate limiting
|
|
211
|
+
if (message.includes('429') && attempt < MAX_RETRIES) {
|
|
212
|
+
const delayMs = RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
213
|
+
await sleep(delayMs);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
await sessionDeps.deleteConversation(sessionState.openAIClient, conversation.id);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Conversation cleanup failures should not fail web search results.
|
|
224
|
+
}
|
|
225
|
+
sessionState.queryCount += 1;
|
|
226
|
+
// Extract citations. response is guaranteed to be assigned by loop logic:
|
|
227
|
+
// either break assigns it, or catch throws (exiting function).
|
|
228
|
+
const { results: citationResults, sources } = extractCitations(response.output ?? []);
|
|
229
|
+
const results = citationResults.length > 0 ? citationResults : extractTextSnippets(response.output ?? []);
|
|
230
|
+
return { results, sources };
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
234
|
+
return {
|
|
235
|
+
results: [],
|
|
236
|
+
degraded: true,
|
|
237
|
+
error: message,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// ── Session cleanup ──────────────────────────────────────────────────────────
|
|
243
|
+
/**
|
|
244
|
+
* Destroy the ephemeral web search agent and conversation.
|
|
245
|
+
*
|
|
246
|
+
* Safe to call multiple times. Logs warnings on cleanup failure but does not throw.
|
|
247
|
+
*/
|
|
248
|
+
export async function destroyWebSearchSession() {
|
|
249
|
+
await cleanupCurrentAgent(false);
|
|
250
|
+
}
|
|
251
|
+
// Register cleanup on process exit
|
|
252
|
+
process.on('beforeExit', () => {
|
|
253
|
+
void destroyWebSearchSession();
|
|
254
|
+
});
|