patchwork-os 0.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.bridge.md +352 -0
- package/README.md +72 -0
- package/deploy/README.md +172 -0
- package/deploy/bootstrap-new-vps.sh +364 -0
- package/deploy/claude-ide-bridge.service.template +67 -0
- package/deploy/claude-ide-bridge@.service +31 -0
- package/deploy/ecosystem.config.js.example +36 -0
- package/deploy/install-vps-service.sh +240 -0
- package/deploy/nginx-claude-bridge.conf.template +129 -0
- package/dist/activityLog.d.ts +112 -0
- package/dist/activityLog.js +399 -0
- package/dist/activityLog.js.map +1 -0
- package/dist/activityTypes.d.ts +28 -0
- package/dist/activityTypes.js +9 -0
- package/dist/activityTypes.js.map +1 -0
- package/dist/adapters/base.d.ts +78 -0
- package/dist/adapters/base.js +14 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude.d.ts +18 -0
- package/dist/adapters/claude.js +276 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/gemini.d.ts +17 -0
- package/dist/adapters/gemini.js +218 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/grok.d.ts +7 -0
- package/dist/adapters/grok.js +21 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +37 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/local.d.ts +7 -0
- package/dist/adapters/local.js +22 -0
- package/dist/adapters/local.js.map +1 -0
- package/dist/adapters/openai.d.ts +22 -0
- package/dist/adapters/openai.js +284 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/adapters/sse.d.ts +13 -0
- package/dist/adapters/sse.js +58 -0
- package/dist/adapters/sse.js.map +1 -0
- package/dist/analyticsAggregator.d.ts +28 -0
- package/dist/analyticsAggregator.js +133 -0
- package/dist/analyticsAggregator.js.map +1 -0
- package/dist/analyticsPrefs.d.ts +9 -0
- package/dist/analyticsPrefs.js +50 -0
- package/dist/analyticsPrefs.js.map +1 -0
- package/dist/analyticsSend.d.ts +12 -0
- package/dist/analyticsSend.js +34 -0
- package/dist/analyticsSend.js.map +1 -0
- package/dist/approvalHttp.d.ts +46 -0
- package/dist/approvalHttp.js +370 -0
- package/dist/approvalHttp.js.map +1 -0
- package/dist/approvalQueue.d.ts +49 -0
- package/dist/approvalQueue.js +84 -0
- package/dist/approvalQueue.js.map +1 -0
- package/dist/automation.d.ts +675 -0
- package/dist/automation.js +1038 -0
- package/dist/automation.js.map +1 -0
- package/dist/bridge.d.ts +85 -0
- package/dist/bridge.js +1535 -0
- package/dist/bridge.js.map +1 -0
- package/dist/bridgeLockDiscovery.d.ts +11 -0
- package/dist/bridgeLockDiscovery.js +49 -0
- package/dist/bridgeLockDiscovery.js.map +1 -0
- package/dist/bridgeToken.d.ts +22 -0
- package/dist/bridgeToken.js +114 -0
- package/dist/bridgeToken.js.map +1 -0
- package/dist/bridgeToolsRules.d.ts +20 -0
- package/dist/bridgeToolsRules.js +79 -0
- package/dist/bridgeToolsRules.js.map +1 -0
- package/dist/ccPermissions.d.ts +59 -0
- package/dist/ccPermissions.js +163 -0
- package/dist/ccPermissions.js.map +1 -0
- package/dist/claudeDriver.d.ts +129 -0
- package/dist/claudeDriver.js +459 -0
- package/dist/claudeDriver.js.map +1 -0
- package/dist/claudeMdPatch.d.ts +29 -0
- package/dist/claudeMdPatch.js +164 -0
- package/dist/claudeMdPatch.js.map +1 -0
- package/dist/claudeOrchestrator.d.ts +171 -0
- package/dist/claudeOrchestrator.js +591 -0
- package/dist/claudeOrchestrator.js.map +1 -0
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +158 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/marketplace.d.ts +11 -0
- package/dist/commands/marketplace.js +120 -0
- package/dist/commands/marketplace.js.map +1 -0
- package/dist/commands/patchworkInit.d.ts +14 -0
- package/dist/commands/patchworkInit.js +155 -0
- package/dist/commands/patchworkInit.js.map +1 -0
- package/dist/commands/task.d.ts +14 -0
- package/dist/commands/task.js +289 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/tokenEfficiency.d.ts +9 -0
- package/dist/commands/tokenEfficiency.js +211 -0
- package/dist/commands/tokenEfficiency.js.map +1 -0
- package/dist/commands/tools.d.ts +28 -0
- package/dist/commands/tools.js +326 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commitIssueLinkLog.d.ts +77 -0
- package/dist/commitIssueLinkLog.js +142 -0
- package/dist/commitIssueLinkLog.js.map +1 -0
- package/dist/companions/registry.d.ts +12 -0
- package/dist/companions/registry.js +71 -0
- package/dist/companions/registry.js.map +1 -0
- package/dist/config.d.ts +105 -0
- package/dist/config.js +720 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto.d.ts +16 -0
- package/dist/crypto.js +34 -0
- package/dist/crypto.js.map +1 -0
- package/dist/dashboard.d.ts +12 -0
- package/dist/dashboard.js +149 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/decisionTraceLog.d.ts +77 -0
- package/dist/decisionTraceLog.js +147 -0
- package/dist/decisionTraceLog.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensionClient.d.ts +279 -0
- package/dist/extensionClient.js +1253 -0
- package/dist/extensionClient.js.map +1 -0
- package/dist/fileLock.d.ts +36 -0
- package/dist/fileLock.js +121 -0
- package/dist/fileLock.js.map +1 -0
- package/dist/fp/activityAnalytics.d.ts +39 -0
- package/dist/fp/activityAnalytics.js +111 -0
- package/dist/fp/activityAnalytics.js.map +1 -0
- package/dist/fp/async.d.ts +48 -0
- package/dist/fp/async.js +60 -0
- package/dist/fp/async.js.map +1 -0
- package/dist/fp/automationInterpreter.d.ts +37 -0
- package/dist/fp/automationInterpreter.js +523 -0
- package/dist/fp/automationInterpreter.js.map +1 -0
- package/dist/fp/automationProgram.d.ts +89 -0
- package/dist/fp/automationProgram.js +29 -0
- package/dist/fp/automationProgram.js.map +1 -0
- package/dist/fp/automationState.d.ts +135 -0
- package/dist/fp/automationState.js +206 -0
- package/dist/fp/automationState.js.map +1 -0
- package/dist/fp/automationUtils.d.ts +31 -0
- package/dist/fp/automationUtils.js +61 -0
- package/dist/fp/automationUtils.js.map +1 -0
- package/dist/fp/brandedTypes.d.ts +32 -0
- package/dist/fp/brandedTypes.js +41 -0
- package/dist/fp/brandedTypes.js.map +1 -0
- package/dist/fp/commandDescription.d.ts +18 -0
- package/dist/fp/commandDescription.js +125 -0
- package/dist/fp/commandDescription.js.map +1 -0
- package/dist/fp/extensionSnapshot.d.ts +10 -0
- package/dist/fp/extensionSnapshot.js +14 -0
- package/dist/fp/extensionSnapshot.js.map +1 -0
- package/dist/fp/index.d.ts +8 -0
- package/dist/fp/index.js +9 -0
- package/dist/fp/index.js.map +1 -0
- package/dist/fp/interpreterContext.d.ts +69 -0
- package/dist/fp/interpreterContext.js +56 -0
- package/dist/fp/interpreterContext.js.map +1 -0
- package/dist/fp/policyParser.d.ts +16 -0
- package/dist/fp/policyParser.js +334 -0
- package/dist/fp/policyParser.js.map +1 -0
- package/dist/fp/result.d.ts +38 -0
- package/dist/fp/result.js +57 -0
- package/dist/fp/result.js.map +1 -0
- package/dist/fp/tokenBucket.d.ts +27 -0
- package/dist/fp/tokenBucket.js +36 -0
- package/dist/fp/tokenBucket.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1465 -0
- package/dist/index.js.map +1 -0
- package/dist/instructionsUtils.d.ts +17 -0
- package/dist/instructionsUtils.js +38 -0
- package/dist/instructionsUtils.js.map +1 -0
- package/dist/lockfile.d.ts +16 -0
- package/dist/lockfile.js +172 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +68 -0
- package/dist/logger.js.map +1 -0
- package/dist/oauth.d.ts +105 -0
- package/dist/oauth.js +880 -0
- package/dist/oauth.js.map +1 -0
- package/dist/orchestrator/childBridgeClient.d.ts +33 -0
- package/dist/orchestrator/childBridgeClient.js +321 -0
- package/dist/orchestrator/childBridgeClient.js.map +1 -0
- package/dist/orchestrator/childBridgeRegistry.d.ts +67 -0
- package/dist/orchestrator/childBridgeRegistry.js +297 -0
- package/dist/orchestrator/childBridgeRegistry.js.map +1 -0
- package/dist/orchestrator/index.d.ts +3 -0
- package/dist/orchestrator/index.js +3 -0
- package/dist/orchestrator/index.js.map +1 -0
- package/dist/orchestrator/orchestratorBridge.d.ts +32 -0
- package/dist/orchestrator/orchestratorBridge.js +412 -0
- package/dist/orchestrator/orchestratorBridge.js.map +1 -0
- package/dist/orchestrator/orchestratorConfig.d.ts +11 -0
- package/dist/orchestrator/orchestratorConfig.js +85 -0
- package/dist/orchestrator/orchestratorConfig.js.map +1 -0
- package/dist/orchestrator/orchestratorTools.d.ts +16 -0
- package/dist/orchestrator/orchestratorTools.js +272 -0
- package/dist/orchestrator/orchestratorTools.js.map +1 -0
- package/dist/patchworkCli.d.ts +15 -0
- package/dist/patchworkCli.js +41 -0
- package/dist/patchworkCli.js.map +1 -0
- package/dist/patchworkConfig.d.ts +28 -0
- package/dist/patchworkConfig.js +30 -0
- package/dist/patchworkConfig.js.map +1 -0
- package/dist/plugin.d.ts +106 -0
- package/dist/plugin.js +31 -0
- package/dist/plugin.js.map +1 -0
- package/dist/pluginLoader.d.ts +44 -0
- package/dist/pluginLoader.js +357 -0
- package/dist/pluginLoader.js.map +1 -0
- package/dist/pluginWatcher.d.ts +24 -0
- package/dist/pluginWatcher.js +139 -0
- package/dist/pluginWatcher.js.map +1 -0
- package/dist/preToolUseHook.d.ts +10 -0
- package/dist/preToolUseHook.js +57 -0
- package/dist/preToolUseHook.js.map +1 -0
- package/dist/probe.d.ts +35 -0
- package/dist/probe.js +143 -0
- package/dist/probe.js.map +1 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.js +1680 -0
- package/dist/prompts.js.map +1 -0
- package/dist/quickTaskPresets.d.ts +64 -0
- package/dist/quickTaskPresets.js +156 -0
- package/dist/quickTaskPresets.js.map +1 -0
- package/dist/recipes/compiler.d.ts +44 -0
- package/dist/recipes/compiler.js +140 -0
- package/dist/recipes/compiler.js.map +1 -0
- package/dist/recipes/installer.d.ts +25 -0
- package/dist/recipes/installer.js +62 -0
- package/dist/recipes/installer.js.map +1 -0
- package/dist/recipes/parser.d.ts +18 -0
- package/dist/recipes/parser.js +160 -0
- package/dist/recipes/parser.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +45 -0
- package/dist/recipes/scheduler.js +110 -0
- package/dist/recipes/scheduler.js.map +1 -0
- package/dist/recipes/schema.d.ts +71 -0
- package/dist/recipes/schema.js +11 -0
- package/dist/recipes/schema.js.map +1 -0
- package/dist/recipesHttp.d.ts +63 -0
- package/dist/recipesHttp.js +183 -0
- package/dist/recipesHttp.js.map +1 -0
- package/dist/resources.d.ts +33 -0
- package/dist/resources.js +266 -0
- package/dist/resources.js.map +1 -0
- package/dist/riskTier.d.ts +40 -0
- package/dist/riskTier.js +142 -0
- package/dist/riskTier.js.map +1 -0
- package/dist/runLog.d.ts +90 -0
- package/dist/runLog.js +143 -0
- package/dist/runLog.js.map +1 -0
- package/dist/server.d.ts +160 -0
- package/dist/server.js +1244 -0
- package/dist/server.js.map +1 -0
- package/dist/sessionCheckpoint.d.ts +37 -0
- package/dist/sessionCheckpoint.js +123 -0
- package/dist/sessionCheckpoint.js.map +1 -0
- package/dist/streamableHttp.d.ts +86 -0
- package/dist/streamableHttp.js +702 -0
- package/dist/streamableHttp.js.map +1 -0
- package/dist/telemetry.d.ts +18 -0
- package/dist/telemetry.js +95 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tools/activityLog.d.ts +140 -0
- package/dist/tools/activityLog.js +204 -0
- package/dist/tools/activityLog.js.map +1 -0
- package/dist/tools/auditDependencies.d.ts +67 -0
- package/dist/tools/auditDependencies.js +298 -0
- package/dist/tools/auditDependencies.js.map +1 -0
- package/dist/tools/batchLsp.d.ts +262 -0
- package/dist/tools/batchLsp.js +328 -0
- package/dist/tools/batchLsp.js.map +1 -0
- package/dist/tools/blame-utils.d.ts +30 -0
- package/dist/tools/blame-utils.js +60 -0
- package/dist/tools/blame-utils.js.map +1 -0
- package/dist/tools/bridgeDoctor.d.ts +78 -0
- package/dist/tools/bridgeDoctor.js +542 -0
- package/dist/tools/bridgeDoctor.js.map +1 -0
- package/dist/tools/bridgeStatus.d.ts +122 -0
- package/dist/tools/bridgeStatus.js +250 -0
- package/dist/tools/bridgeStatus.js.map +1 -0
- package/dist/tools/cancelClaudeTask.d.ts +48 -0
- package/dist/tools/cancelClaudeTask.js +56 -0
- package/dist/tools/cancelClaudeTask.js.map +1 -0
- package/dist/tools/checkDocumentDirty.d.ts +56 -0
- package/dist/tools/checkDocumentDirty.js +74 -0
- package/dist/tools/checkDocumentDirty.js.map +1 -0
- package/dist/tools/clipboard.d.ts +80 -0
- package/dist/tools/clipboard.js +211 -0
- package/dist/tools/clipboard.js.map +1 -0
- package/dist/tools/closeTabs.d.ts +84 -0
- package/dist/tools/closeTabs.js +97 -0
- package/dist/tools/closeTabs.js.map +1 -0
- package/dist/tools/codeLens.d.ts +50 -0
- package/dist/tools/codeLens.js +47 -0
- package/dist/tools/codeLens.js.map +1 -0
- package/dist/tools/contextBundle.d.ts +75 -0
- package/dist/tools/contextBundle.js +218 -0
- package/dist/tools/contextBundle.js.map +1 -0
- package/dist/tools/createIssueFromAIComment.d.ts +75 -0
- package/dist/tools/createIssueFromAIComment.js +119 -0
- package/dist/tools/createIssueFromAIComment.js.map +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +103 -0
- package/dist/tools/ctxGetTaskContext.js +274 -0
- package/dist/tools/ctxGetTaskContext.js.map +1 -0
- package/dist/tools/ctxQueryTraces.d.ts +142 -0
- package/dist/tools/ctxQueryTraces.js +194 -0
- package/dist/tools/ctxQueryTraces.js.map +1 -0
- package/dist/tools/ctxSaveTrace.d.ts +87 -0
- package/dist/tools/ctxSaveTrace.js +94 -0
- package/dist/tools/ctxSaveTrace.js.map +1 -0
- package/dist/tools/debug.d.ts +206 -0
- package/dist/tools/debug.js +234 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/decorations.d.ts +130 -0
- package/dist/tools/decorations.js +160 -0
- package/dist/tools/decorations.js.map +1 -0
- package/dist/tools/detectUnusedCode.d.ts +78 -0
- package/dist/tools/detectUnusedCode.js +173 -0
- package/dist/tools/detectUnusedCode.js.map +1 -0
- package/dist/tools/documentLinks.d.ts +62 -0
- package/dist/tools/documentLinks.js +55 -0
- package/dist/tools/documentLinks.js.map +1 -0
- package/dist/tools/editText.d.ts +108 -0
- package/dist/tools/editText.js +318 -0
- package/dist/tools/editText.js.map +1 -0
- package/dist/tools/enrichCommit.d.ts +89 -0
- package/dist/tools/enrichCommit.js +201 -0
- package/dist/tools/enrichCommit.js.map +1 -0
- package/dist/tools/enrichStackTrace.d.ts +121 -0
- package/dist/tools/enrichStackTrace.js +194 -0
- package/dist/tools/enrichStackTrace.js.map +1 -0
- package/dist/tools/explainDiagnostic.d.ts +137 -0
- package/dist/tools/explainDiagnostic.js +230 -0
- package/dist/tools/explainDiagnostic.js.map +1 -0
- package/dist/tools/explainSymbol.d.ts +119 -0
- package/dist/tools/explainSymbol.js +177 -0
- package/dist/tools/explainSymbol.js.map +1 -0
- package/dist/tools/fileOperations.d.ts +186 -0
- package/dist/tools/fileOperations.js +330 -0
- package/dist/tools/fileOperations.js.map +1 -0
- package/dist/tools/fileWatcher.d.ts +107 -0
- package/dist/tools/fileWatcher.js +121 -0
- package/dist/tools/fileWatcher.js.map +1 -0
- package/dist/tools/findFiles.d.ts +65 -0
- package/dist/tools/findFiles.js +142 -0
- package/dist/tools/findFiles.js.map +1 -0
- package/dist/tools/findRelatedTests.d.ts +83 -0
- package/dist/tools/findRelatedTests.js +196 -0
- package/dist/tools/findRelatedTests.js.map +1 -0
- package/dist/tools/fixAllLintErrors.d.ts +66 -0
- package/dist/tools/fixAllLintErrors.js +128 -0
- package/dist/tools/fixAllLintErrors.js.map +1 -0
- package/dist/tools/foldingRanges.d.ts +50 -0
- package/dist/tools/foldingRanges.js +51 -0
- package/dist/tools/foldingRanges.js.map +1 -0
- package/dist/tools/formatAndSave.d.ts +57 -0
- package/dist/tools/formatAndSave.js +87 -0
- package/dist/tools/formatAndSave.js.map +1 -0
- package/dist/tools/formatDocument.d.ts +61 -0
- package/dist/tools/formatDocument.js +144 -0
- package/dist/tools/formatDocument.js.map +1 -0
- package/dist/tools/generateAPIDocumentation.d.ts +62 -0
- package/dist/tools/generateAPIDocumentation.js +249 -0
- package/dist/tools/generateAPIDocumentation.js.map +1 -0
- package/dist/tools/generateTests.d.ts +75 -0
- package/dist/tools/generateTests.js +226 -0
- package/dist/tools/generateTests.js.map +1 -0
- package/dist/tools/getAIComments.d.ts +79 -0
- package/dist/tools/getAIComments.js +93 -0
- package/dist/tools/getAIComments.js.map +1 -0
- package/dist/tools/getAnalyticsReport.d.ts +102 -0
- package/dist/tools/getAnalyticsReport.js +137 -0
- package/dist/tools/getAnalyticsReport.js.map +1 -0
- package/dist/tools/getArchitectureContext.d.ts +85 -0
- package/dist/tools/getArchitectureContext.js +135 -0
- package/dist/tools/getArchitectureContext.js.map +1 -0
- package/dist/tools/getBufferContent.d.ts +80 -0
- package/dist/tools/getBufferContent.js +207 -0
- package/dist/tools/getBufferContent.js.map +1 -0
- package/dist/tools/getChangeImpact.d.ts +76 -0
- package/dist/tools/getChangeImpact.js +184 -0
- package/dist/tools/getChangeImpact.js.map +1 -0
- package/dist/tools/getClaudeTaskStatus.d.ts +87 -0
- package/dist/tools/getClaudeTaskStatus.js +89 -0
- package/dist/tools/getClaudeTaskStatus.js.map +1 -0
- package/dist/tools/getCodeCoverage.d.ts +86 -0
- package/dist/tools/getCodeCoverage.js +237 -0
- package/dist/tools/getCodeCoverage.js.map +1 -0
- package/dist/tools/getCommitsForIssue.d.ts +98 -0
- package/dist/tools/getCommitsForIssue.js +106 -0
- package/dist/tools/getCommitsForIssue.js.map +1 -0
- package/dist/tools/getCurrentSelection.d.ts +123 -0
- package/dist/tools/getCurrentSelection.js +113 -0
- package/dist/tools/getCurrentSelection.js.map +1 -0
- package/dist/tools/getDebugState.d.ts +140 -0
- package/dist/tools/getDebugState.js +109 -0
- package/dist/tools/getDebugState.js.map +1 -0
- package/dist/tools/getDependencyTree.d.ts +59 -0
- package/dist/tools/getDependencyTree.js +207 -0
- package/dist/tools/getDependencyTree.js.map +1 -0
- package/dist/tools/getDiagnostics.d.ts +108 -0
- package/dist/tools/getDiagnostics.js +371 -0
- package/dist/tools/getDiagnostics.js.map +1 -0
- package/dist/tools/getDiffFromHandoff.d.ts +89 -0
- package/dist/tools/getDiffFromHandoff.js +163 -0
- package/dist/tools/getDiffFromHandoff.js.map +1 -0
- package/dist/tools/getDocumentSymbols.d.ts +74 -0
- package/dist/tools/getDocumentSymbols.js +177 -0
- package/dist/tools/getDocumentSymbols.js.map +1 -0
- package/dist/tools/getFileTree.d.ts +66 -0
- package/dist/tools/getFileTree.js +131 -0
- package/dist/tools/getFileTree.js.map +1 -0
- package/dist/tools/getGitDiff.d.ts +50 -0
- package/dist/tools/getGitDiff.js +73 -0
- package/dist/tools/getGitDiff.js.map +1 -0
- package/dist/tools/getGitHotspots.d.ts +88 -0
- package/dist/tools/getGitHotspots.js +145 -0
- package/dist/tools/getGitHotspots.js.map +1 -0
- package/dist/tools/getGitLog.d.ts +62 -0
- package/dist/tools/getGitLog.js +87 -0
- package/dist/tools/getGitLog.js.map +1 -0
- package/dist/tools/getGitStatus.d.ts +72 -0
- package/dist/tools/getGitStatus.js +126 -0
- package/dist/tools/getGitStatus.js.map +1 -0
- package/dist/tools/getImportTree.d.ts +73 -0
- package/dist/tools/getImportTree.js +223 -0
- package/dist/tools/getImportTree.js.map +1 -0
- package/dist/tools/getImportedSignatures.d.ts +62 -0
- package/dist/tools/getImportedSignatures.js +255 -0
- package/dist/tools/getImportedSignatures.js.map +1 -0
- package/dist/tools/getOpenEditors.d.ts +62 -0
- package/dist/tools/getOpenEditors.js +126 -0
- package/dist/tools/getOpenEditors.js.map +1 -0
- package/dist/tools/getPRTemplate.d.ts +68 -0
- package/dist/tools/getPRTemplate.js +187 -0
- package/dist/tools/getPRTemplate.js.map +1 -0
- package/dist/tools/getProjectContext.d.ts +114 -0
- package/dist/tools/getProjectContext.js +344 -0
- package/dist/tools/getProjectContext.js.map +1 -0
- package/dist/tools/getProjectInfo.d.ts +51 -0
- package/dist/tools/getProjectInfo.js +325 -0
- package/dist/tools/getProjectInfo.js.map +1 -0
- package/dist/tools/getSecurityAdvisories.d.ts +105 -0
- package/dist/tools/getSecurityAdvisories.js +472 -0
- package/dist/tools/getSecurityAdvisories.js.map +1 -0
- package/dist/tools/getSessionUsage.d.ts +58 -0
- package/dist/tools/getSessionUsage.js +57 -0
- package/dist/tools/getSessionUsage.js.map +1 -0
- package/dist/tools/getSymbolHistory.d.ts +157 -0
- package/dist/tools/getSymbolHistory.js +256 -0
- package/dist/tools/getSymbolHistory.js.map +1 -0
- package/dist/tools/getToolCapabilities.d.ts +69 -0
- package/dist/tools/getToolCapabilities.js +298 -0
- package/dist/tools/getToolCapabilities.js.map +1 -0
- package/dist/tools/getTypeSignature.d.ts +70 -0
- package/dist/tools/getTypeSignature.js +132 -0
- package/dist/tools/getTypeSignature.js.map +1 -0
- package/dist/tools/getWorkspaceFolders.d.ts +58 -0
- package/dist/tools/getWorkspaceFolders.js +69 -0
- package/dist/tools/getWorkspaceFolders.js.map +1 -0
- package/dist/tools/getWorkspaceSettings.d.ts +44 -0
- package/dist/tools/getWorkspaceSettings.js +70 -0
- package/dist/tools/getWorkspaceSettings.js.map +1 -0
- package/dist/tools/git-utils.d.ts +16 -0
- package/dist/tools/git-utils.js +46 -0
- package/dist/tools/git-utils.js.map +1 -0
- package/dist/tools/gitHistory.d.ts +110 -0
- package/dist/tools/gitHistory.js +167 -0
- package/dist/tools/gitHistory.js.map +1 -0
- package/dist/tools/gitWrite.d.ts +612 -0
- package/dist/tools/gitWrite.js +983 -0
- package/dist/tools/gitWrite.js.map +1 -0
- package/dist/tools/github/actions.d.ts +152 -0
- package/dist/tools/github/actions.js +195 -0
- package/dist/tools/github/actions.js.map +1 -0
- package/dist/tools/github/index.d.ts +3 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/index.js.map +1 -0
- package/dist/tools/github/issues.d.ts +281 -0
- package/dist/tools/github/issues.js +340 -0
- package/dist/tools/github/issues.js.map +1 -0
- package/dist/tools/github/pr.d.ts +433 -0
- package/dist/tools/github/pr.js +588 -0
- package/dist/tools/github/pr.js.map +1 -0
- package/dist/tools/github/shared.d.ts +4 -0
- package/dist/tools/github/shared.js +12 -0
- package/dist/tools/github/shared.js.map +1 -0
- package/dist/tools/handoffNote.d.ts +106 -0
- package/dist/tools/handoffNote.js +232 -0
- package/dist/tools/handoffNote.js.map +1 -0
- package/dist/tools/headless/lspClient.d.ts +26 -0
- package/dist/tools/headless/lspClient.js +221 -0
- package/dist/tools/headless/lspClient.js.map +1 -0
- package/dist/tools/headless/lspFallback.d.ts +28 -0
- package/dist/tools/headless/lspFallback.js +122 -0
- package/dist/tools/headless/lspFallback.js.map +1 -0
- package/dist/tools/hoverAtCursor.d.ts +54 -0
- package/dist/tools/hoverAtCursor.js +68 -0
- package/dist/tools/hoverAtCursor.js.map +1 -0
- package/dist/tools/httpClient.d.ts +141 -0
- package/dist/tools/httpClient.js +486 -0
- package/dist/tools/httpClient.js.map +1 -0
- package/dist/tools/index.d.ts +49 -0
- package/dist/tools/index.js +672 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/inlayHints.d.ts +81 -0
- package/dist/tools/inlayHints.js +76 -0
- package/dist/tools/inlayHints.js.map +1 -0
- package/dist/tools/issueRefs.d.ts +14 -0
- package/dist/tools/issueRefs.js +27 -0
- package/dist/tools/issueRefs.js.map +1 -0
- package/dist/tools/jumpToFirstError.d.ts +63 -0
- package/dist/tools/jumpToFirstError.js +124 -0
- package/dist/tools/jumpToFirstError.js.map +1 -0
- package/dist/tools/launchQuickTask.d.ts +76 -0
- package/dist/tools/launchQuickTask.js +170 -0
- package/dist/tools/launchQuickTask.js.map +1 -0
- package/dist/tools/linters/biome.d.ts +2 -0
- package/dist/tools/linters/biome.js +44 -0
- package/dist/tools/linters/biome.js.map +1 -0
- package/dist/tools/linters/cargo.d.ts +2 -0
- package/dist/tools/linters/cargo.js +45 -0
- package/dist/tools/linters/cargo.js.map +1 -0
- package/dist/tools/linters/eslint.d.ts +2 -0
- package/dist/tools/linters/eslint.js +59 -0
- package/dist/tools/linters/eslint.js.map +1 -0
- package/dist/tools/linters/govet.d.ts +2 -0
- package/dist/tools/linters/govet.js +37 -0
- package/dist/tools/linters/govet.js.map +1 -0
- package/dist/tools/linters/pyright.d.ts +2 -0
- package/dist/tools/linters/pyright.js +34 -0
- package/dist/tools/linters/pyright.js.map +1 -0
- package/dist/tools/linters/ruff.d.ts +2 -0
- package/dist/tools/linters/ruff.js +30 -0
- package/dist/tools/linters/ruff.js.map +1 -0
- package/dist/tools/linters/types.d.ts +16 -0
- package/dist/tools/linters/types.js +2 -0
- package/dist/tools/linters/types.js.map +1 -0
- package/dist/tools/linters/typescript.d.ts +2 -0
- package/dist/tools/linters/typescript.js +38 -0
- package/dist/tools/linters/typescript.js.map +1 -0
- package/dist/tools/listClaudeTasks.d.ts +84 -0
- package/dist/tools/listClaudeTasks.js +88 -0
- package/dist/tools/listClaudeTasks.js.map +1 -0
- package/dist/tools/listTerminals.d.ts +55 -0
- package/dist/tools/listTerminals.js +78 -0
- package/dist/tools/listTerminals.js.map +1 -0
- package/dist/tools/lsp.d.ts +1086 -0
- package/dist/tools/lsp.js +1339 -0
- package/dist/tools/lsp.js.map +1 -0
- package/dist/tools/navigateToSymbolByName.d.ts +56 -0
- package/dist/tools/navigateToSymbolByName.js +170 -0
- package/dist/tools/navigateToSymbolByName.js.map +1 -0
- package/dist/tools/openDiff.d.ts +66 -0
- package/dist/tools/openDiff.js +126 -0
- package/dist/tools/openDiff.js.map +1 -0
- package/dist/tools/openFile.d.ts +69 -0
- package/dist/tools/openFile.js +129 -0
- package/dist/tools/openFile.js.map +1 -0
- package/dist/tools/openInBrowser.d.ts +55 -0
- package/dist/tools/openInBrowser.js +129 -0
- package/dist/tools/openInBrowser.js.map +1 -0
- package/dist/tools/organizeImports.d.ts +56 -0
- package/dist/tools/organizeImports.js +115 -0
- package/dist/tools/organizeImports.js.map +1 -0
- package/dist/tools/performanceReport.d.ts +133 -0
- package/dist/tools/performanceReport.js +218 -0
- package/dist/tools/performanceReport.js.map +1 -0
- package/dist/tools/planPersistence.d.ts +306 -0
- package/dist/tools/planPersistence.js +485 -0
- package/dist/tools/planPersistence.js.map +1 -0
- package/dist/tools/previewEdit.d.ts +107 -0
- package/dist/tools/previewEdit.js +270 -0
- package/dist/tools/previewEdit.js.map +1 -0
- package/dist/tools/recentTracesDigest.d.ts +35 -0
- package/dist/tools/recentTracesDigest.js +98 -0
- package/dist/tools/recentTracesDigest.js.map +1 -0
- package/dist/tools/refactorAnalyze.d.ts +78 -0
- package/dist/tools/refactorAnalyze.js +141 -0
- package/dist/tools/refactorAnalyze.js.map +1 -0
- package/dist/tools/refactorExtractFunction.d.ts +52 -0
- package/dist/tools/refactorExtractFunction.js +121 -0
- package/dist/tools/refactorExtractFunction.js.map +1 -0
- package/dist/tools/refactorPreview.d.ts +75 -0
- package/dist/tools/refactorPreview.js +93 -0
- package/dist/tools/refactorPreview.js.map +1 -0
- package/dist/tools/replaceBlock.d.ts +62 -0
- package/dist/tools/replaceBlock.js +125 -0
- package/dist/tools/replaceBlock.js.map +1 -0
- package/dist/tools/resumeClaudeTask.d.ts +75 -0
- package/dist/tools/resumeClaudeTask.js +149 -0
- package/dist/tools/resumeClaudeTask.js.map +1 -0
- package/dist/tools/runClaudeTask.d.ts +97 -0
- package/dist/tools/runClaudeTask.js +224 -0
- package/dist/tools/runClaudeTask.js.map +1 -0
- package/dist/tools/runCommand.d.ts +82 -0
- package/dist/tools/runCommand.js +101 -0
- package/dist/tools/runCommand.js.map +1 -0
- package/dist/tools/runTests.d.ts +146 -0
- package/dist/tools/runTests.js +315 -0
- package/dist/tools/runTests.js.map +1 -0
- package/dist/tools/saveDocument.d.ts +50 -0
- package/dist/tools/saveDocument.js +73 -0
- package/dist/tools/saveDocument.js.map +1 -0
- package/dist/tools/screenshot.d.ts +23 -0
- package/dist/tools/screenshot.js +43 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/screenshotAndAnnotate.d.ts +103 -0
- package/dist/tools/screenshotAndAnnotate.js +192 -0
- package/dist/tools/screenshotAndAnnotate.js.map +1 -0
- package/dist/tools/searchAndReplace.d.ts +108 -0
- package/dist/tools/searchAndReplace.js +281 -0
- package/dist/tools/searchAndReplace.js.map +1 -0
- package/dist/tools/searchTools.d.ts +61 -0
- package/dist/tools/searchTools.js +85 -0
- package/dist/tools/searchTools.js.map +1 -0
- package/dist/tools/searchWorkspace.d.ts +99 -0
- package/dist/tools/searchWorkspace.js +189 -0
- package/dist/tools/searchWorkspace.js.map +1 -0
- package/dist/tools/selectionRanges.d.ts +58 -0
- package/dist/tools/selectionRanges.js +61 -0
- package/dist/tools/selectionRanges.js.map +1 -0
- package/dist/tools/semanticTokens.d.ts +87 -0
- package/dist/tools/semanticTokens.js +86 -0
- package/dist/tools/semanticTokens.js.map +1 -0
- package/dist/tools/setActiveWorkspaceFolder.d.ts +41 -0
- package/dist/tools/setActiveWorkspaceFolder.js +38 -0
- package/dist/tools/setActiveWorkspaceFolder.js.map +1 -0
- package/dist/tools/signatureHelp.d.ts +86 -0
- package/dist/tools/signatureHelp.js +79 -0
- package/dist/tools/signatureHelp.js.map +1 -0
- package/dist/tools/spawnWorkspace.d.ts +103 -0
- package/dist/tools/spawnWorkspace.js +268 -0
- package/dist/tools/spawnWorkspace.js.map +1 -0
- package/dist/tools/stackTraceParser.d.ts +43 -0
- package/dist/tools/stackTraceParser.js +139 -0
- package/dist/tools/stackTraceParser.js.map +1 -0
- package/dist/tools/terminal.d.ts +352 -0
- package/dist/tools/terminal.js +670 -0
- package/dist/tools/terminal.js.map +1 -0
- package/dist/tools/testRunners/cargoTest.d.ts +2 -0
- package/dist/tools/testRunners/cargoTest.js +129 -0
- package/dist/tools/testRunners/cargoTest.js.map +1 -0
- package/dist/tools/testRunners/goTest.d.ts +2 -0
- package/dist/tools/testRunners/goTest.js +108 -0
- package/dist/tools/testRunners/goTest.js.map +1 -0
- package/dist/tools/testRunners/pytest.d.ts +2 -0
- package/dist/tools/testRunners/pytest.js +135 -0
- package/dist/tools/testRunners/pytest.js.map +1 -0
- package/dist/tools/testRunners/types.d.ts +18 -0
- package/dist/tools/testRunners/types.js +2 -0
- package/dist/tools/testRunners/types.js.map +1 -0
- package/dist/tools/testRunners/vitestJest.d.ts +3 -0
- package/dist/tools/testRunners/vitestJest.js +215 -0
- package/dist/tools/testRunners/vitestJest.js.map +1 -0
- package/dist/tools/testTraceToSource.d.ts +80 -0
- package/dist/tools/testTraceToSource.js +206 -0
- package/dist/tools/testTraceToSource.js.map +1 -0
- package/dist/tools/transaction.d.ts +243 -0
- package/dist/tools/transaction.js +309 -0
- package/dist/tools/transaction.js.map +1 -0
- package/dist/tools/typeHierarchy.d.ts +77 -0
- package/dist/tools/typeHierarchy.js +86 -0
- package/dist/tools/typeHierarchy.js.map +1 -0
- package/dist/tools/utils.d.ts +124 -0
- package/dist/tools/utils.js +566 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools/vscodeCommands.d.ts +90 -0
- package/dist/tools/vscodeCommands.js +112 -0
- package/dist/tools/vscodeCommands.js.map +1 -0
- package/dist/tools/vscodeTasks.d.ts +102 -0
- package/dist/tools/vscodeTasks.js +110 -0
- package/dist/tools/vscodeTasks.js.map +1 -0
- package/dist/tools/watchDiagnostics.d.ts +64 -0
- package/dist/tools/watchDiagnostics.js +270 -0
- package/dist/tools/watchDiagnostics.js.map +1 -0
- package/dist/tools/workspaceSettings.d.ts +57 -0
- package/dist/tools/workspaceSettings.js +80 -0
- package/dist/tools/workspaceSettings.js.map +1 -0
- package/dist/transport.d.ts +207 -0
- package/dist/transport.js +1272 -0
- package/dist/transport.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +31 -0
- package/dist/version.js.map +1 -0
- package/dist/wsUtils.d.ts +8 -0
- package/dist/wsUtils.js +54 -0
- package/dist/wsUtils.js.map +1 -0
- package/package.json +118 -0
- package/scripts/gen-claude-desktop-config.sh +124 -0
- package/scripts/gen-mcp-config.sh +390 -0
- package/scripts/install-extension.sh +106 -0
- package/scripts/mcp-stdio-shim.cjs +482 -0
- package/scripts/postinstall.mjs +68 -0
- package/scripts/start-all.sh +502 -0
- package/scripts/start-orchestrator.sh +186 -0
- package/scripts/start-remote.sh +126 -0
- package/scripts/start-vps.sh +116 -0
- package/templates/CLAUDE.bridge.md +125 -0
- package/templates/automation-policies/security-first.json +46 -0
- package/templates/automation-policies/strict-lint.json +41 -0
- package/templates/automation-policies/test-driven.json +54 -0
- package/templates/automation-policy.example.json +105 -0
- package/templates/bridge-tools.md +111 -0
- package/templates/dispatch-context.md +33 -0
- package/templates/managed-agent/code-review-agent.md +50 -0
- package/templates/managed-agent/managed-agent-mcp.json +102 -0
- package/templates/recipes/ambient-journal.yaml +11 -0
- package/templates/recipes/daily-status.yaml +21 -0
- package/templates/recipes/lint-on-save.yaml +13 -0
- package/templates/recipes/stale-branches.yaml +18 -0
- package/templates/recipes/watch-failing-tests.yaml +15 -0
- package/templates/scheduled-tasks/dependency-audit/SKILL.md +77 -0
- package/templates/scheduled-tasks/health-check/SKILL.md +73 -0
- package/templates/scheduled-tasks/nightly-review/SKILL.md +69 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,1244 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { WebSocket, WebSocketServer as WsServer } from "ws";
|
|
4
|
+
import { routeApprovalRequest } from "./approvalHttp.js";
|
|
5
|
+
import { getApprovalQueue } from "./approvalQueue.js";
|
|
6
|
+
import { timingSafeStringEqual } from "./crypto.js";
|
|
7
|
+
import { renderDashboardHtml } from "./dashboard.js";
|
|
8
|
+
import { loadConfig as loadPatchworkConfig, defaultConfigPath as patchworkConfigPath, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
|
|
9
|
+
import { BRIDGE_PROTOCOL_VERSION, PACKAGE_LICENSE, PACKAGE_VERSION, } from "./version.js";
|
|
10
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
11
|
+
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
function enableTcpKeepalive(ws) {
|
|
13
|
+
const rawSocket = ws
|
|
14
|
+
._socket;
|
|
15
|
+
if (rawSocket?.setKeepAlive) {
|
|
16
|
+
rawSocket.setKeepAlive(true, 60_000); // 60s TCP keepalive as defense-in-depth
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Return the CORS origin to reflect, or null if the origin is untrusted.
|
|
21
|
+
* Loopback origins are always allowed. Additional origins can be passed via
|
|
22
|
+
* --cors-origin (e.g. https://claude.ai for remote deployments).
|
|
23
|
+
*/
|
|
24
|
+
export function corsOrigin(requestOrigin, extraOrigins = []) {
|
|
25
|
+
if (!requestOrigin)
|
|
26
|
+
return null;
|
|
27
|
+
if (extraOrigins.includes(requestOrigin))
|
|
28
|
+
return requestOrigin;
|
|
29
|
+
try {
|
|
30
|
+
const { hostname, protocol } = new URL(requestOrigin);
|
|
31
|
+
if (protocol === "http:" &&
|
|
32
|
+
(hostname === "localhost" ||
|
|
33
|
+
hostname === "127.0.0.1" ||
|
|
34
|
+
hostname === "[::1]")) {
|
|
35
|
+
return requestOrigin;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// malformed origin — deny
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Re-export canonical constant-time comparison for use in this module.
|
|
44
|
+
// Implementation lives in src/crypto.ts — see there for security notes.
|
|
45
|
+
const timingSafeTokenCompare = timingSafeStringEqual;
|
|
46
|
+
function setupPongHandler(ws) {
|
|
47
|
+
ws.on("pong", (_data) => {
|
|
48
|
+
ws.isAlive = true;
|
|
49
|
+
ws.missedPongs = 0;
|
|
50
|
+
// Always record the current time — do not trust the client-echoed payload
|
|
51
|
+
// timestamp, which can be spoofed to make lastPongTime appear stale.
|
|
52
|
+
ws.lastPongTime = Date.now();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// 500ms minimum between connections per client type.
|
|
56
|
+
// Tradeoff: multi-agent scenarios where two agents connect simultaneously may
|
|
57
|
+
// hit this limit; they will retry and connect successfully on the next attempt.
|
|
58
|
+
// Raised from 50ms to reduce connection-storm DoS surface in public deployments.
|
|
59
|
+
const MIN_CONNECTION_INTERVAL_MS = 500;
|
|
60
|
+
export class Server extends EventEmitter {
|
|
61
|
+
authToken;
|
|
62
|
+
logger;
|
|
63
|
+
extraCorsOrigins;
|
|
64
|
+
pingIntervalMs;
|
|
65
|
+
httpServer;
|
|
66
|
+
wss;
|
|
67
|
+
pingInterval = null;
|
|
68
|
+
lastClaudeConnectionTime = 0;
|
|
69
|
+
lastExtensionConnectionTime = 0;
|
|
70
|
+
startTime = Date.now();
|
|
71
|
+
/** OAuth 2.0 Authorization Server — set via setOAuthServer() when running in remote mode */
|
|
72
|
+
oauthServer = null;
|
|
73
|
+
oauthIssuerUrl = null;
|
|
74
|
+
sseSubscriberCount = 0;
|
|
75
|
+
static MAX_SSE_SUBSCRIBERS = 20;
|
|
76
|
+
/** Set by bridge to provide health data */
|
|
77
|
+
healthDataFn = null;
|
|
78
|
+
/** Set by bridge to provide Prometheus metrics */
|
|
79
|
+
metricsFn = null;
|
|
80
|
+
/** Set by bridge to provide rich status data */
|
|
81
|
+
statusFn = null;
|
|
82
|
+
/** Set by bridge to provide performance report data for /dashboard/data */
|
|
83
|
+
perfDataFn = null;
|
|
84
|
+
/** Set by bridge to provide readiness data (MCP handshake complete, tool count, extension) */
|
|
85
|
+
readyFn = null;
|
|
86
|
+
/** Set by bridge to provide task list data (sanitized — no raw prompts) */
|
|
87
|
+
tasksFn = null;
|
|
88
|
+
/** Patchwork: set by bridge to list installed recipes for the dashboard. */
|
|
89
|
+
recipesFn = null;
|
|
90
|
+
/** Patchwork: set by bridge to save a new recipe draft to disk. */
|
|
91
|
+
saveRecipeFn = null;
|
|
92
|
+
/** Patchwork: set by bridge to query the recipe run audit log. */
|
|
93
|
+
runsFn = null;
|
|
94
|
+
/** Patchwork: set by bridge to launch a named recipe via the orchestrator. */
|
|
95
|
+
runRecipeFn = null;
|
|
96
|
+
/** Patchwork: admin-controlled managed settings path (highest rule precedence). */
|
|
97
|
+
managedSettingsPath = undefined;
|
|
98
|
+
/** Patchwork: live approval gate level — mutated by POST /settings, read by bridge per-session setup. */
|
|
99
|
+
approvalGate = "off";
|
|
100
|
+
/** Patchwork: outbound webhook URL for approval notifications (from dashboard.webhookUrl in config). */
|
|
101
|
+
approvalWebhookUrl = undefined;
|
|
102
|
+
/** Patchwork: approval decision audit callback wired to activityLog.recordEvent. */
|
|
103
|
+
onApprovalDecision = undefined;
|
|
104
|
+
/** Patchwork: set by bridge to match + fire webhook-triggered recipes. */
|
|
105
|
+
webhookFn = null;
|
|
106
|
+
/** Set by bridge to handle MCP Streamable HTTP sessions (POST/GET/DELETE /mcp) */
|
|
107
|
+
httpMcpHandler = null;
|
|
108
|
+
/** Set by bridge to subscribe a caller to real-time activity events. Returns unsubscribe fn. */
|
|
109
|
+
streamFn = null;
|
|
110
|
+
/** Set to true via --no-dashboard to disable the /dashboard route. */
|
|
111
|
+
noDashboard = false;
|
|
112
|
+
/** Set by bridge to provide analytics report data (top tools, hooks, tasks) */
|
|
113
|
+
analyticsFn = null;
|
|
114
|
+
/** Set by bridge to answer GET /activity — history of recent tool calls + lifecycle events. */
|
|
115
|
+
activityFn = null;
|
|
116
|
+
/** Set by bridge to answer GET /approvals/:callId — decision record + nearby session activity. */
|
|
117
|
+
approvalDetailFn = null;
|
|
118
|
+
/** Set by bridge to answer GET /traces via ctxQueryTraces over persistent logs. */
|
|
119
|
+
tracesFn = null;
|
|
120
|
+
/** Set by bridge to handle CC hook notify events via POST /notify */
|
|
121
|
+
notifyFn = null;
|
|
122
|
+
/** Patchwork: set by bridge to list active agent sessions for the dashboard. */
|
|
123
|
+
sessionsFn = null;
|
|
124
|
+
/** Patchwork: set by bridge to answer GET /sessions/:id with per-session event stream + approvals. */
|
|
125
|
+
sessionDetailFn = null;
|
|
126
|
+
/** Set by bridge to handle POST /launch-quick-task — invokes launchQuickTask tool in-process. */
|
|
127
|
+
launchQuickTaskFn = null;
|
|
128
|
+
/**
|
|
129
|
+
* Attach an OAuth 2.0 Authorization Server.
|
|
130
|
+
* When set, the bridge exposes:
|
|
131
|
+
* GET /.well-known/oauth-authorization-server
|
|
132
|
+
* GET /oauth/authorize
|
|
133
|
+
* POST /oauth/token
|
|
134
|
+
* POST /oauth/revoke
|
|
135
|
+
* Bearer tokens issued via the OAuth flow are accepted in addition to the
|
|
136
|
+
* static bridge token, enabling claude.ai's authenticated MCP server flow.
|
|
137
|
+
*/
|
|
138
|
+
setOAuthServer(oauth, issuerUrl) {
|
|
139
|
+
this.oauthServer = oauth;
|
|
140
|
+
this.oauthIssuerUrl = issuerUrl;
|
|
141
|
+
}
|
|
142
|
+
/** Hosts accepted in the WebSocket upgrade Host header (DNS-rebinding guard). */
|
|
143
|
+
allowedHosts;
|
|
144
|
+
constructor(authToken, logger, extraCorsOrigins = [], pingIntervalMs = 30_000) {
|
|
145
|
+
super();
|
|
146
|
+
this.authToken = authToken;
|
|
147
|
+
this.logger = logger;
|
|
148
|
+
this.extraCorsOrigins = extraCorsOrigins;
|
|
149
|
+
this.pingIntervalMs = pingIntervalMs;
|
|
150
|
+
// Defense-in-depth: ensure token is non-empty so timingSafeTokenCompare
|
|
151
|
+
// cannot accept a blank Authorization header against an empty token.
|
|
152
|
+
if (authToken.length === 0) {
|
|
153
|
+
throw new Error("authToken must not be empty");
|
|
154
|
+
}
|
|
155
|
+
// Build the WS Host allowlist: loopback always allowed, plus hostnames
|
|
156
|
+
// extracted from --cors-origin values so remote reverse-proxy deployments
|
|
157
|
+
// (where the proxy forwards the real Host header) are not rejected.
|
|
158
|
+
this.allowedHosts = new Set(LOOPBACK_HOSTS);
|
|
159
|
+
for (const origin of extraCorsOrigins) {
|
|
160
|
+
try {
|
|
161
|
+
this.allowedHosts.add(new URL(origin).hostname);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// ignore malformed origins — already validated at startup
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (authToken.length < 32) {
|
|
168
|
+
logger.warn(`authToken is only ${authToken.length} chars — production tokens should be ≥ 32 chars (crypto.randomBytes(32).toString('hex'))`);
|
|
169
|
+
}
|
|
170
|
+
this.httpServer = http.createServer(async (req, res) => {
|
|
171
|
+
// Security headers on all responses
|
|
172
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
173
|
+
res.setHeader("Cache-Control", "no-store");
|
|
174
|
+
// CORS — set on every response so browsers can read 401s and initiate OAuth
|
|
175
|
+
const allowedOrigin = corsOrigin(req.headers.origin, this.extraCorsOrigins);
|
|
176
|
+
if (allowedOrigin) {
|
|
177
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
178
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
179
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
|
|
180
|
+
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
181
|
+
}
|
|
182
|
+
const parsedUrl = new URL(req.url ?? "/", "http://localhost");
|
|
183
|
+
// ── OAuth 2.0 endpoints (unauthenticated — handled before bearer check) ──
|
|
184
|
+
// RFC 8414 discovery document
|
|
185
|
+
if (parsedUrl.pathname === "/.well-known/oauth-authorization-server" &&
|
|
186
|
+
req.method === "GET") {
|
|
187
|
+
if (this.oauthServer) {
|
|
188
|
+
this.oauthServer.handleDiscovery(res);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
192
|
+
res.end("OAuth not configured");
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// RFC 9396 Protected Resource Metadata — Claude.ai probes this to discover
|
|
197
|
+
// which authorization server protects this resource. Both the bare and
|
|
198
|
+
// resource-path variants are handled.
|
|
199
|
+
if (req.method === "GET" &&
|
|
200
|
+
(parsedUrl.pathname === "/.well-known/oauth-protected-resource" ||
|
|
201
|
+
parsedUrl.pathname.startsWith("/.well-known/oauth-protected-resource/"))) {
|
|
202
|
+
if (this.oauthServer && this.oauthIssuerUrl) {
|
|
203
|
+
res.writeHead(200, {
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
"Cache-Control": "no-store",
|
|
206
|
+
});
|
|
207
|
+
res.end(JSON.stringify({
|
|
208
|
+
resource: this.oauthIssuerUrl,
|
|
209
|
+
authorization_servers: [this.oauthIssuerUrl],
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
214
|
+
res.end("OAuth not configured");
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Authorization endpoint
|
|
219
|
+
if (parsedUrl.pathname === "/oauth/authorize" &&
|
|
220
|
+
(req.method === "GET" || req.method === "POST")) {
|
|
221
|
+
if (this.oauthServer) {
|
|
222
|
+
this.oauthServer.handleAuthorize(req, res);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
226
|
+
res.end("OAuth not configured");
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Dynamic Client Registration endpoint (RFC 7591)
|
|
231
|
+
if (parsedUrl.pathname === "/oauth/register") {
|
|
232
|
+
if (this.oauthServer) {
|
|
233
|
+
this.oauthServer.handleRegister(req, res).catch((err) => {
|
|
234
|
+
if (!res.headersSent) {
|
|
235
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
236
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
242
|
+
res.end("OAuth not configured");
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// Token endpoint
|
|
247
|
+
if (parsedUrl.pathname === "/oauth/token" && req.method === "POST") {
|
|
248
|
+
if (this.oauthServer) {
|
|
249
|
+
this.oauthServer.handleToken(req, res).catch((err) => {
|
|
250
|
+
if (!res.headersSent) {
|
|
251
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
252
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
258
|
+
res.end("OAuth not configured");
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Revocation endpoint (RFC 7009)
|
|
263
|
+
if (parsedUrl.pathname === "/oauth/revoke" && req.method === "POST") {
|
|
264
|
+
if (this.oauthServer) {
|
|
265
|
+
this.oauthServer.handleRevoke(req, res).catch(() => {
|
|
266
|
+
// RFC 7009: always 200
|
|
267
|
+
if (!res.headersSent) {
|
|
268
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
269
|
+
res.end("{}");
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
275
|
+
res.end("{}");
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// ── MCP server-card (public) ──────────────────────────────────────────
|
|
280
|
+
if (req.url === "/.well-known/mcp/server-card.json" ||
|
|
281
|
+
req.url === "/.well-known/mcp") {
|
|
282
|
+
const card = {
|
|
283
|
+
name: "claude-ide-bridge",
|
|
284
|
+
version: BRIDGE_PROTOCOL_VERSION,
|
|
285
|
+
description: "MCP bridge providing full IDE integration for Claude Code — LSP, diagnostics, file operations, terminal, debug adapters, and AI task orchestration",
|
|
286
|
+
homepage: "https://github.com/Oolab-labs/claude-ide-bridge",
|
|
287
|
+
transport: ["websocket", "stdio", "streamable-http"],
|
|
288
|
+
capabilities: {
|
|
289
|
+
tools: true,
|
|
290
|
+
resources: true,
|
|
291
|
+
prompts: true,
|
|
292
|
+
elicitation: true,
|
|
293
|
+
},
|
|
294
|
+
author: "Oolab Labs",
|
|
295
|
+
license: PACKAGE_LICENSE,
|
|
296
|
+
repository: "https://github.com/Oolab-labs/claude-ide-bridge",
|
|
297
|
+
};
|
|
298
|
+
res.writeHead(200, {
|
|
299
|
+
"Content-Type": "application/json",
|
|
300
|
+
"Access-Control-Allow-Origin": "*",
|
|
301
|
+
});
|
|
302
|
+
res.end(JSON.stringify(card, null, 2));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// CORS preflight for /mcp — browsers (and Claude Desktop's web renderer) send
|
|
306
|
+
// OPTIONS before POST. Respond without requiring auth so the preflight succeeds.
|
|
307
|
+
if (req.method === "OPTIONS" && parsedUrl.pathname === "/mcp") {
|
|
308
|
+
const origin = corsOrigin(req.headers.origin, this.extraCorsOrigins);
|
|
309
|
+
if (origin) {
|
|
310
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
311
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
312
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
|
|
313
|
+
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
314
|
+
}
|
|
315
|
+
res.writeHead(204);
|
|
316
|
+
res.end();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Unauthenticated liveness probe — safe to expose; contains no sensitive data.
|
|
320
|
+
// NOTE: /dashboard and /dashboard/data are unauthenticated. They expose only
|
|
321
|
+
// version, uptime, session count, and extension state — no workspace paths
|
|
322
|
+
// or tool outputs. For public VPS deployments (--bind 0.0.0.0), restrict
|
|
323
|
+
// at nginx/firewall or disable with --no-dashboard.
|
|
324
|
+
if ((req.url === "/dashboard" || req.url === "/dashboard/") &&
|
|
325
|
+
req.method === "GET" &&
|
|
326
|
+
!this.noDashboard) {
|
|
327
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
328
|
+
res.end(renderDashboardHtml(PACKAGE_VERSION));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (req.url === "/dashboard/data" &&
|
|
332
|
+
req.method === "GET" &&
|
|
333
|
+
!this.noDashboard) {
|
|
334
|
+
const health = this.healthDataFn?.() ?? {};
|
|
335
|
+
const status = this.statusFn?.() ?? {};
|
|
336
|
+
const data = {
|
|
337
|
+
version: PACKAGE_VERSION,
|
|
338
|
+
uptimeMs: Date.now() - this.startTime,
|
|
339
|
+
sessions: this.wss.clients.size,
|
|
340
|
+
extensionConnected: health.extensionConnected ??
|
|
341
|
+
false,
|
|
342
|
+
extensionVersion: health
|
|
343
|
+
.extensionVersion,
|
|
344
|
+
events: status.events ?? [],
|
|
345
|
+
perf: this.perfDataFn?.() ?? null,
|
|
346
|
+
};
|
|
347
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
348
|
+
res.end(JSON.stringify(data));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (req.url === "/ping" && req.method === "GET") {
|
|
352
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
353
|
+
res.end(JSON.stringify({ ok: true, v: PACKAGE_VERSION }));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// ── Bearer token authentication ───────────────────────────────────────
|
|
357
|
+
// All other HTTP endpoints require a valid Bearer token.
|
|
358
|
+
// Accepts either:
|
|
359
|
+
// (a) the bridge's static token (--fixed-token / generated on start), or
|
|
360
|
+
// (b) an OAuth 2.0 access token issued via /oauth/token (when oauthServer is set)
|
|
361
|
+
const authHeader = req.headers.authorization ?? "";
|
|
362
|
+
const bearerFromHeader = authHeader.startsWith("Bearer ")
|
|
363
|
+
? authHeader.slice(7)
|
|
364
|
+
: "";
|
|
365
|
+
// ?token= query param support was removed — tokens in URLs appear in
|
|
366
|
+
// HTTP access logs, proxy logs, and Referrer headers. Use the
|
|
367
|
+
// Authorization: Bearer header exclusively.
|
|
368
|
+
const bearer = bearerFromHeader;
|
|
369
|
+
const isStaticToken = timingSafeTokenCompare(bearer, this.authToken);
|
|
370
|
+
const oauthResolved = !isStaticToken && this.oauthServer
|
|
371
|
+
? this.oauthServer.resolveBearerToken(bearer)
|
|
372
|
+
: null;
|
|
373
|
+
// oauthResolved is the bridge token if the OAuth token is valid; null otherwise
|
|
374
|
+
if (!isStaticToken && !oauthResolved) {
|
|
375
|
+
// RFC 6750: only include error= when a token was actually presented but invalid
|
|
376
|
+
const tokenPresented = bearer.length > 0;
|
|
377
|
+
const wwwAuth = this.oauthServer && this.oauthIssuerUrl
|
|
378
|
+
? `Bearer realm="claude-ide-bridge", resource_metadata="${this.oauthIssuerUrl}/.well-known/oauth-protected-resource"${tokenPresented ? `, error="invalid_token"` : ""}`
|
|
379
|
+
: `Bearer realm="claude-ide-bridge"${tokenPresented ? `, error="invalid_token"` : ""}`;
|
|
380
|
+
res.writeHead(401, {
|
|
381
|
+
"Content-Type": "text/plain",
|
|
382
|
+
"WWW-Authenticate": wwwAuth,
|
|
383
|
+
});
|
|
384
|
+
res.end("Unauthorized");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (parsedUrl.pathname === "/metrics" && req.method === "GET") {
|
|
388
|
+
try {
|
|
389
|
+
const body = this.metricsFn?.() ?? "";
|
|
390
|
+
res.writeHead(200, {
|
|
391
|
+
"Content-Type": "text/plain; version=0.0.4; charset=utf-8",
|
|
392
|
+
});
|
|
393
|
+
res.end(body);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
397
|
+
res.end(JSON.stringify({
|
|
398
|
+
error: err instanceof Error ? err.message : String(err),
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (req.url === "/health" && req.method === "GET") {
|
|
404
|
+
try {
|
|
405
|
+
const data = {
|
|
406
|
+
status: "ok",
|
|
407
|
+
uptimeMs: Date.now() - this.startTime,
|
|
408
|
+
connections: this.wss.clients.size,
|
|
409
|
+
...(this.healthDataFn?.() ?? {}),
|
|
410
|
+
};
|
|
411
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
412
|
+
res.end(JSON.stringify(data));
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
416
|
+
res.end(JSON.stringify({
|
|
417
|
+
error: err instanceof Error ? err.message : String(err),
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (parsedUrl.pathname === "/activity" && req.method === "GET") {
|
|
423
|
+
try {
|
|
424
|
+
const lastStr = parsedUrl.searchParams.get("last");
|
|
425
|
+
const last = lastStr !== null && /^\d+$/.test(lastStr)
|
|
426
|
+
? Math.min(Number(lastStr), 500)
|
|
427
|
+
: 100;
|
|
428
|
+
const data = this.activityFn?.(last) ?? [];
|
|
429
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
430
|
+
res.end(JSON.stringify({ events: data, count: data.length }));
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
434
|
+
res.end(JSON.stringify({
|
|
435
|
+
error: err instanceof Error ? err.message : String(err),
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (parsedUrl.pathname === "/traces" && req.method === "GET") {
|
|
441
|
+
try {
|
|
442
|
+
const q = parsedUrl.searchParams;
|
|
443
|
+
const sinceStr = q.get("since");
|
|
444
|
+
const limitStr = q.get("limit");
|
|
445
|
+
const data = this.tracesFn
|
|
446
|
+
? await this.tracesFn({
|
|
447
|
+
traceType: q.get("traceType") ?? undefined,
|
|
448
|
+
key: q.get("key") ?? undefined,
|
|
449
|
+
q: q.get("q") ?? undefined,
|
|
450
|
+
tag: q.get("tag") ?? undefined,
|
|
451
|
+
since: sinceStr !== null && /^\d+$/.test(sinceStr)
|
|
452
|
+
? Number(sinceStr)
|
|
453
|
+
: undefined,
|
|
454
|
+
limit: limitStr !== null && /^\d+$/.test(limitStr)
|
|
455
|
+
? Number(limitStr)
|
|
456
|
+
: undefined,
|
|
457
|
+
})
|
|
458
|
+
: {
|
|
459
|
+
traces: [],
|
|
460
|
+
count: 0,
|
|
461
|
+
sources: {
|
|
462
|
+
approval: false,
|
|
463
|
+
enrichment: false,
|
|
464
|
+
recipe_run: false,
|
|
465
|
+
},
|
|
466
|
+
hint: "Trace sources not wired yet.",
|
|
467
|
+
};
|
|
468
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
469
|
+
res.end(JSON.stringify(data));
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
473
|
+
res.end(JSON.stringify({
|
|
474
|
+
error: err instanceof Error ? err.message : String(err),
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (parsedUrl.pathname === "/analytics" && req.method === "GET") {
|
|
480
|
+
try {
|
|
481
|
+
const wh = parsedUrl.searchParams.get("windowHours");
|
|
482
|
+
const windowHours = wh !== null && /^\d+$/.test(wh) ? Number(wh) : undefined;
|
|
483
|
+
const data = await (this.analyticsFn
|
|
484
|
+
? this.analyticsFn(windowHours)
|
|
485
|
+
: Promise.resolve({
|
|
486
|
+
generatedAt: new Date().toISOString(),
|
|
487
|
+
windowHours: windowHours ?? 24,
|
|
488
|
+
topTools: [],
|
|
489
|
+
hooksLast24h: 0,
|
|
490
|
+
recentAutomationTasks: [],
|
|
491
|
+
hint: "Analytics not available — bridge driver not configured.",
|
|
492
|
+
}));
|
|
493
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
494
|
+
res.end(JSON.stringify(data));
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
498
|
+
res.end(JSON.stringify({
|
|
499
|
+
error: err instanceof Error ? err.message : String(err),
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (req.url === "/status" && req.method === "GET") {
|
|
505
|
+
try {
|
|
506
|
+
const data = {
|
|
507
|
+
uptimeMs: Date.now() - this.startTime,
|
|
508
|
+
...(this.statusFn?.() ?? {}),
|
|
509
|
+
};
|
|
510
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
511
|
+
res.end(JSON.stringify(data));
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
515
|
+
res.end(JSON.stringify({
|
|
516
|
+
error: err instanceof Error ? err.message : String(err),
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (req.url === "/ready" && req.method === "GET") {
|
|
522
|
+
try {
|
|
523
|
+
const info = this.readyFn?.() ?? {
|
|
524
|
+
ready: false,
|
|
525
|
+
toolCount: 0,
|
|
526
|
+
extensionConnected: false,
|
|
527
|
+
};
|
|
528
|
+
if (info.ready) {
|
|
529
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
530
|
+
res.end(JSON.stringify({
|
|
531
|
+
ready: true,
|
|
532
|
+
toolCount: info.toolCount,
|
|
533
|
+
extensionConnected: info.extensionConnected,
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
538
|
+
res.end(JSON.stringify({
|
|
539
|
+
ready: false,
|
|
540
|
+
reason: "awaiting MCP handshake",
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
546
|
+
res.end(JSON.stringify({
|
|
547
|
+
error: err instanceof Error ? err.message : String(err),
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (req.url === "/stream" && req.method === "GET") {
|
|
553
|
+
if (this.sseSubscriberCount >= Server.MAX_SSE_SUBSCRIBERS) {
|
|
554
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
555
|
+
res.end(JSON.stringify({ error: "Too many SSE subscribers (max 20)" }));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
this.sseSubscriberCount++;
|
|
559
|
+
// Disable socket timeout — SSE connections are long-lived by design
|
|
560
|
+
res.socket?.setTimeout(0);
|
|
561
|
+
res.writeHead(200, {
|
|
562
|
+
"Content-Type": "text/event-stream",
|
|
563
|
+
"Cache-Control": "no-cache",
|
|
564
|
+
Connection: "keep-alive",
|
|
565
|
+
});
|
|
566
|
+
res.flushHeaders();
|
|
567
|
+
const unsub = this.streamFn?.((kind, entry) => {
|
|
568
|
+
try {
|
|
569
|
+
res.write(`data: ${JSON.stringify({ kind, ...entry })}\n\n`);
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
// Client disconnected — unsubscribe on next tick
|
|
573
|
+
unsub?.();
|
|
574
|
+
}
|
|
575
|
+
}) ?? (() => { });
|
|
576
|
+
// Keep-alive comment ping every 15s so proxies don't close idle connections
|
|
577
|
+
const ping = setInterval(() => {
|
|
578
|
+
try {
|
|
579
|
+
res.write(": ping\n\n");
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
clearInterval(ping);
|
|
583
|
+
unsub();
|
|
584
|
+
}
|
|
585
|
+
}, 15_000);
|
|
586
|
+
ping.unref();
|
|
587
|
+
req.on("close", () => {
|
|
588
|
+
this.sseSubscriberCount--;
|
|
589
|
+
clearInterval(ping);
|
|
590
|
+
unsub();
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (req.url === "/tasks" && req.method === "GET") {
|
|
595
|
+
try {
|
|
596
|
+
const data = this.tasksFn?.() ?? { tasks: [] };
|
|
597
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
598
|
+
res.end(JSON.stringify(data));
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
602
|
+
res.end(JSON.stringify({
|
|
603
|
+
error: err instanceof Error ? err.message : String(err),
|
|
604
|
+
}));
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (parsedUrl.pathname?.startsWith("/hooks/") && req.method === "POST") {
|
|
609
|
+
const hookPath = parsedUrl.pathname.substring("/hooks".length);
|
|
610
|
+
const chunks = [];
|
|
611
|
+
req.on("data", (c) => chunks.push(c));
|
|
612
|
+
req.on("end", () => {
|
|
613
|
+
void (async () => {
|
|
614
|
+
let payload;
|
|
615
|
+
if (chunks.length > 0) {
|
|
616
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
617
|
+
if (body.trim()) {
|
|
618
|
+
try {
|
|
619
|
+
payload = JSON.parse(body);
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
payload = body;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (!this.webhookFn) {
|
|
627
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
628
|
+
res.end(JSON.stringify({
|
|
629
|
+
ok: false,
|
|
630
|
+
error: "Webhooks unavailable — start bridge with --claude-driver subprocess",
|
|
631
|
+
}));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const result = await this.webhookFn(hookPath, payload);
|
|
635
|
+
const status = result.ok
|
|
636
|
+
? 200
|
|
637
|
+
: result.error === "not_found"
|
|
638
|
+
? 404
|
|
639
|
+
: 400;
|
|
640
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
641
|
+
res.end(JSON.stringify(result));
|
|
642
|
+
})();
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (parsedUrl.pathname === "/recipes/run" && req.method === "POST") {
|
|
647
|
+
const chunks = [];
|
|
648
|
+
req.on("data", (c) => chunks.push(c));
|
|
649
|
+
req.on("end", () => {
|
|
650
|
+
void (async () => {
|
|
651
|
+
try {
|
|
652
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
653
|
+
const parsed = JSON.parse(body || "{}");
|
|
654
|
+
const name = parsed.name;
|
|
655
|
+
if (typeof name !== "string" || !name) {
|
|
656
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
657
|
+
res.end(JSON.stringify({ ok: false, error: "name required" }));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (!this.runRecipeFn) {
|
|
661
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
662
|
+
res.end(JSON.stringify({
|
|
663
|
+
ok: false,
|
|
664
|
+
error: "Recipe execution unavailable — requires --claude-driver subprocess",
|
|
665
|
+
}));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const result = await this.runRecipeFn(name);
|
|
669
|
+
res.writeHead(result.ok ? 200 : 400, {
|
|
670
|
+
"Content-Type": "application/json",
|
|
671
|
+
});
|
|
672
|
+
res.end(JSON.stringify(result));
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
676
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
677
|
+
}
|
|
678
|
+
})();
|
|
679
|
+
});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (parsedUrl.pathname === "/runs" && req.method === "GET") {
|
|
683
|
+
try {
|
|
684
|
+
const sp = parsedUrl.searchParams;
|
|
685
|
+
const limitRaw = sp.get("limit");
|
|
686
|
+
const afterRaw = sp.get("after");
|
|
687
|
+
const trigger = sp.get("trigger");
|
|
688
|
+
const status = sp.get("status");
|
|
689
|
+
const recipe = sp.get("recipe");
|
|
690
|
+
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : Number.NaN;
|
|
691
|
+
const after = afterRaw ? Number.parseInt(afterRaw, 10) : Number.NaN;
|
|
692
|
+
const runs = this.runsFn?.({
|
|
693
|
+
...(Number.isFinite(limit) && { limit }),
|
|
694
|
+
...(trigger && { trigger }),
|
|
695
|
+
...(status && { status }),
|
|
696
|
+
...(recipe && { recipe }),
|
|
697
|
+
...(Number.isFinite(after) && { after }),
|
|
698
|
+
}) ?? [];
|
|
699
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
700
|
+
res.end(JSON.stringify({ runs }));
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
704
|
+
res.end(JSON.stringify({
|
|
705
|
+
error: err instanceof Error ? err.message : String(err),
|
|
706
|
+
}));
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (req.url === "/recipes" && req.method === "POST") {
|
|
711
|
+
const chunks = [];
|
|
712
|
+
req.on("data", (c) => chunks.push(c));
|
|
713
|
+
req.on("end", () => {
|
|
714
|
+
try {
|
|
715
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
716
|
+
const draft = JSON.parse(body || "{}");
|
|
717
|
+
if (typeof draft.name !== "string" || !draft.name) {
|
|
718
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
719
|
+
res.end(JSON.stringify({ ok: false, error: "name required" }));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (!this.saveRecipeFn) {
|
|
723
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
724
|
+
res.end(JSON.stringify({
|
|
725
|
+
ok: false,
|
|
726
|
+
error: "Recipe saving unavailable",
|
|
727
|
+
}));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const result = this.saveRecipeFn(draft);
|
|
731
|
+
res.writeHead(result.ok ? 201 : 400, {
|
|
732
|
+
"Content-Type": "application/json",
|
|
733
|
+
});
|
|
734
|
+
res.end(JSON.stringify(result));
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
738
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (req.url === "/recipes" && req.method === "GET") {
|
|
744
|
+
try {
|
|
745
|
+
const data = this.recipesFn?.() ?? { recipesDir: null, recipes: [] };
|
|
746
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
747
|
+
res.end(JSON.stringify(data));
|
|
748
|
+
}
|
|
749
|
+
catch (err) {
|
|
750
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
751
|
+
res.end(JSON.stringify({
|
|
752
|
+
error: err instanceof Error ? err.message : String(err),
|
|
753
|
+
}));
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const sessionDetailMatch = /^\/sessions\/([A-Za-z0-9-]+)$/.exec(parsedUrl.pathname);
|
|
758
|
+
if (sessionDetailMatch && req.method === "GET") {
|
|
759
|
+
const id = sessionDetailMatch[1];
|
|
760
|
+
try {
|
|
761
|
+
const data = this.sessionDetailFn?.(id);
|
|
762
|
+
if (!data || !data.summary) {
|
|
763
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
764
|
+
res.end(JSON.stringify({ error: "unknown sessionId" }));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
768
|
+
res.end(JSON.stringify(data));
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
772
|
+
res.end(JSON.stringify({
|
|
773
|
+
error: err instanceof Error ? err.message : String(err),
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (parsedUrl.pathname === "/sessions" && req.method === "GET") {
|
|
779
|
+
try {
|
|
780
|
+
if (!this.sessionsFn) {
|
|
781
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
782
|
+
res.end(JSON.stringify({ error: "sessions not available" }));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const data = this.sessionsFn();
|
|
786
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
787
|
+
res.end(JSON.stringify(data));
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
791
|
+
res.end(JSON.stringify({
|
|
792
|
+
error: err instanceof Error ? err.message : String(err),
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (parsedUrl.pathname === "/settings" && req.method === "POST") {
|
|
798
|
+
const chunks = [];
|
|
799
|
+
req.on("data", (c) => chunks.push(c));
|
|
800
|
+
req.on("end", () => {
|
|
801
|
+
try {
|
|
802
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
803
|
+
const raw = body.webhookUrl?.trim() ?? "";
|
|
804
|
+
if (raw !== "" && !/^https:\/\/.+/.test(raw)) {
|
|
805
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
806
|
+
res.end(JSON.stringify({ error: "webhookUrl must be HTTPS" }));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const gateRaw = body.approvalGate;
|
|
810
|
+
if (gateRaw !== undefined &&
|
|
811
|
+
gateRaw !== "off" &&
|
|
812
|
+
gateRaw !== "high" &&
|
|
813
|
+
gateRaw !== "all") {
|
|
814
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
815
|
+
res.end(JSON.stringify({
|
|
816
|
+
error: 'approvalGate must be "off", "high", or "all"',
|
|
817
|
+
}));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const configPath = patchworkConfigPath();
|
|
821
|
+
const cfg = loadPatchworkConfig(configPath);
|
|
822
|
+
cfg.dashboard = {
|
|
823
|
+
port: cfg.dashboard?.port ?? 3000,
|
|
824
|
+
requireApproval: cfg.dashboard?.requireApproval ?? ["high"],
|
|
825
|
+
pushNotifications: cfg.dashboard?.pushNotifications ?? false,
|
|
826
|
+
webhookUrl: raw || undefined,
|
|
827
|
+
};
|
|
828
|
+
if (gateRaw !== undefined) {
|
|
829
|
+
cfg.approvalGate = gateRaw;
|
|
830
|
+
this.approvalGate = gateRaw;
|
|
831
|
+
}
|
|
832
|
+
savePatchworkConfig(cfg, configPath);
|
|
833
|
+
this.approvalWebhookUrl = raw || undefined;
|
|
834
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
835
|
+
res.end(JSON.stringify({ ok: true }));
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
839
|
+
res.end(JSON.stringify({
|
|
840
|
+
error: err instanceof Error ? err.message : String(err),
|
|
841
|
+
}));
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
// CC hook notify endpoint — lightweight alternative to full MCP session for hook wiring.
|
|
847
|
+
if (parsedUrl.pathname === "/notify" && req.method === "POST") {
|
|
848
|
+
const chunks = [];
|
|
849
|
+
req.on("data", (c) => chunks.push(c));
|
|
850
|
+
req.on("end", () => {
|
|
851
|
+
try {
|
|
852
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
853
|
+
const parsed = JSON.parse(body);
|
|
854
|
+
const event = parsed.event ?? "";
|
|
855
|
+
const args = parsed.args ?? {};
|
|
856
|
+
if (!this.notifyFn) {
|
|
857
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
858
|
+
res.end(JSON.stringify({
|
|
859
|
+
ok: false,
|
|
860
|
+
error: "Automation not enabled",
|
|
861
|
+
}));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const result = this.notifyFn(event, args);
|
|
865
|
+
res.writeHead(result.ok ? 200 : 400, {
|
|
866
|
+
"Content-Type": "application/json",
|
|
867
|
+
});
|
|
868
|
+
res.end(JSON.stringify(result));
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
872
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
// Single-approval detail lookup for the dashboard detail page.
|
|
878
|
+
const detailMatch = /^\/approvals\/([A-Za-z0-9-]+)$/.exec(parsedUrl.pathname);
|
|
879
|
+
if (detailMatch && req.method === "GET") {
|
|
880
|
+
const callId = detailMatch[1];
|
|
881
|
+
try {
|
|
882
|
+
const data = this.approvalDetailFn?.(callId);
|
|
883
|
+
if (!data || (!data.pending && !data.decision)) {
|
|
884
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
885
|
+
res.end(JSON.stringify({ error: "unknown callId" }));
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
889
|
+
res.end(JSON.stringify(data));
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
893
|
+
res.end(JSON.stringify({
|
|
894
|
+
error: err instanceof Error ? err.message : String(err),
|
|
895
|
+
}));
|
|
896
|
+
}
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
// Patchwork approval surface — PreToolUse hook + dashboard approve/reject.
|
|
900
|
+
// Bearer auth already checked above.
|
|
901
|
+
if (parsedUrl.pathname === "/approvals" ||
|
|
902
|
+
parsedUrl.pathname === "/cc-permissions" ||
|
|
903
|
+
/^\/(approve|reject)\/[A-Za-z0-9-]+$/.test(parsedUrl.pathname)) {
|
|
904
|
+
const chunks = [];
|
|
905
|
+
req.on("data", (c) => chunks.push(c));
|
|
906
|
+
req.on("end", async () => {
|
|
907
|
+
let parsedBody;
|
|
908
|
+
if (chunks.length > 0) {
|
|
909
|
+
try {
|
|
910
|
+
parsedBody = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
914
|
+
res.end(JSON.stringify({ error: "invalid JSON body" }));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
const result = await routeApprovalRequest({
|
|
920
|
+
method: req.method ?? "GET",
|
|
921
|
+
path: parsedUrl.pathname,
|
|
922
|
+
body: parsedBody,
|
|
923
|
+
query: parsedUrl.searchParams,
|
|
924
|
+
}, {
|
|
925
|
+
queue: getApprovalQueue(),
|
|
926
|
+
workspace: process.cwd(),
|
|
927
|
+
managedSettingsPath: this.managedSettingsPath,
|
|
928
|
+
onDecision: this.onApprovalDecision,
|
|
929
|
+
webhookUrl: this.approvalWebhookUrl,
|
|
930
|
+
approvalGate: this.approvalGate,
|
|
931
|
+
});
|
|
932
|
+
res.writeHead(result.status, {
|
|
933
|
+
"Content-Type": "application/json",
|
|
934
|
+
});
|
|
935
|
+
res.end(JSON.stringify(result.body));
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
939
|
+
res.end(JSON.stringify({
|
|
940
|
+
error: err instanceof Error ? err.message : String(err),
|
|
941
|
+
}));
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
// Quick-task launch endpoint — mirrors /notify pattern. Bearer auth already checked above.
|
|
947
|
+
if (parsedUrl.pathname === "/launch-quick-task" &&
|
|
948
|
+
req.method === "POST") {
|
|
949
|
+
const chunks = [];
|
|
950
|
+
req.on("data", (c) => chunks.push(c));
|
|
951
|
+
req.on("end", () => {
|
|
952
|
+
void (async () => {
|
|
953
|
+
try {
|
|
954
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
955
|
+
const parsed = JSON.parse(body);
|
|
956
|
+
const presetId = parsed.presetId;
|
|
957
|
+
const source = parsed.source ?? "cli";
|
|
958
|
+
if (typeof presetId !== "string" || !presetId) {
|
|
959
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
960
|
+
res.end(JSON.stringify({
|
|
961
|
+
ok: false,
|
|
962
|
+
error: "presetId required",
|
|
963
|
+
}));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (!this.launchQuickTaskFn) {
|
|
967
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
968
|
+
res.end(JSON.stringify({
|
|
969
|
+
ok: false,
|
|
970
|
+
error: "Quick tasks unavailable — requires --claude-driver subprocess",
|
|
971
|
+
}));
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const result = await this.launchQuickTaskFn(presetId, source);
|
|
975
|
+
res.writeHead(result.ok ? 200 : 429, {
|
|
976
|
+
"Content-Type": "application/json",
|
|
977
|
+
});
|
|
978
|
+
res.end(JSON.stringify(result));
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
982
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
983
|
+
}
|
|
984
|
+
})();
|
|
985
|
+
});
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
// MCP Streamable HTTP transport — POST/GET/DELETE /mcp.
|
|
989
|
+
// Bearer auth is already checked above, so all requests here are authenticated.
|
|
990
|
+
// The Mcp-Session-Id header routes to the correct session.
|
|
991
|
+
// OPTIONS is handled before auth so CORS preflight works.
|
|
992
|
+
if (parsedUrl.pathname === "/mcp" && this.httpMcpHandler) {
|
|
993
|
+
if (req.method === "POST" ||
|
|
994
|
+
req.method === "GET" ||
|
|
995
|
+
req.method === "DELETE") {
|
|
996
|
+
this.httpMcpHandler(req, res).catch((err) => {
|
|
997
|
+
this.logger.error(`HTTP MCP handler error: ${err instanceof Error ? err.message : String(err)}`);
|
|
998
|
+
if (!res.headersSent) {
|
|
999
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1000
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1007
|
+
res.end("Not found");
|
|
1008
|
+
});
|
|
1009
|
+
// Do NOT pass server — we handle upgrade manually for pre-handshake auth
|
|
1010
|
+
this.wss = new WsServer({
|
|
1011
|
+
noServer: true,
|
|
1012
|
+
maxPayload: 4 * 1024 * 1024,
|
|
1013
|
+
perMessageDeflate: false,
|
|
1014
|
+
});
|
|
1015
|
+
// Authenticate on upgrade BEFORE completing the WebSocket handshake
|
|
1016
|
+
this.httpServer.on("upgrade", (request, socket, head) => {
|
|
1017
|
+
// Prevent unhandled error events on the raw socket during upgrade
|
|
1018
|
+
socket.on("error", () => socket.destroy());
|
|
1019
|
+
// Validate Host header to defend against DNS rebinding.
|
|
1020
|
+
// Strip port suffix, handling both IPv4 (host:port) and IPv6 ([::1]:port).
|
|
1021
|
+
const rawHost = request.headers.host ?? "";
|
|
1022
|
+
const host = rawHost.startsWith("[")
|
|
1023
|
+
? rawHost.slice(0, rawHost.indexOf("]") + 1) // [::1]:port → [::1]
|
|
1024
|
+
: rawHost.replace(/:\d+$/, ""); // host:port → host
|
|
1025
|
+
if (!host || !this.allowedHosts.has(host)) {
|
|
1026
|
+
this.logger.warn(`Rejected connection with invalid Host header: ${host}`);
|
|
1027
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1028
|
+
socket.destroy();
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
// Reject browser-originated connections — a browser tab on any origin can
|
|
1032
|
+
// connect to ws://localhost:<port> but will always send an Origin header.
|
|
1033
|
+
// VS Code extension and Claude Code CLI connections either omit Origin or
|
|
1034
|
+
// send "vscode-file://" / "vscode-webview://". Any other origin is a browser
|
|
1035
|
+
// page attempting a cross-origin WebSocket and is rejected here as defense-
|
|
1036
|
+
// in-depth (the auth token is the primary guard).
|
|
1037
|
+
const origin = request.headers.origin;
|
|
1038
|
+
if (origin !== undefined &&
|
|
1039
|
+
!origin.startsWith("vscode-file://") &&
|
|
1040
|
+
!origin.startsWith("vscode-webview://") &&
|
|
1041
|
+
!origin.startsWith("vscode-app://")) {
|
|
1042
|
+
this.logger.warn(`Rejected connection with unexpected Origin header: ${origin}`);
|
|
1043
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1044
|
+
socket.destroy();
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const now = Date.now();
|
|
1048
|
+
// Check for extension connection (distinct header)
|
|
1049
|
+
const extensionToken = request.headers["x-claude-ide-extension"];
|
|
1050
|
+
if (typeof extensionToken === "string" &&
|
|
1051
|
+
timingSafeTokenCompare(extensionToken, this.authToken)) {
|
|
1052
|
+
// Rate limit per client type to prevent connection-storm DoS
|
|
1053
|
+
if (now - this.lastExtensionConnectionTime <
|
|
1054
|
+
MIN_CONNECTION_INTERVAL_MS) {
|
|
1055
|
+
socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n");
|
|
1056
|
+
socket.destroy();
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
this.lastExtensionConnectionTime = now;
|
|
1060
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
1061
|
+
const alive = ws;
|
|
1062
|
+
alive.isAlive = true;
|
|
1063
|
+
alive.missedPongs = 0;
|
|
1064
|
+
alive.lastPongTime = Date.now();
|
|
1065
|
+
enableTcpKeepalive(ws);
|
|
1066
|
+
setupPongHandler(alive);
|
|
1067
|
+
this.emit("extension", ws);
|
|
1068
|
+
ws.once("close", () => {
|
|
1069
|
+
this.lastExtensionConnectionTime = 0;
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
// Check for Claude Code connection
|
|
1075
|
+
const token = request.headers["x-claude-code-ide-authorization"];
|
|
1076
|
+
if (typeof token !== "string" ||
|
|
1077
|
+
!timingSafeTokenCompare(token, this.authToken)) {
|
|
1078
|
+
this.logger.warn("Rejected unauthorized WebSocket upgrade");
|
|
1079
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
1080
|
+
socket.destroy();
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
// Rate limit per client type to prevent connection-storm DoS
|
|
1084
|
+
if (now - this.lastClaudeConnectionTime < MIN_CONNECTION_INTERVAL_MS) {
|
|
1085
|
+
socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n");
|
|
1086
|
+
socket.destroy();
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
this.lastClaudeConnectionTime = now;
|
|
1090
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
1091
|
+
this.wss.emit("connection", ws, request);
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
this.wss.on("connection", (raw, req) => {
|
|
1095
|
+
const ws = raw;
|
|
1096
|
+
// Propagate the client-supplied session ID (for grace-period resumption) so
|
|
1097
|
+
// the bridge handler can look up an in-grace session without needing the raw request.
|
|
1098
|
+
const incomingSessionId = req.headers["x-claude-code-session-id"];
|
|
1099
|
+
if (typeof incomingSessionId === "string" &&
|
|
1100
|
+
SESSION_ID_RE.test(incomingSessionId)) {
|
|
1101
|
+
ws.clientSessionId = incomingSessionId;
|
|
1102
|
+
}
|
|
1103
|
+
this.logger.debug("Claude Code connected");
|
|
1104
|
+
ws.isAlive = true;
|
|
1105
|
+
ws.missedPongs = 0;
|
|
1106
|
+
ws.lastPongTime = Date.now();
|
|
1107
|
+
ws.lastPingTime = 0;
|
|
1108
|
+
enableTcpKeepalive(raw);
|
|
1109
|
+
setupPongHandler(ws);
|
|
1110
|
+
ws.on("close", (code, reason) => {
|
|
1111
|
+
// Strip control chars (newlines, ANSI codes) to prevent log injection.
|
|
1112
|
+
// Cap at 123 bytes — RFC 6455 §7.4 limit for close reason payloads.
|
|
1113
|
+
const reasonStr = reason?.length
|
|
1114
|
+
? reason
|
|
1115
|
+
.toString()
|
|
1116
|
+
.replace(/[\x00-\x1f\x7f]/g, "")
|
|
1117
|
+
.slice(0, 123)
|
|
1118
|
+
: "(none)";
|
|
1119
|
+
const lastSeen = ws.lastPongTime
|
|
1120
|
+
? `${Math.round((Date.now() - ws.lastPongTime) / 1000)}s ago`
|
|
1121
|
+
: "never";
|
|
1122
|
+
this.logger.info(`Claude Code WebSocket closed — code=${code} reason="${reasonStr}" ` +
|
|
1123
|
+
`disconnectReason=${ws.disconnectReason ?? "client_initiated"} ` +
|
|
1124
|
+
`missedPongs=${ws.missedPongs ?? 0} lastPong=${lastSeen}`);
|
|
1125
|
+
});
|
|
1126
|
+
ws.on("error", (err) => {
|
|
1127
|
+
this.logger.error(`WebSocket client error: ${err.message}`);
|
|
1128
|
+
});
|
|
1129
|
+
this.emit("connection", ws);
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
async listen(port, bindAddress = "127.0.0.1") {
|
|
1133
|
+
const LOOPBACK = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
1134
|
+
if (!LOOPBACK.has(bindAddress)) {
|
|
1135
|
+
this.logger.warn(`WARNING: Bridge bound to ${bindAddress} — not a loopback address. Any host that can reach this address and obtain the auth token can connect. Use --bind 127.0.0.1 (default) for local-only access.`);
|
|
1136
|
+
}
|
|
1137
|
+
// Mitigate slow-loris attacks: bound the headers phase.
|
|
1138
|
+
// requestTimeout is NOT disabled globally — SSE handlers must disable it
|
|
1139
|
+
// per-response via `res.socket?.setTimeout(0)` for their own long-lived stream.
|
|
1140
|
+
this.httpServer.headersTimeout = 5_000;
|
|
1141
|
+
this.httpServer.requestTimeout = 30_000;
|
|
1142
|
+
return new Promise((resolve, reject) => {
|
|
1143
|
+
this.httpServer
|
|
1144
|
+
.listen(port, bindAddress, () => {
|
|
1145
|
+
const addr = this.httpServer.address();
|
|
1146
|
+
if (!addr || typeof addr === "string") {
|
|
1147
|
+
if (this.pingInterval) {
|
|
1148
|
+
clearInterval(this.pingInterval);
|
|
1149
|
+
this.pingInterval = null;
|
|
1150
|
+
}
|
|
1151
|
+
reject(new Error("Unexpected server address"));
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
// Ping clients every pingIntervalMs (default 30s); terminate after 4 missed pongs (120s tolerance)
|
|
1155
|
+
this.pingInterval = setInterval(() => {
|
|
1156
|
+
const now = Date.now();
|
|
1157
|
+
for (const raw of this.wss.clients) {
|
|
1158
|
+
const client = raw;
|
|
1159
|
+
// Sleep/wake detection: if timer fired much later than expected,
|
|
1160
|
+
// the system likely slept. Reset and probe instead of killing.
|
|
1161
|
+
if (client.lastPingTime && now - client.lastPingTime > 45_000) {
|
|
1162
|
+
client.missedPongs = 0;
|
|
1163
|
+
client.isAlive = false;
|
|
1164
|
+
client.lastPingTime = now;
|
|
1165
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1166
|
+
client.ping(Buffer.from(now.toString()));
|
|
1167
|
+
}
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if (!client.isAlive) {
|
|
1171
|
+
client.missedPongs = (client.missedPongs ?? 0) + 1;
|
|
1172
|
+
if (client.missedPongs >= 4) {
|
|
1173
|
+
client.disconnectReason = "pong_timeout";
|
|
1174
|
+
this.logger.warn(`Terminating unresponsive client — 4 missed pongs ` +
|
|
1175
|
+
`lastPong=${client.lastPongTime ? `${Math.round((now - client.lastPongTime) / 1000)}s ago` : "never"}`);
|
|
1176
|
+
client.terminate();
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
client.isAlive = false;
|
|
1181
|
+
client.lastPingTime = now;
|
|
1182
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1183
|
+
client.ping(Buffer.from(now.toString()));
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
// Enforce a minimum interval so a misconfigured value cannot spin
|
|
1187
|
+
// the event loop (setInterval(fn, 0) would fire on every tick).
|
|
1188
|
+
// 10 ms floor is low enough for tests, high enough to be safe.
|
|
1189
|
+
}, Math.max(10, this.pingIntervalMs));
|
|
1190
|
+
resolve(addr.port);
|
|
1191
|
+
})
|
|
1192
|
+
.on("error", reject);
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
async findAndListen(preferredPort, bindAddress = "127.0.0.1") {
|
|
1196
|
+
if (preferredPort) {
|
|
1197
|
+
if (preferredPort < 1 || preferredPort > 65535) {
|
|
1198
|
+
this.logger.warn(`Invalid port ${preferredPort} (must be 1-65535), falling back to OS-assigned port`);
|
|
1199
|
+
// Fall through to OS-assigned port below
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
return this.listen(preferredPort, bindAddress);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
// Port 0 lets the OS kernel assign a free port atomically
|
|
1206
|
+
return this.listen(0, bindAddress);
|
|
1207
|
+
}
|
|
1208
|
+
async close() {
|
|
1209
|
+
if (this.pingInterval)
|
|
1210
|
+
clearInterval(this.pingInterval);
|
|
1211
|
+
for (const client of this.wss.clients) {
|
|
1212
|
+
client.close(1001, "Server shutting down");
|
|
1213
|
+
}
|
|
1214
|
+
// Wait up to 2s for graceful disconnect, then force-terminate
|
|
1215
|
+
await new Promise((resolve) => {
|
|
1216
|
+
const forceTimer = setTimeout(() => {
|
|
1217
|
+
for (const client of this.wss.clients) {
|
|
1218
|
+
client.terminate();
|
|
1219
|
+
}
|
|
1220
|
+
resolve();
|
|
1221
|
+
}, 2000);
|
|
1222
|
+
this.wss.close(() => {
|
|
1223
|
+
clearTimeout(forceTimer);
|
|
1224
|
+
resolve();
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
// Drain idle keep-alive HTTP connections (Node 20+ native)
|
|
1228
|
+
this.httpServer.closeIdleConnections();
|
|
1229
|
+
await new Promise((resolve) => {
|
|
1230
|
+
// Hard failsafe: force-close any remaining HTTP connections after 10s
|
|
1231
|
+
const hardTimer = setTimeout(() => {
|
|
1232
|
+
this.httpServer.closeAllConnections();
|
|
1233
|
+
resolve();
|
|
1234
|
+
}, 10_000);
|
|
1235
|
+
this.httpServer.close((err) => {
|
|
1236
|
+
clearTimeout(hardTimer);
|
|
1237
|
+
if (err)
|
|
1238
|
+
this.logger.error(`HTTP server close error: ${err.message}`);
|
|
1239
|
+
resolve();
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
//# sourceMappingURL=server.js.map
|