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/bridge.js
ADDED
|
@@ -0,0 +1,1535 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { WebSocket } from "ws";
|
|
6
|
+
import { ActivityLog } from "./activityLog.js";
|
|
7
|
+
import { buildSummary } from "./analyticsAggregator.js";
|
|
8
|
+
import { getAnalyticsPref } from "./analyticsPrefs.js";
|
|
9
|
+
import { sendAnalytics } from "./analyticsSend.js";
|
|
10
|
+
import { getApprovalQueue } from "./approvalQueue.js";
|
|
11
|
+
import { AutomationHooks, loadPolicy } from "./automation.js";
|
|
12
|
+
import { loadOrCreateBridgeToken } from "./bridgeToken.js";
|
|
13
|
+
import { repairBridgeToolsRulesIfStale } from "./bridgeToolsRules.js";
|
|
14
|
+
import { createDriver } from "./claudeDriver.js";
|
|
15
|
+
import { ClaudeOrchestrator } from "./claudeOrchestrator.js";
|
|
16
|
+
import { CommitIssueLinkLog } from "./commitIssueLinkLog.js";
|
|
17
|
+
import { DecisionTraceLog } from "./decisionTraceLog.js";
|
|
18
|
+
import { ExtensionClient } from "./extensionClient.js";
|
|
19
|
+
import { FileLock } from "./fileLock.js";
|
|
20
|
+
import { buildEnforcementReminder } from "./instructionsUtils.js";
|
|
21
|
+
import { LockFileManager } from "./lockfile.js";
|
|
22
|
+
import { Logger } from "./logger.js";
|
|
23
|
+
import { OAuthServerImpl } from "./oauth.js";
|
|
24
|
+
import { loadPlugins, loadPluginsFull } from "./pluginLoader.js";
|
|
25
|
+
import { PluginWatcher } from "./pluginWatcher.js";
|
|
26
|
+
import { probeAll } from "./probe.js";
|
|
27
|
+
import { RecipeScheduler } from "./recipes/scheduler.js";
|
|
28
|
+
import { findWebhookRecipe, listInstalledRecipes, loadRecipePrompt, renderWebhookPrompt, saveRecipe, } from "./recipesHttp.js";
|
|
29
|
+
import { classifyTool } from "./riskTier.js";
|
|
30
|
+
import { RecipeRunLog } from "./runLog.js";
|
|
31
|
+
import { Server } from "./server.js";
|
|
32
|
+
import { SessionCheckpoint } from "./sessionCheckpoint.js";
|
|
33
|
+
import { StreamableHttpHandler } from "./streamableHttp.js";
|
|
34
|
+
import { initTelemetry, shutdownTelemetry } from "./telemetry.js";
|
|
35
|
+
import { createCtxQueryTracesTool } from "./tools/ctxQueryTraces.js";
|
|
36
|
+
import { readNote, writeNote } from "./tools/handoffNote.js";
|
|
37
|
+
import { registerAllTools } from "./tools/index.js";
|
|
38
|
+
import { cleanupTempDirs } from "./tools/openDiff.js";
|
|
39
|
+
import { buildRecentTracesDigest } from "./tools/recentTracesDigest.js";
|
|
40
|
+
import { resolveFilePath } from "./tools/utils.js";
|
|
41
|
+
import { McpTransport } from "./transport.js";
|
|
42
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
43
|
+
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
44
|
+
let globalHandlersRegistered = false;
|
|
45
|
+
/** Collect the union of openedFiles across all sessions in a checkpoint. */
|
|
46
|
+
export function extractRestoredFiles(checkpoint, workspace) {
|
|
47
|
+
const all = new Set();
|
|
48
|
+
for (const s of checkpoint.sessions) {
|
|
49
|
+
for (const f of s.openedFiles) {
|
|
50
|
+
// Only restore paths that resolve safely within the workspace
|
|
51
|
+
try {
|
|
52
|
+
resolveFilePath(f, workspace);
|
|
53
|
+
all.add(f);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// skip paths that fail workspace containment
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return all;
|
|
61
|
+
}
|
|
62
|
+
function formatDuration(ms) {
|
|
63
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
64
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
65
|
+
const seconds = totalSeconds % 60;
|
|
66
|
+
if (minutes === 0)
|
|
67
|
+
return `${seconds}s`;
|
|
68
|
+
return `${minutes}m ${seconds}s`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Detect whether the workspace is a linked git worktree (not the main one).
|
|
72
|
+
* In the main worktree `.git` is a directory; in a linked worktree it is a
|
|
73
|
+
* file containing `gitdir: <path>`. Used to surface a Cowork-awareness hint
|
|
74
|
+
* at startup — Cowork runs in a linked worktree, but so do many non-Cowork
|
|
75
|
+
* workflows, so the hint is informational only.
|
|
76
|
+
*/
|
|
77
|
+
export function isInGitWorktree(workspace) {
|
|
78
|
+
try {
|
|
79
|
+
const stat = fs.statSync(path.join(workspace, ".git"));
|
|
80
|
+
return stat.isFile();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export class Bridge {
|
|
87
|
+
config;
|
|
88
|
+
logger;
|
|
89
|
+
lockFile;
|
|
90
|
+
server;
|
|
91
|
+
extensionClient;
|
|
92
|
+
activityLog;
|
|
93
|
+
authToken;
|
|
94
|
+
sessions = new Map();
|
|
95
|
+
fileLock = new FileLock();
|
|
96
|
+
probes = null;
|
|
97
|
+
ready = false;
|
|
98
|
+
stopped = false;
|
|
99
|
+
listChangedTimer = null;
|
|
100
|
+
/** True when sendListChanged fired but no session had an open WS to receive it. */
|
|
101
|
+
pendingListChanged = false;
|
|
102
|
+
lastConnectAt = null;
|
|
103
|
+
lastDisconnectAt = null;
|
|
104
|
+
lastDisconnectCode = null;
|
|
105
|
+
lastDisconnectReason = null;
|
|
106
|
+
checkpoint = null;
|
|
107
|
+
orchestrator = null;
|
|
108
|
+
/** openedFiles restored from the previous-run checkpoint; consumed by the first connecting session. */
|
|
109
|
+
restoredOpenedFiles = null;
|
|
110
|
+
checkpointRestored = null;
|
|
111
|
+
port = 0;
|
|
112
|
+
pluginTools = [];
|
|
113
|
+
pluginWatcher = null;
|
|
114
|
+
automationHooks = undefined;
|
|
115
|
+
recipeScheduler = null;
|
|
116
|
+
recipeRunLog = null;
|
|
117
|
+
commitIssueLinkLog = null;
|
|
118
|
+
decisionTraceLog = null;
|
|
119
|
+
/** Pre-computed digest of recent decisions, refreshed on each session connect. */
|
|
120
|
+
recentTracesDigest = [];
|
|
121
|
+
httpMcpHandler = null;
|
|
122
|
+
oauthServer = null;
|
|
123
|
+
/** Incremented each time the VS Code extension (re)connects — guards stale async callbacks. */
|
|
124
|
+
extensionConnectionGeneration = 0;
|
|
125
|
+
/** Tracks whether a debug session was active — detects true→false transition for onDebugSessionEnd. */
|
|
126
|
+
_lastDebugSessionActive = false;
|
|
127
|
+
/** Total number of VS Code extension disconnects since bridge start. */
|
|
128
|
+
extensionDisconnectCount = 0;
|
|
129
|
+
/** ISO timestamp of last getProjectContext cache write — drives status-bar "context X min ago". */
|
|
130
|
+
_lastContextCachedAt = null;
|
|
131
|
+
wsHeartbeatInterval = null;
|
|
132
|
+
constructor(config) {
|
|
133
|
+
this.config = config;
|
|
134
|
+
this.logger = new Logger(config.verbose, config.jsonl);
|
|
135
|
+
this.lockFile = new LockFileManager(this.logger);
|
|
136
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
137
|
+
this.authToken = config.fixedToken ?? loadOrCreateBridgeToken(configDir);
|
|
138
|
+
this.server = new Server(this.authToken, this.logger, config.corsOrigins);
|
|
139
|
+
if (config.issuerUrl) {
|
|
140
|
+
this.oauthServer = new OAuthServerImpl(this.authToken, config.issuerUrl, {
|
|
141
|
+
configDir,
|
|
142
|
+
tokenTtlMs: config.oauthTokenTtlMs,
|
|
143
|
+
});
|
|
144
|
+
this.server.setOAuthServer(this.oauthServer, config.issuerUrl);
|
|
145
|
+
this.logger.info(`OAuth 2.0 enabled — issuer: ${config.issuerUrl}`);
|
|
146
|
+
}
|
|
147
|
+
this.activityLog = new ActivityLog();
|
|
148
|
+
if (config.auditLogPath) {
|
|
149
|
+
this.activityLog.setPersistPath(config.auditLogPath);
|
|
150
|
+
this.logger.info(`Audit log: ${config.auditLogPath}`);
|
|
151
|
+
}
|
|
152
|
+
this.extensionClient = new ExtensionClient(this.logger);
|
|
153
|
+
// Handle new Claude Code connections
|
|
154
|
+
this.server.on("connection", async (ws) => {
|
|
155
|
+
// Reject connections before probes are ready
|
|
156
|
+
if (!this.ready) {
|
|
157
|
+
this.logger.warn("Connection rejected — bridge not ready yet");
|
|
158
|
+
ws.close(1013, "Bridge not ready");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// ── Session resumption ────────────────────────────────────────────────
|
|
162
|
+
// If the client sent X-Claude-Code-Session-Id and we have a matching
|
|
163
|
+
// session in the grace period, reattach the new WebSocket to it instead
|
|
164
|
+
// of creating a fresh session. This eliminates re-initialization overhead
|
|
165
|
+
// after brief disconnects (sleep/wake, network blip, bridge restart).
|
|
166
|
+
//
|
|
167
|
+
// Session resumption trust boundary: any client presenting a valid auth
|
|
168
|
+
// token and a matching session ID can reattach to a grace-period session.
|
|
169
|
+
// Session IDs are random UUIDs (128-bit), so guessing is infeasible. In
|
|
170
|
+
// single-user local deployments this is safe. In remote deployments using
|
|
171
|
+
// --fixed-token with multiple agents sharing one token, agents can inherit
|
|
172
|
+
// each other's session state (openedFiles, etc.) — this is intentional
|
|
173
|
+
// for the orchestrator pattern. For strict isolation between agents, use
|
|
174
|
+
// separate bridge instances with separate tokens.
|
|
175
|
+
const clientSessionId = ws
|
|
176
|
+
.clientSessionId;
|
|
177
|
+
if (clientSessionId) {
|
|
178
|
+
const existing = this.sessions.get(clientSessionId);
|
|
179
|
+
if (existing?.graceTimer) {
|
|
180
|
+
clearTimeout(existing.graceTimer);
|
|
181
|
+
existing.graceTimer = null;
|
|
182
|
+
existing.ws = ws;
|
|
183
|
+
existing.wsAlive = true;
|
|
184
|
+
existing.transport.attach(ws);
|
|
185
|
+
ws.on("pong", () => {
|
|
186
|
+
existing.wsAlive = true;
|
|
187
|
+
});
|
|
188
|
+
this.lastConnectAt = new Date().toISOString();
|
|
189
|
+
this.logger.info(`Session ${clientSessionId.slice(0, 8)} resumed — grace period cancelled`);
|
|
190
|
+
this.activityLog.recordEvent("session_resumed", {
|
|
191
|
+
sessionId: clientSessionId,
|
|
192
|
+
});
|
|
193
|
+
this.logger.event("session_resumed", { sessionId: clientSessionId });
|
|
194
|
+
// Re-attach close/error handlers to the new WebSocket
|
|
195
|
+
ws.on("close", (code, reason) => {
|
|
196
|
+
this.lastDisconnectAt = new Date().toISOString();
|
|
197
|
+
this.lastDisconnectCode = code;
|
|
198
|
+
this.lastDisconnectReason = reason.toString() || null;
|
|
199
|
+
this.logger.info(`Claude Code disconnected (session ${clientSessionId.slice(0, 8)}) code=${code} reason=${reason.toString() || "(none)"}`);
|
|
200
|
+
this.activityLog.recordEvent("claude_disconnected", {
|
|
201
|
+
sessionId: clientSessionId,
|
|
202
|
+
});
|
|
203
|
+
this.logger.event("claude_disconnected", {
|
|
204
|
+
sessionId: clientSessionId,
|
|
205
|
+
});
|
|
206
|
+
const s = this.sessions.get(clientSessionId);
|
|
207
|
+
if (s && !s.graceTimer) {
|
|
208
|
+
s.graceTimer = setTimeout(() => {
|
|
209
|
+
this.cleanupSession(clientSessionId);
|
|
210
|
+
}, this.config.gracePeriodMs);
|
|
211
|
+
this.activityLog.recordEvent("grace_started", {
|
|
212
|
+
sessionId: clientSessionId,
|
|
213
|
+
gracePeriodMs: this.config.gracePeriodMs,
|
|
214
|
+
});
|
|
215
|
+
this.logger.info(`Grace period started for session ${clientSessionId.slice(0, 8)} (${this.config.gracePeriodMs / 1000}s)`);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
ws.on("error", (err) => {
|
|
219
|
+
this.logger.error(`WebSocket error (session ${clientSessionId.slice(0, 8)}): ${err.message}`);
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
225
|
+
// Reject connections beyond capacity (grace-period sessions don't count)
|
|
226
|
+
const activeSessionCount = [...this.sessions.values()].filter((s) => !s.graceTimer).length;
|
|
227
|
+
if (activeSessionCount >= this.config.maxSessions) {
|
|
228
|
+
this.logger.warn(`Session capacity reached (${this.config.maxSessions} active). Rejecting connection.`);
|
|
229
|
+
ws.close(1013, "Bridge at capacity");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (this.sessions.size === 0) {
|
|
233
|
+
void this.maybeAutoSnapshotHandoff();
|
|
234
|
+
this._startPeriodicSnapshots();
|
|
235
|
+
}
|
|
236
|
+
const sessionId = randomUUID();
|
|
237
|
+
const transport = new McpTransport(this.logger);
|
|
238
|
+
transport.workspace = this.config.workspace;
|
|
239
|
+
transport.sessionId = sessionId;
|
|
240
|
+
transport.setActivityLog(this.activityLog);
|
|
241
|
+
transport.setToolRateLimit(this.config.toolRateLimit);
|
|
242
|
+
if (this.server.approvalGate !== "off") {
|
|
243
|
+
this.logger.info(`[patchwork] approval gate active: ${this.server.approvalGate} tier(s) require dashboard approval`);
|
|
244
|
+
transport.setApprovalGate(async ({ toolName, params, sessionId }) => {
|
|
245
|
+
const tier = classifyTool(toolName);
|
|
246
|
+
if (this.server.approvalGate === "off")
|
|
247
|
+
return "bypass";
|
|
248
|
+
if (this.server.approvalGate !== "all" && tier !== "high")
|
|
249
|
+
return "bypass";
|
|
250
|
+
const queue = getApprovalQueue();
|
|
251
|
+
const { promise } = queue.request({
|
|
252
|
+
toolName,
|
|
253
|
+
params,
|
|
254
|
+
tier,
|
|
255
|
+
sessionId: sessionId ?? undefined,
|
|
256
|
+
riskSignals: [],
|
|
257
|
+
});
|
|
258
|
+
return promise;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
transport.setExtensionConnectedFn(() => this.extensionClient.isConnected());
|
|
262
|
+
// Refresh trace digest before setting instructions. Underlying query
|
|
263
|
+
// is pure in-memory so this is microtask-cheap; awaiting ensures the
|
|
264
|
+
// session's first `initialize` response includes the fresh digest
|
|
265
|
+
// rather than the previous (empty on first connect) cache.
|
|
266
|
+
await this.refreshRecentTracesDigest();
|
|
267
|
+
transport.setInstructions(this.buildInstructions());
|
|
268
|
+
transport.onInitialized = () => {
|
|
269
|
+
if (this.pendingListChanged && ws.readyState === WebSocket.OPEN) {
|
|
270
|
+
McpTransport.sendNotification(ws, "notifications/tools/list_changed", undefined, this.logger);
|
|
271
|
+
// Delay clearing the flag so that if the socket errors immediately after
|
|
272
|
+
// the send (before the client processes the notification), a reconnecting
|
|
273
|
+
// session still receives the list_changed on its own onInitialized.
|
|
274
|
+
setTimeout(() => {
|
|
275
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
276
|
+
this.pendingListChanged = false;
|
|
277
|
+
}
|
|
278
|
+
}, 200);
|
|
279
|
+
}
|
|
280
|
+
if (this.checkpointRestored) {
|
|
281
|
+
const { fileCount, ageSec } = this.checkpointRestored;
|
|
282
|
+
this.checkpointRestored = null;
|
|
283
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
284
|
+
McpTransport.sendNotification(ws, "notifications/message", {
|
|
285
|
+
level: "info",
|
|
286
|
+
logger: "bridge",
|
|
287
|
+
data: `Session restored from checkpoint: ${fileCount} file(s) tracked (${ageSec}s ago).`,
|
|
288
|
+
}, this.logger);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
// Restore previously-tracked files into the first connecting session, then
|
|
293
|
+
// clear so subsequent sessions in the same run start with a clean slate.
|
|
294
|
+
let openedFiles;
|
|
295
|
+
if (this.sessions.size === 0 && this.restoredOpenedFiles !== null) {
|
|
296
|
+
const captured = this.restoredOpenedFiles;
|
|
297
|
+
this.restoredOpenedFiles = null;
|
|
298
|
+
openedFiles = new Set(captured);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
openedFiles = new Set();
|
|
302
|
+
}
|
|
303
|
+
const session = {
|
|
304
|
+
id: sessionId,
|
|
305
|
+
ws,
|
|
306
|
+
transport,
|
|
307
|
+
openedFiles,
|
|
308
|
+
terminalPrefix: `s${sessionId.slice(0, 8)}-`,
|
|
309
|
+
graceTimer: null,
|
|
310
|
+
connectedAt: Date.now(),
|
|
311
|
+
wsAlive: true,
|
|
312
|
+
};
|
|
313
|
+
ws.on("pong", () => {
|
|
314
|
+
session.wsAlive = true;
|
|
315
|
+
});
|
|
316
|
+
// Register tools for this session — probes guaranteed non-null due to ready guard above
|
|
317
|
+
const probes = this.probes ?? {};
|
|
318
|
+
// addTransport BEFORE registerAllTools so that if a reload fires between
|
|
319
|
+
// these two calls, the new transport is already tracked by the watcher.
|
|
320
|
+
this.pluginWatcher?.addTransport(transport);
|
|
321
|
+
const pluginTools = this.pluginWatcher?.getTools() ?? this.pluginTools;
|
|
322
|
+
registerAllTools(transport, this.config, session.openedFiles, probes, this.extensionClient, this.activityLog, session.terminalPrefix, this.fileLock, this.sessions, this.orchestrator, sessionId, pluginTools, this.automationHooks, () => ({
|
|
323
|
+
at: this.lastDisconnectAt,
|
|
324
|
+
code: this.lastDisconnectCode,
|
|
325
|
+
reason: this.lastDisconnectReason,
|
|
326
|
+
}), (generatedAt) => {
|
|
327
|
+
this._lastContextCachedAt = generatedAt;
|
|
328
|
+
this._emitLiveState();
|
|
329
|
+
}, () => this.extensionDisconnectCount, this.commitIssueLinkLog ?? undefined, this.recipeRunLog ?? undefined, this.decisionTraceLog ?? undefined);
|
|
330
|
+
transport.attach(ws);
|
|
331
|
+
this.sessions.set(sessionId, session);
|
|
332
|
+
this.logger.info(`Claude Code connected (session ${sessionId.slice(0, 8)}) — ${this.sessions.size} active session${this.sessions.size === 1 ? "" : "s"}`);
|
|
333
|
+
this.lastConnectAt = new Date().toISOString();
|
|
334
|
+
this.activityLog.recordEvent("claude_connected", {
|
|
335
|
+
sessionId,
|
|
336
|
+
activeSessions: this.sessions.size,
|
|
337
|
+
});
|
|
338
|
+
this.logger.event("claude_connected", {
|
|
339
|
+
sessionId,
|
|
340
|
+
activeSessions: this.sessions.size,
|
|
341
|
+
});
|
|
342
|
+
// Only notify on the first active session — subsequent agents don't need to re-signal
|
|
343
|
+
if (this.sessions.size === 1) {
|
|
344
|
+
this.extensionClient.notifyClaudeConnectionState(true);
|
|
345
|
+
}
|
|
346
|
+
ws.on("close", (code, reason) => {
|
|
347
|
+
this.lastDisconnectAt = new Date().toISOString();
|
|
348
|
+
this.lastDisconnectCode = code;
|
|
349
|
+
this.lastDisconnectReason = reason.toString() || null;
|
|
350
|
+
this.logger.info(`Claude Code disconnected (session ${sessionId.slice(0, 8)}) code=${code} reason=${reason.toString() || "(none)"}`);
|
|
351
|
+
this.activityLog.recordEvent("claude_disconnected", {
|
|
352
|
+
sessionId,
|
|
353
|
+
});
|
|
354
|
+
this.logger.event("claude_disconnected", { sessionId });
|
|
355
|
+
const s = this.sessions.get(sessionId);
|
|
356
|
+
if (s && !s.graceTimer) {
|
|
357
|
+
s.graceTimer = setTimeout(() => {
|
|
358
|
+
this.cleanupSession(sessionId);
|
|
359
|
+
}, this.config.gracePeriodMs);
|
|
360
|
+
this.activityLog.recordEvent("grace_started", {
|
|
361
|
+
sessionId,
|
|
362
|
+
gracePeriodMs: this.config.gracePeriodMs,
|
|
363
|
+
});
|
|
364
|
+
this.logger.info(`Grace period started for session ${sessionId.slice(0, 8)} (${this.config.gracePeriodMs / 1000}s)`);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
ws.on("error", (err) => {
|
|
368
|
+
// Log the error; the "close" event always follows an "error" in the ws
|
|
369
|
+
// library and is the single authoritative place to start the grace timer.
|
|
370
|
+
// Starting it here too creates a dual-handler race where both handlers
|
|
371
|
+
// could observe !s.graceTimer before either setTimeout returns its handle.
|
|
372
|
+
this.logger.error(`WebSocket error (session ${sessionId.slice(0, 8)}): ${err.message}`);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
// (sendListChanged is a private method — see below)
|
|
376
|
+
// Handle VS Code extension connections — notify Claude immediately (no debounce)
|
|
377
|
+
// so it re-queries capabilities and discovers LSP/terminal/selection are now available.
|
|
378
|
+
this.server.on("extension", (ws) => {
|
|
379
|
+
this.logger.info("VS Code extension connected — LSP, terminal, and editor tools now available");
|
|
380
|
+
this.activityLog.recordEvent("extension_connected");
|
|
381
|
+
this.logger.event("extension_connected");
|
|
382
|
+
this.extensionClient.handleExtensionConnection(ws);
|
|
383
|
+
// Push current live state to newly connected extension
|
|
384
|
+
this._emitLiveState();
|
|
385
|
+
// Immediate list_changed — tell Claude Code that extension tools are now available
|
|
386
|
+
for (const session of this.sessions.values()) {
|
|
387
|
+
if (session.ws.readyState === WebSocket.OPEN) {
|
|
388
|
+
McpTransport.sendNotification(session.ws, "notifications/tools/list_changed", undefined, this.logger);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Refresh workspace folders from extension (multi-root workspace support).
|
|
392
|
+
// Only broadcast a second list_changed if the folders actually changed,
|
|
393
|
+
// to avoid a spurious double-notification on every extension connect.
|
|
394
|
+
//
|
|
395
|
+
// Generation guard: if the extension disconnects and reconnects before
|
|
396
|
+
// getWorkspaceFolders() resolves, the stale response is discarded so it
|
|
397
|
+
// cannot overwrite the config populated by the newer connection.
|
|
398
|
+
const myGen = ++this.extensionConnectionGeneration;
|
|
399
|
+
const prevFolders = (this.config.workspaceFolders ?? []).join(",");
|
|
400
|
+
this.extensionClient
|
|
401
|
+
.getWorkspaceFolders()
|
|
402
|
+
.then((folders) => {
|
|
403
|
+
if (myGen !== this.extensionConnectionGeneration)
|
|
404
|
+
return;
|
|
405
|
+
if (folders && folders.length > 0) {
|
|
406
|
+
this.config.workspaceFolders = folders.map((f) => f.path);
|
|
407
|
+
this.logger.info(`Workspace folders: ${this.config.workspaceFolders.join(", ")}`);
|
|
408
|
+
// Only re-broadcast if the folder list changed
|
|
409
|
+
if (this.config.workspaceFolders.join(",") !== prevFolders) {
|
|
410
|
+
for (const session of this.sessions.values()) {
|
|
411
|
+
if (session.ws.readyState === WebSocket.OPEN) {
|
|
412
|
+
McpTransport.sendNotification(session.ws, "notifications/tools/list_changed", undefined, this.logger);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
.catch((err) => {
|
|
419
|
+
this.logger.warn(`getWorkspaceFolders failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
// Notify Claude when extension disconnects so it knows to fall back to CLI/grep mode
|
|
423
|
+
this.extensionClient.onExtensionDisconnected = () => {
|
|
424
|
+
this.logger.info("VS Code extension disconnected — falling back to file-system tools only");
|
|
425
|
+
this.extensionDisconnectCount++;
|
|
426
|
+
this.activityLog.recordEvent("extension_disconnected");
|
|
427
|
+
this.logger.event("extension_disconnected_notify");
|
|
428
|
+
this.sendListChanged();
|
|
429
|
+
};
|
|
430
|
+
// Forward diagnostics changes from extension to Claude Code
|
|
431
|
+
this.extensionClient.onDiagnosticsChanged = (_file, _diagnostics) => {
|
|
432
|
+
this.logger.event("diagnostics_changed", { file: _file });
|
|
433
|
+
this.sendListChanged();
|
|
434
|
+
this.automationHooks?.handleDiagnosticsChanged(_file, _diagnostics ?? []);
|
|
435
|
+
};
|
|
436
|
+
// Forward AI comment changes from extension to Claude Code
|
|
437
|
+
this.extensionClient.onAICommentsChanged = (_comments) => {
|
|
438
|
+
this.logger.event("ai_comments_changed", { count: _comments.length });
|
|
439
|
+
this.sendListChanged();
|
|
440
|
+
};
|
|
441
|
+
// Forward file change notifications from extension to Claude Code
|
|
442
|
+
this.extensionClient.onFileChanged = (id, type, file) => {
|
|
443
|
+
this.logger.event("file_changed", { id, type, file });
|
|
444
|
+
this.sendListChanged();
|
|
445
|
+
this.automationHooks?.handleFileSaved(id, type, file);
|
|
446
|
+
this.automationHooks?.handleFileChanged(id, type, file);
|
|
447
|
+
};
|
|
448
|
+
// Forward debug session changes from extension to Claude Code
|
|
449
|
+
this.extensionClient.onDebugSessionChanged = (state) => {
|
|
450
|
+
this.logger.event("debug_session_changed");
|
|
451
|
+
this.sendListChanged();
|
|
452
|
+
// Detect false→true transition (session started)
|
|
453
|
+
if (!this._lastDebugSessionActive && state.hasActiveSession) {
|
|
454
|
+
const breakpoints = state.breakpoints ?? [];
|
|
455
|
+
this.automationHooks?.handleDebugSessionStart({
|
|
456
|
+
sessionName: state.sessionName ?? "unknown",
|
|
457
|
+
sessionType: state.sessionType ?? "unknown",
|
|
458
|
+
breakpointCount: breakpoints.filter((b) => b.enabled).length,
|
|
459
|
+
activeFile: breakpoints[0]?.file ?? "",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// Detect true→false transition (session ended)
|
|
463
|
+
if (this._lastDebugSessionActive && !state.hasActiveSession) {
|
|
464
|
+
this.automationHooks?.handleDebugSessionEnd({
|
|
465
|
+
sessionName: state.sessionName ?? "unknown",
|
|
466
|
+
sessionType: state.sessionType ?? "unknown",
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
this._lastDebugSessionActive = state.hasActiveSession;
|
|
470
|
+
this._emitLiveState();
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/** Push live bridge state to the extension for status-bar display. */
|
|
474
|
+
_emitLiveState() {
|
|
475
|
+
const preCompactArmed = this.automationHooks?.isPreCompactEnabled() ?? false;
|
|
476
|
+
this.extensionClient.sendPush("extension/bridgeLiveState", {
|
|
477
|
+
contextCachedAt: this._lastContextCachedAt,
|
|
478
|
+
preCompactArmed,
|
|
479
|
+
debugSessionActive: this._lastDebugSessionActive,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
/** Build a rich auto-snapshot string from live bridge state. */
|
|
483
|
+
_buildSnapshotSummary() {
|
|
484
|
+
const ts = new Date().toISOString();
|
|
485
|
+
const extConnected = this.extensionClient.isConnected();
|
|
486
|
+
const lines = [`[auto-snapshot ${ts}]`];
|
|
487
|
+
lines.push(`Workspace: ${this.config.workspace}`);
|
|
488
|
+
lines.push(`Extension: ${extConnected ? "connected" : "disconnected"}`);
|
|
489
|
+
lines.push(`Active sessions: ${this.sessions.size}`);
|
|
490
|
+
// Diagnostics summary from the extension client's live cache
|
|
491
|
+
if (extConnected) {
|
|
492
|
+
let errors = 0;
|
|
493
|
+
let warnings = 0;
|
|
494
|
+
const errorFiles = [];
|
|
495
|
+
for (const [file, diags] of this.extensionClient.latestDiagnostics) {
|
|
496
|
+
const e = diags.filter((d) => d.severity === "error").length;
|
|
497
|
+
const w = diags.filter((d) => d.severity === "warning").length;
|
|
498
|
+
errors += e;
|
|
499
|
+
warnings += w;
|
|
500
|
+
if (e > 0)
|
|
501
|
+
errorFiles.push(file.split("/").pop() ?? file);
|
|
502
|
+
}
|
|
503
|
+
lines.push(`Diagnostics: ${errors} errors, ${warnings} warnings`);
|
|
504
|
+
if (errorFiles.length > 0) {
|
|
505
|
+
lines.push(`Error files: ${errorFiles.slice(0, 5).join(", ")}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Top 3 most-called tools from activity log
|
|
509
|
+
const statsMap = this.activityLog.stats();
|
|
510
|
+
const topTools = Object.entries(statsMap)
|
|
511
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
512
|
+
.slice(0, 3)
|
|
513
|
+
.map(([name, s]) => `${name}(${s.count})`)
|
|
514
|
+
.join(", ");
|
|
515
|
+
if (topTools)
|
|
516
|
+
lines.push(`Top tools: ${topTools}`);
|
|
517
|
+
return lines.join("\n");
|
|
518
|
+
}
|
|
519
|
+
/** Write an auto-snapshot handoff note when a new first session connects, unless one was recently written. */
|
|
520
|
+
_autoSnapshotInFlight = false;
|
|
521
|
+
async maybeAutoSnapshotHandoff() {
|
|
522
|
+
// Guard against concurrent calls (e.g. two clients connecting simultaneously)
|
|
523
|
+
if (this._autoSnapshotInFlight)
|
|
524
|
+
return;
|
|
525
|
+
this._autoSnapshotInFlight = true;
|
|
526
|
+
try {
|
|
527
|
+
const existing = await readNote(this.config.workspace);
|
|
528
|
+
if (existing && Date.now() - existing.updatedAt < 5 * 60_000) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
await writeNote(this._buildSnapshotSummary(), this.config.workspace, undefined, true);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// best-effort — never let this crash the connection handler
|
|
535
|
+
}
|
|
536
|
+
finally {
|
|
537
|
+
this._autoSnapshotInFlight = false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/** Write a periodic auto-snapshot while sessions are active (every 5 minutes). */
|
|
541
|
+
_periodicSnapshotTimer = null;
|
|
542
|
+
_startPeriodicSnapshots() {
|
|
543
|
+
if (this._periodicSnapshotTimer)
|
|
544
|
+
return;
|
|
545
|
+
this._periodicSnapshotTimer = setInterval(() => {
|
|
546
|
+
if (this.sessions.size === 0)
|
|
547
|
+
return; // no active sessions — skip
|
|
548
|
+
void writeNote(this._buildSnapshotSummary(), this.config.workspace, undefined, true).catch(() => {
|
|
549
|
+
/* best-effort */
|
|
550
|
+
});
|
|
551
|
+
}, 5 * 60_000);
|
|
552
|
+
this._periodicSnapshotTimer.unref(); // don't prevent Node exit
|
|
553
|
+
}
|
|
554
|
+
_stopPeriodicSnapshots() {
|
|
555
|
+
if (this._periodicSnapshotTimer) {
|
|
556
|
+
clearInterval(this._periodicSnapshotTimer);
|
|
557
|
+
this._periodicSnapshotTimer = null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/** Debounced tools/list_changed notification — max one per 2 seconds. */
|
|
561
|
+
sendListChanged() {
|
|
562
|
+
if (this.stopped)
|
|
563
|
+
return; // Don't fire after stop()
|
|
564
|
+
if (this.listChangedTimer)
|
|
565
|
+
return; // Already scheduled
|
|
566
|
+
this.listChangedTimer = setTimeout(() => {
|
|
567
|
+
this.listChangedTimer = null;
|
|
568
|
+
let notifiedAny = false;
|
|
569
|
+
for (const session of this.sessions.values()) {
|
|
570
|
+
if (session.ws.readyState === WebSocket.OPEN) {
|
|
571
|
+
McpTransport.sendNotification(session.ws, "notifications/tools/list_changed", undefined, this.logger);
|
|
572
|
+
notifiedAny = true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Also notify HTTP sessions — they have their own session map in the handler.
|
|
576
|
+
this.httpMcpHandler?.broadcastListChanged();
|
|
577
|
+
// No open WebSocket sessions — mark pending so the next WS session gets it on initialize.
|
|
578
|
+
// HTTP sessions always receive their tools via broadcastListChanged above.
|
|
579
|
+
if (!notifiedAny) {
|
|
580
|
+
this.pendingListChanged = true;
|
|
581
|
+
}
|
|
582
|
+
}, 2000);
|
|
583
|
+
}
|
|
584
|
+
_buildCheckpoint(port) {
|
|
585
|
+
const sessions = [];
|
|
586
|
+
for (const s of this.sessions.values()) {
|
|
587
|
+
sessions.push({
|
|
588
|
+
id: s.id.slice(0, 8),
|
|
589
|
+
connectedAt: s.connectedAt,
|
|
590
|
+
openedFiles: [...s.openedFiles],
|
|
591
|
+
terminalPrefix: s.terminalPrefix,
|
|
592
|
+
inGrace: s.graceTimer !== null,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
port,
|
|
597
|
+
savedAt: Date.now(),
|
|
598
|
+
sessions,
|
|
599
|
+
extensionConnected: this.extensionClient.isConnected(),
|
|
600
|
+
gracePeriodMs: this.config.gracePeriodMs,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
buildInstructions() {
|
|
604
|
+
const lines = [`claude-ide-bridge v${PACKAGE_VERSION}`];
|
|
605
|
+
lines.push("");
|
|
606
|
+
if (this.recentTracesDigest.length > 0) {
|
|
607
|
+
lines.push(...this.recentTracesDigest);
|
|
608
|
+
lines.push("");
|
|
609
|
+
}
|
|
610
|
+
lines.push("CONTEXT PLATFORM:");
|
|
611
|
+
lines.push(" Use ctx tools for issue/PR/error context — not gh or githubViewPR.");
|
|
612
|
+
lines.push(" ctxGetTaskContext(ref) — unified context for any issue, PR, commit, or error");
|
|
613
|
+
lines.push(" ctxQueryTraces(query) — search past decisions");
|
|
614
|
+
lines.push(" ctxSaveTrace(ref, problem, solution) — record fix after resolving a task");
|
|
615
|
+
lines.push("");
|
|
616
|
+
lines.push(...buildEnforcementReminder());
|
|
617
|
+
return lines.join("\n");
|
|
618
|
+
}
|
|
619
|
+
async refreshRecentTracesDigest() {
|
|
620
|
+
try {
|
|
621
|
+
this.recentTracesDigest = await buildRecentTracesDigest({
|
|
622
|
+
activityLog: this.activityLog,
|
|
623
|
+
commitIssueLinkLog: this.commitIssueLinkLog,
|
|
624
|
+
recipeRunLog: this.recipeRunLog,
|
|
625
|
+
decisionTraceLog: this.decisionTraceLog,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
this.logger.warn?.(`[traces-digest] refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
cleanupSession(id) {
|
|
633
|
+
const session = this.sessions.get(id);
|
|
634
|
+
if (!session)
|
|
635
|
+
return;
|
|
636
|
+
if (session.graceTimer) {
|
|
637
|
+
clearTimeout(session.graceTimer);
|
|
638
|
+
this.activityLog.recordEvent("grace_expired", {
|
|
639
|
+
sessionId: id,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
// Read stats before detach() — counters survive detach
|
|
643
|
+
const { callCount, errorCount } = session.transport.getStats();
|
|
644
|
+
const durationMs = Date.now() - session.connectedAt;
|
|
645
|
+
this.pluginWatcher?.removeTransport(session.transport);
|
|
646
|
+
session.transport.detach();
|
|
647
|
+
session.openedFiles.clear();
|
|
648
|
+
this.sessions.delete(id);
|
|
649
|
+
const errorPart = errorCount > 0
|
|
650
|
+
? ` (${errorCount} error${errorCount === 1 ? "" : "s"})`
|
|
651
|
+
: "";
|
|
652
|
+
this.logger.info(`Session ${id.slice(0, 8)} done — ${callCount} tool call${callCount === 1 ? "" : "s"}${errorPart}, ${formatDuration(durationMs)}`);
|
|
653
|
+
if (this.sessions.size === 0) {
|
|
654
|
+
this.logger.info("Bridge idle — waiting for next connection");
|
|
655
|
+
// Only notify for normal disconnects; stop() sends its own aggregate notification
|
|
656
|
+
if (!this.stopped) {
|
|
657
|
+
this.extensionClient.notifyClaudeConnectionState(false, {
|
|
658
|
+
callCount,
|
|
659
|
+
errorCount,
|
|
660
|
+
durationMs,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
this.logger.info(`Active sessions: ${this.sessions.size}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** Returns the port the bridge is listening on (0 before start()). */
|
|
669
|
+
getPort() {
|
|
670
|
+
return this.port;
|
|
671
|
+
}
|
|
672
|
+
/** Returns the auth token for this bridge instance. */
|
|
673
|
+
getAuthToken() {
|
|
674
|
+
return this.authToken;
|
|
675
|
+
}
|
|
676
|
+
async start() {
|
|
677
|
+
// 0. Initialize OpenTelemetry (no-op when OTEL_EXPORTER_OTLP_ENDPOINT is unset)
|
|
678
|
+
initTelemetry();
|
|
679
|
+
// 0a. Auto-repair .claude/rules/bridge-tools.md if stale (present but missing the
|
|
680
|
+
// current version sentinel). Older package versions write stale files that may
|
|
681
|
+
// lack new tool substitution rules. Repair is silent on success; falls back to
|
|
682
|
+
// warn-only if the template is missing or the write fails.
|
|
683
|
+
repairBridgeToolsRulesIfStale(this.config.workspace, (msg) => this.logger.info(msg));
|
|
684
|
+
// 1. Probe available CLI tools (pass workspace so local node_modules/.bin is checked)
|
|
685
|
+
this.probes = await probeAll(this.config.workspace);
|
|
686
|
+
this.ready = true;
|
|
687
|
+
// 2. Load plugins (after probes, before accepting sessions)
|
|
688
|
+
this.pluginTools = await loadPlugins(this.config.plugins, this.config, this.logger);
|
|
689
|
+
if (this.config.pluginWatch && this.config.plugins.length > 0) {
|
|
690
|
+
const loadedPlugins = await loadPluginsFull(this.config.plugins, this.config, this.logger);
|
|
691
|
+
this.pluginTools = loadedPlugins.flatMap((p) => p.tools);
|
|
692
|
+
this.pluginWatcher = new PluginWatcher(this.config, this.logger, () => this.sendListChanged());
|
|
693
|
+
this.pluginWatcher.start(loadedPlugins);
|
|
694
|
+
this.logger.info(`[plugin-watch] Watching ${loadedPlugins.length} plugin director${loadedPlugins.length === 1 ? "y" : "ies"}`);
|
|
695
|
+
}
|
|
696
|
+
const probes = this.probes;
|
|
697
|
+
const probeList = (keys) => Object.entries(probes)
|
|
698
|
+
.filter(([k, v]) => v && (!keys || keys.includes(k)))
|
|
699
|
+
.map(([k]) => k)
|
|
700
|
+
.join(", ") || "none";
|
|
701
|
+
this.logger.info(`Probed tools: ${probeList()}`);
|
|
702
|
+
this.logger.info(`Available linters: ${probeList(["tsc", "eslint", "pyright", "ruff", "cargo", "go", "biome"])}`);
|
|
703
|
+
this.logger.info(`Available test runners: ${probeList(["vitest", "jest", "pytest", "cargo", "go"])}`);
|
|
704
|
+
// 2. Initialize Claude driver and orchestrator (if configured)
|
|
705
|
+
if (this.config.claudeDriver !== "none") {
|
|
706
|
+
const driver = createDriver(this.config.claudeDriver, this.config.claudeBinary, this.config.antBinary, (msg) => this.logger.info(msg));
|
|
707
|
+
// Patchwork: enrichment link log is useful regardless of orchestrator.
|
|
708
|
+
const patchworkDir = path.join(os.homedir(), ".patchwork");
|
|
709
|
+
this.commitIssueLinkLog = new CommitIssueLinkLog({
|
|
710
|
+
dir: patchworkDir,
|
|
711
|
+
logger: this.logger,
|
|
712
|
+
});
|
|
713
|
+
this.decisionTraceLog = new DecisionTraceLog({
|
|
714
|
+
dir: patchworkDir,
|
|
715
|
+
logger: this.logger,
|
|
716
|
+
});
|
|
717
|
+
if (driver) {
|
|
718
|
+
// Recipe run-history needs the orchestrator to produce anything.
|
|
719
|
+
this.recipeRunLog = new RecipeRunLog({
|
|
720
|
+
dir: patchworkDir,
|
|
721
|
+
logger: this.logger,
|
|
722
|
+
});
|
|
723
|
+
this.orchestrator = new ClaudeOrchestrator(driver, this.config.workspace, (msg) => this.logger.info(msg), (taskId, chunk) => this.extensionClient.notifyTaskOutput(taskId, chunk), (taskId, status) => {
|
|
724
|
+
this.extensionClient.notifyTaskDone(taskId, status);
|
|
725
|
+
const task = this.orchestrator?.getTask(taskId);
|
|
726
|
+
if (task && this.recipeRunLog) {
|
|
727
|
+
this.recipeRunLog.record({
|
|
728
|
+
id: task.id,
|
|
729
|
+
triggerSource: task.triggerSource,
|
|
730
|
+
status: task.status,
|
|
731
|
+
createdAt: task.createdAt,
|
|
732
|
+
...(task.startedAt !== undefined && {
|
|
733
|
+
startedAt: task.startedAt,
|
|
734
|
+
}),
|
|
735
|
+
...(task.doneAt !== undefined && { doneAt: task.doneAt }),
|
|
736
|
+
...(task.model !== undefined && { model: task.model }),
|
|
737
|
+
...(task.output !== undefined && { output: task.output }),
|
|
738
|
+
...(task.errorMessage !== undefined && {
|
|
739
|
+
errorMessage: task.errorMessage,
|
|
740
|
+
}),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (status === "done" && this.automationHooks) {
|
|
744
|
+
// Loop guard: skip automation-spawned tasks to prevent infinite chains
|
|
745
|
+
if (!task?.isAutomationTask) {
|
|
746
|
+
this.automationHooks.handleTaskSuccess({
|
|
747
|
+
taskId,
|
|
748
|
+
output: task?.output ?? "",
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}, {
|
|
753
|
+
save: () => {
|
|
754
|
+
if (this.checkpoint) {
|
|
755
|
+
this.checkpoint.write(this._buildCheckpoint(this.port));
|
|
756
|
+
}
|
|
757
|
+
// Persist terminal tasks for cross-session resumability (best-effort)
|
|
758
|
+
if (this.port > 0 && this.orchestrator) {
|
|
759
|
+
void this.orchestrator.persistTasks(this.port).catch(() => {
|
|
760
|
+
/* best-effort */
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
this.logger.info(`[bridge] Claude driver: ${driver.name}`);
|
|
766
|
+
// Patchwork: start cron-trigger scheduler once the orchestrator exists.
|
|
767
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
768
|
+
this.recipeScheduler = new RecipeScheduler({
|
|
769
|
+
recipesDir,
|
|
770
|
+
enqueue: (opts) => this.orchestrator.enqueue(opts),
|
|
771
|
+
logger: this.logger,
|
|
772
|
+
});
|
|
773
|
+
const scheduled = this.recipeScheduler.start();
|
|
774
|
+
if (scheduled.length > 0) {
|
|
775
|
+
this.logger.info(`[patchwork] scheduled ${scheduled.length} cron recipe${scheduled.length === 1 ? "" : "s"}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (this.config.automationEnabled) {
|
|
780
|
+
if (!this.orchestrator) {
|
|
781
|
+
throw new Error("--automation requires --claude-driver != none");
|
|
782
|
+
}
|
|
783
|
+
if (!this.config.automationPolicyPath) {
|
|
784
|
+
throw new Error("--automation requires --automation-policy <path>");
|
|
785
|
+
}
|
|
786
|
+
const policy = loadPolicy(this.config.automationPolicyPath);
|
|
787
|
+
this.automationHooks = new AutomationHooks(policy, this.orchestrator, (msg) => this.logger.info(msg), this.extensionClient, this.config.workspace);
|
|
788
|
+
this.logger.info(`[bridge] Automation enabled (policy: ${this.config.automationPolicyPath})`);
|
|
789
|
+
}
|
|
790
|
+
// 3. Wire up /health endpoint data and /metrics
|
|
791
|
+
this.server.healthDataFn = () => {
|
|
792
|
+
let sessionsInGrace = 0;
|
|
793
|
+
for (const s of this.sessions.values()) {
|
|
794
|
+
if (s.graceTimer)
|
|
795
|
+
sessionsInGrace++;
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
claudeCode: this.sessions.size > 0,
|
|
799
|
+
activeSessions: this.sessions.size,
|
|
800
|
+
sessionsInGrace,
|
|
801
|
+
gracePeriodMs: this.config.gracePeriodMs,
|
|
802
|
+
lastConnectAt: this.lastConnectAt,
|
|
803
|
+
lastDisconnectAt: this.lastDisconnectAt,
|
|
804
|
+
lastDisconnectCode: this.lastDisconnectCode,
|
|
805
|
+
lastDisconnectReason: this.lastDisconnectReason,
|
|
806
|
+
extensionConnected: this.extensionClient.isConnected(),
|
|
807
|
+
extensionCircuitBreaker: this.extensionClient.getCircuitBreakerState(),
|
|
808
|
+
extensionDisconnectCount: this.extensionDisconnectCount,
|
|
809
|
+
recentActivity: this.activityLog.query({ last: 10 }),
|
|
810
|
+
};
|
|
811
|
+
};
|
|
812
|
+
this.server.metricsFn = () => this.activityLog.toPrometheus({
|
|
813
|
+
rateLimitRejected: this.activityLog.getRateLimitRejections(),
|
|
814
|
+
extensionDisconnects: this.extensionDisconnectCount,
|
|
815
|
+
});
|
|
816
|
+
this.server.perfDataFn = () => {
|
|
817
|
+
const windowMs = 60 * 60_000; // 1h window for dashboard
|
|
818
|
+
const windowedS = this.activityLog.windowedStats(windowMs);
|
|
819
|
+
const allPercentiles = this.activityLog.percentiles();
|
|
820
|
+
let totalCalls = 0;
|
|
821
|
+
let totalErrors = 0;
|
|
822
|
+
for (const s of Object.values(windowedS)) {
|
|
823
|
+
totalCalls += s.count;
|
|
824
|
+
totalErrors += s.errors;
|
|
825
|
+
}
|
|
826
|
+
const p95Values = Object.values(allPercentiles).map((p) => p.p95);
|
|
827
|
+
const overallP95Ms = p95Values.length > 0 ? Math.max(...p95Values) : 0;
|
|
828
|
+
const perTool = {};
|
|
829
|
+
for (const [tool, pct] of Object.entries(allPercentiles)) {
|
|
830
|
+
const ws = windowedS[tool];
|
|
831
|
+
perTool[tool] = {
|
|
832
|
+
p50: pct.p50,
|
|
833
|
+
p95: pct.p95,
|
|
834
|
+
p99: pct.p99,
|
|
835
|
+
sampleCount: pct.sampleCount,
|
|
836
|
+
calls: ws?.count ?? 0,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const cb = this.extensionClient.getCircuitBreakerState();
|
|
840
|
+
const errorRatePct = totalCalls > 0
|
|
841
|
+
? Math.round((totalErrors / totalCalls) * 10000) / 100
|
|
842
|
+
: 0;
|
|
843
|
+
let score = 100;
|
|
844
|
+
const signals = [];
|
|
845
|
+
if (cb.suspended) {
|
|
846
|
+
score -= 20;
|
|
847
|
+
signals.push("Circuit breaker suspended");
|
|
848
|
+
}
|
|
849
|
+
if (errorRatePct > 5) {
|
|
850
|
+
score -= 15;
|
|
851
|
+
signals.push(`Error rate critical (${errorRatePct}%)`);
|
|
852
|
+
}
|
|
853
|
+
else if (errorRatePct > 1) {
|
|
854
|
+
score -= 10;
|
|
855
|
+
signals.push(`Error rate elevated (${errorRatePct}%)`);
|
|
856
|
+
}
|
|
857
|
+
if (overallP95Ms > 2000) {
|
|
858
|
+
score -= 10;
|
|
859
|
+
signals.push(`p95 latency critical (${overallP95Ms}ms)`);
|
|
860
|
+
}
|
|
861
|
+
else if (overallP95Ms > 500) {
|
|
862
|
+
score -= 5;
|
|
863
|
+
signals.push(`p95 latency elevated (${overallP95Ms}ms)`);
|
|
864
|
+
}
|
|
865
|
+
const rl = this.activityLog.getRateLimitRejections();
|
|
866
|
+
if (rl > 0) {
|
|
867
|
+
score -= 10;
|
|
868
|
+
signals.push(`${rl} rate-limit rejection(s)`);
|
|
869
|
+
}
|
|
870
|
+
if (!this.extensionClient.isConnected())
|
|
871
|
+
signals.push("Extension disconnected");
|
|
872
|
+
score = Math.max(0, Math.min(100, score));
|
|
873
|
+
return {
|
|
874
|
+
latency: { perTool, overallP95Ms },
|
|
875
|
+
health: { score, signals },
|
|
876
|
+
};
|
|
877
|
+
};
|
|
878
|
+
this.server.activityFn = (last) => {
|
|
879
|
+
return this.activityLog.queryTimeline({ last });
|
|
880
|
+
};
|
|
881
|
+
this.server.approvalDetailFn = (callId) => {
|
|
882
|
+
const queue = getApprovalQueue();
|
|
883
|
+
const pending = queue.list().find((p) => p.callId === callId) ?? null;
|
|
884
|
+
const { decision, nearby } = this.activityLog.findApprovalByCallId(callId);
|
|
885
|
+
return {
|
|
886
|
+
pending: pending,
|
|
887
|
+
decision: decision,
|
|
888
|
+
nearby: nearby,
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
this.server.tracesFn = async (query) => {
|
|
892
|
+
const tool = createCtxQueryTracesTool({
|
|
893
|
+
activityLog: this.activityLog,
|
|
894
|
+
commitIssueLinkLog: this.commitIssueLinkLog,
|
|
895
|
+
recipeRunLog: this.recipeRunLog,
|
|
896
|
+
decisionTraceLog: this.decisionTraceLog,
|
|
897
|
+
});
|
|
898
|
+
const result = await tool.handler({
|
|
899
|
+
...(query.traceType && { traceType: query.traceType }),
|
|
900
|
+
...(query.key && { key: query.key }),
|
|
901
|
+
...(query.q && { q: query.q }),
|
|
902
|
+
...(query.tag && { tag: query.tag }),
|
|
903
|
+
...(query.since !== undefined && { since: query.since }),
|
|
904
|
+
...(query.limit !== undefined && { limit: query.limit }),
|
|
905
|
+
});
|
|
906
|
+
const structured = result
|
|
907
|
+
.structuredContent;
|
|
908
|
+
if (structured && typeof structured === "object") {
|
|
909
|
+
return structured;
|
|
910
|
+
}
|
|
911
|
+
const text = result.content[0]?.text;
|
|
912
|
+
if (typeof text === "string") {
|
|
913
|
+
try {
|
|
914
|
+
return JSON.parse(text);
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
return { traces: [], count: 0 };
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return { traces: [], count: 0 };
|
|
921
|
+
};
|
|
922
|
+
this.server.analyticsFn = async (windowHours) => {
|
|
923
|
+
const wh = typeof windowHours === "number" && windowHours >= 1 ? windowHours : 24;
|
|
924
|
+
const cutoff = Date.now() - wh * 3_600 * 1_000;
|
|
925
|
+
const statsMap = this.activityLog.stats();
|
|
926
|
+
const topTools = Object.entries(statsMap)
|
|
927
|
+
.map(([tool, s]) => ({
|
|
928
|
+
tool,
|
|
929
|
+
calls: s.count,
|
|
930
|
+
errors: s.errors,
|
|
931
|
+
avgMs: s.avgDurationMs,
|
|
932
|
+
}))
|
|
933
|
+
.sort((a, b) => b.calls - a.calls)
|
|
934
|
+
.slice(0, 10);
|
|
935
|
+
// Count automation tasks (isAutomationTask) created within the window.
|
|
936
|
+
// These originate from automation hooks (onFileSave, onGitCommit, etc.)
|
|
937
|
+
// and accurately represent "hooks fired" rather than session lifecycle events.
|
|
938
|
+
const hooksLast24h = this.orchestrator
|
|
939
|
+
? this.orchestrator
|
|
940
|
+
.list()
|
|
941
|
+
.filter((t) => t.isAutomationTask && t.createdAt > cutoff).length
|
|
942
|
+
: 0;
|
|
943
|
+
const recentAutomationTasks = this.orchestrator
|
|
944
|
+
? this.orchestrator
|
|
945
|
+
.list()
|
|
946
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
947
|
+
.slice(0, 20)
|
|
948
|
+
.map((t) => ({
|
|
949
|
+
id: t.id,
|
|
950
|
+
status: t.status,
|
|
951
|
+
...(t.triggerSource !== undefined && {
|
|
952
|
+
triggerSource: t.triggerSource,
|
|
953
|
+
}),
|
|
954
|
+
...(t.startedAt !== undefined &&
|
|
955
|
+
t.doneAt !== undefined && {
|
|
956
|
+
durationMs: t.doneAt - t.startedAt,
|
|
957
|
+
}),
|
|
958
|
+
createdAt: new Date(t.createdAt).toISOString(),
|
|
959
|
+
...(t.output !== undefined && {
|
|
960
|
+
output: t.output.slice(0, 2000),
|
|
961
|
+
}),
|
|
962
|
+
...(t.errorMessage !== undefined && {
|
|
963
|
+
errorMessage: t.errorMessage,
|
|
964
|
+
}),
|
|
965
|
+
}))
|
|
966
|
+
: [];
|
|
967
|
+
return {
|
|
968
|
+
generatedAt: new Date().toISOString(),
|
|
969
|
+
windowHours: wh,
|
|
970
|
+
topTools,
|
|
971
|
+
hooksLast24h,
|
|
972
|
+
recentAutomationTasks,
|
|
973
|
+
};
|
|
974
|
+
};
|
|
975
|
+
this.server.streamFn = (listener) => this.activityLog.subscribe(listener);
|
|
976
|
+
this.server.tasksFn = () => ({
|
|
977
|
+
tasks: (this.orchestrator?.list() ?? []).map((t) => ({
|
|
978
|
+
taskId: t.id,
|
|
979
|
+
sessionId: t.sessionId.slice(0, 8),
|
|
980
|
+
status: t.status,
|
|
981
|
+
cancelReason: t.cancelReason,
|
|
982
|
+
createdAt: t.createdAt,
|
|
983
|
+
startedAt: t.startedAt,
|
|
984
|
+
doneAt: t.doneAt,
|
|
985
|
+
// Omit prompt (may contain sensitive content) and cap output
|
|
986
|
+
output: t.output !== undefined ? t.output.slice(0, 2000) : undefined,
|
|
987
|
+
// Cap stderrTail at 500 chars — subprocess stderr may contain paths,
|
|
988
|
+
// env fragments, or user-code errors; match existing redaction policy.
|
|
989
|
+
stderrTail: t.stderrTail ? t.stderrTail.slice(-500) : undefined,
|
|
990
|
+
wasAborted: t.wasAborted,
|
|
991
|
+
startupMs: t.startupMs,
|
|
992
|
+
errorMessage: t.errorMessage,
|
|
993
|
+
timeoutMs: t.timeoutMs,
|
|
994
|
+
})),
|
|
995
|
+
});
|
|
996
|
+
this.server.managedSettingsPath =
|
|
997
|
+
this.config.managedSettingsPath ?? undefined;
|
|
998
|
+
this.server.approvalGate = this.config.approvalGate ?? "off";
|
|
999
|
+
this.server.approvalWebhookUrl =
|
|
1000
|
+
this.config.approvalWebhookUrl ?? undefined;
|
|
1001
|
+
this.server.onApprovalDecision = (event, meta) => this.activityLog.recordEvent(event, meta);
|
|
1002
|
+
this.server.recipesFn = () => {
|
|
1003
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1004
|
+
return listInstalledRecipes(recipesDir);
|
|
1005
|
+
};
|
|
1006
|
+
this.server.saveRecipeFn = (draft) => {
|
|
1007
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1008
|
+
return saveRecipe(recipesDir, draft);
|
|
1009
|
+
};
|
|
1010
|
+
this.server.runsFn = (q) => {
|
|
1011
|
+
if (!this.recipeRunLog)
|
|
1012
|
+
return [];
|
|
1013
|
+
return this.recipeRunLog.query({
|
|
1014
|
+
...(q.limit !== undefined && { limit: q.limit }),
|
|
1015
|
+
...(q.trigger !== undefined && {
|
|
1016
|
+
trigger: q.trigger,
|
|
1017
|
+
}),
|
|
1018
|
+
...(q.status !== undefined && {
|
|
1019
|
+
status: q.status,
|
|
1020
|
+
}),
|
|
1021
|
+
...(q.recipe !== undefined && { recipe: q.recipe }),
|
|
1022
|
+
...(q.after !== undefined && { after: q.after }),
|
|
1023
|
+
});
|
|
1024
|
+
};
|
|
1025
|
+
this.server.sessionsFn = () => [...this.sessions.values()].map((s) => ({
|
|
1026
|
+
id: s.id,
|
|
1027
|
+
connectedAt: new Date(s.connectedAt).toISOString(),
|
|
1028
|
+
openedFileCount: s.openedFiles.size,
|
|
1029
|
+
pendingApprovals: getApprovalQueue()
|
|
1030
|
+
.list()
|
|
1031
|
+
.filter((a) => a.sessionId === s.id).length,
|
|
1032
|
+
}));
|
|
1033
|
+
this.server.sessionDetailFn = (id) => {
|
|
1034
|
+
const s = this.sessions.get(id);
|
|
1035
|
+
const summary = s
|
|
1036
|
+
? {
|
|
1037
|
+
id: s.id,
|
|
1038
|
+
connectedAt: new Date(s.connectedAt).toISOString(),
|
|
1039
|
+
openedFileCount: s.openedFiles.size,
|
|
1040
|
+
pendingApprovals: getApprovalQueue()
|
|
1041
|
+
.list()
|
|
1042
|
+
.filter((a) => a.sessionId === s.id).length,
|
|
1043
|
+
}
|
|
1044
|
+
: null;
|
|
1045
|
+
const lifecycle = this.activityLog.querySessionLifecycle(id, 100);
|
|
1046
|
+
const tools = this.activityLog.querySessionTools(id, 100);
|
|
1047
|
+
const decisions = this.decisionTraceLog?.query({ sessionId: id, limit: 50 }) ?? [];
|
|
1048
|
+
const approvals = getApprovalQueue()
|
|
1049
|
+
.list()
|
|
1050
|
+
.filter((a) => a.sessionId === id);
|
|
1051
|
+
return {
|
|
1052
|
+
summary,
|
|
1053
|
+
lifecycle: lifecycle,
|
|
1054
|
+
tools: tools,
|
|
1055
|
+
decisions: decisions,
|
|
1056
|
+
approvals: approvals,
|
|
1057
|
+
};
|
|
1058
|
+
};
|
|
1059
|
+
this.server.webhookFn = async (hookPath, payload) => {
|
|
1060
|
+
if (!this.orchestrator) {
|
|
1061
|
+
return {
|
|
1062
|
+
ok: false,
|
|
1063
|
+
error: "orchestrator_unavailable",
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1067
|
+
const match = findWebhookRecipe(recipesDir, hookPath);
|
|
1068
|
+
if (!match) {
|
|
1069
|
+
return { ok: false, error: "not_found" };
|
|
1070
|
+
}
|
|
1071
|
+
const loaded = loadRecipePrompt(recipesDir, match.name);
|
|
1072
|
+
if (!loaded) {
|
|
1073
|
+
return { ok: false, error: "recipe_file_missing" };
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
const taskId = this.orchestrator.enqueue({
|
|
1077
|
+
prompt: renderWebhookPrompt(loaded.prompt, payload),
|
|
1078
|
+
triggerSource: `webhook:${match.name}`,
|
|
1079
|
+
});
|
|
1080
|
+
return { ok: true, taskId, name: match.name };
|
|
1081
|
+
}
|
|
1082
|
+
catch (err) {
|
|
1083
|
+
return {
|
|
1084
|
+
ok: false,
|
|
1085
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
this.server.runRecipeFn = async (name) => {
|
|
1090
|
+
if (!this.orchestrator) {
|
|
1091
|
+
return {
|
|
1092
|
+
ok: false,
|
|
1093
|
+
error: "Orchestrator unavailable — start bridge with --claude-driver subprocess",
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1097
|
+
const loaded = loadRecipePrompt(recipesDir, name);
|
|
1098
|
+
if (!loaded) {
|
|
1099
|
+
return {
|
|
1100
|
+
ok: false,
|
|
1101
|
+
error: `Recipe "${name}" not found in ${recipesDir}`,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
const taskId = this.orchestrator.enqueue({
|
|
1106
|
+
prompt: loaded.prompt,
|
|
1107
|
+
triggerSource: `recipe:${name}`,
|
|
1108
|
+
});
|
|
1109
|
+
return { ok: true, taskId };
|
|
1110
|
+
}
|
|
1111
|
+
catch (err) {
|
|
1112
|
+
return {
|
|
1113
|
+
ok: false,
|
|
1114
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
this.server.readyFn = () => {
|
|
1119
|
+
// Count tools from the first active session (all sessions share the same tool set)
|
|
1120
|
+
const anySession = [...this.sessions.values()][0];
|
|
1121
|
+
const toolCount = anySession?.transport.toolCount ?? 0;
|
|
1122
|
+
return {
|
|
1123
|
+
ready: this.ready,
|
|
1124
|
+
toolCount,
|
|
1125
|
+
extensionConnected: this.extensionClient.isConnected(),
|
|
1126
|
+
};
|
|
1127
|
+
};
|
|
1128
|
+
this.server.statusFn = () => {
|
|
1129
|
+
let sessionsInGrace = 0;
|
|
1130
|
+
const sessionList = [];
|
|
1131
|
+
for (const s of this.sessions.values()) {
|
|
1132
|
+
if (s.graceTimer)
|
|
1133
|
+
sessionsInGrace++;
|
|
1134
|
+
sessionList.push({
|
|
1135
|
+
id: s.id.slice(0, 8),
|
|
1136
|
+
connectedAt: new Date(s.connectedAt).toISOString(),
|
|
1137
|
+
inGrace: s.graceTimer !== null,
|
|
1138
|
+
openedFiles: s.openedFiles.size,
|
|
1139
|
+
terminalPrefix: s.terminalPrefix,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
claudeCode: this.sessions.size > 0,
|
|
1144
|
+
activeSessions: this.sessions.size,
|
|
1145
|
+
sessionsInGrace,
|
|
1146
|
+
gracePeriodMs: this.config.gracePeriodMs,
|
|
1147
|
+
lastConnectAt: this.lastConnectAt,
|
|
1148
|
+
lastDisconnectAt: this.lastDisconnectAt,
|
|
1149
|
+
lastDisconnectCode: this.lastDisconnectCode,
|
|
1150
|
+
lastDisconnectReason: this.lastDisconnectReason,
|
|
1151
|
+
sessions: sessionList,
|
|
1152
|
+
extension: this.extensionClient.isConnected(),
|
|
1153
|
+
extensionCircuitBreaker: this.extensionClient.getCircuitBreakerState(),
|
|
1154
|
+
timeline: this.activityLog.queryTimeline({ last: 50 }),
|
|
1155
|
+
patchwork: {
|
|
1156
|
+
workspace: this.config.workspace,
|
|
1157
|
+
approvalGate: this.server.approvalGate,
|
|
1158
|
+
fullMode: this.config.fullMode,
|
|
1159
|
+
claudeDriver: this.config.claudeDriver,
|
|
1160
|
+
automationEnabled: this.config.automationEnabled,
|
|
1161
|
+
port: this.port,
|
|
1162
|
+
webhookUrl: this.server.approvalWebhookUrl ?? null,
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
};
|
|
1166
|
+
// 3b-notify. Wire CC hook notify endpoint
|
|
1167
|
+
this.server.notifyFn = (event, args) => {
|
|
1168
|
+
if (!this.automationHooks) {
|
|
1169
|
+
return { ok: false, error: "Automation not enabled" };
|
|
1170
|
+
}
|
|
1171
|
+
switch (event) {
|
|
1172
|
+
case "PreCompact":
|
|
1173
|
+
this.automationHooks.handlePreCompact();
|
|
1174
|
+
return { ok: true };
|
|
1175
|
+
case "PostCompact":
|
|
1176
|
+
this.automationHooks.handlePostCompact();
|
|
1177
|
+
return { ok: true };
|
|
1178
|
+
case "InstructionsLoaded":
|
|
1179
|
+
this.automationHooks.handleInstructionsLoaded();
|
|
1180
|
+
return { ok: true };
|
|
1181
|
+
case "TaskCreated":
|
|
1182
|
+
if (!args.taskId || !args.prompt) {
|
|
1183
|
+
return { ok: false, error: "Missing taskId or prompt" };
|
|
1184
|
+
}
|
|
1185
|
+
this.automationHooks.handleTaskCreated({
|
|
1186
|
+
taskId: args.taskId,
|
|
1187
|
+
prompt: args.prompt,
|
|
1188
|
+
});
|
|
1189
|
+
return { ok: true };
|
|
1190
|
+
case "PermissionDenied":
|
|
1191
|
+
if (!args.tool || !args.reason) {
|
|
1192
|
+
return { ok: false, error: "Missing tool or reason" };
|
|
1193
|
+
}
|
|
1194
|
+
this.automationHooks.handlePermissionDenied({
|
|
1195
|
+
tool: args.tool,
|
|
1196
|
+
reason: args.reason,
|
|
1197
|
+
});
|
|
1198
|
+
return { ok: true };
|
|
1199
|
+
case "CwdChanged":
|
|
1200
|
+
if (!args.cwd)
|
|
1201
|
+
return { ok: false, error: "Missing cwd" };
|
|
1202
|
+
this.automationHooks.handleCwdChanged(args.cwd);
|
|
1203
|
+
return { ok: true };
|
|
1204
|
+
default:
|
|
1205
|
+
return { ok: false, error: `Unknown CC event: ${event}` };
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
// 3c-launchQuickTask. Wire POST /launch-quick-task endpoint. Pick any live
|
|
1209
|
+
// session's transport to dispatch (all sessions share the same registered
|
|
1210
|
+
// tools). When no sessions are connected, the endpoint returns 503.
|
|
1211
|
+
this.server.launchQuickTaskFn = async (presetId, source) => {
|
|
1212
|
+
const firstSession = this.sessions.values().next().value;
|
|
1213
|
+
if (!firstSession) {
|
|
1214
|
+
return {
|
|
1215
|
+
ok: false,
|
|
1216
|
+
error: "No active bridge session — connect a client first",
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
const result = await firstSession.transport.invokeToolDirect("launchQuickTask", { presetId, source });
|
|
1220
|
+
if (result === null) {
|
|
1221
|
+
return {
|
|
1222
|
+
ok: false,
|
|
1223
|
+
error: "launchQuickTask not registered — requires --claude-driver subprocess",
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
const r = result;
|
|
1227
|
+
if (r.isError) {
|
|
1228
|
+
const errText = r.content?.[0]?.text ?? "{}";
|
|
1229
|
+
try {
|
|
1230
|
+
const parsed = JSON.parse(errText);
|
|
1231
|
+
return {
|
|
1232
|
+
ok: false,
|
|
1233
|
+
error: parsed.error ?? "unknown error",
|
|
1234
|
+
code: parsed.code,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
catch {
|
|
1238
|
+
return { ok: false, error: errText };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return { ok: true, result: r.structuredContent ?? null };
|
|
1242
|
+
};
|
|
1243
|
+
// 3b. Set up Streamable HTTP transport handler (POST/GET/DELETE /mcp)
|
|
1244
|
+
this.httpMcpHandler = new StreamableHttpHandler(this.config, probes, this.extensionClient, this.activityLog, this.fileLock, this.sessions, this.orchestrator, this.logger, () => this.pluginWatcher?.getTools() ?? this.pluginTools, () => this.pluginWatcher, this.oauthServer
|
|
1245
|
+
? (token) => this.oauthServer?.resolveBearerScope(token) ?? null
|
|
1246
|
+
: null, async () => {
|
|
1247
|
+
await this.refreshRecentTracesDigest();
|
|
1248
|
+
return this.buildInstructions();
|
|
1249
|
+
});
|
|
1250
|
+
this.server.httpMcpHandler = (req, res) => this.httpMcpHandler?.handle(req, res) ?? Promise.resolve();
|
|
1251
|
+
// 3. Check for stale lock files
|
|
1252
|
+
this.lockFile.cleanStale();
|
|
1253
|
+
// 4. Find port and start server — if this throws, clean up the HTTP handler
|
|
1254
|
+
// so its cleanupTimer interval does not leak.
|
|
1255
|
+
let port;
|
|
1256
|
+
try {
|
|
1257
|
+
port = await this.server.findAndListen(this.config.port, this.config.bindAddress);
|
|
1258
|
+
}
|
|
1259
|
+
catch (err) {
|
|
1260
|
+
this.httpMcpHandler.close();
|
|
1261
|
+
this.httpMcpHandler = null;
|
|
1262
|
+
throw err;
|
|
1263
|
+
}
|
|
1264
|
+
this.port = port;
|
|
1265
|
+
// 4b. Start WebSocket keepalive heartbeat (keeps MCP session alive during long idle periods)
|
|
1266
|
+
this._startWsHeartbeat();
|
|
1267
|
+
// 4c. Load persisted tasks from previous sessions (best-effort)
|
|
1268
|
+
if (this.orchestrator) {
|
|
1269
|
+
await this.orchestrator.loadPersistedTasks(port).catch(() => {
|
|
1270
|
+
/* best-effort */
|
|
1271
|
+
});
|
|
1272
|
+
const reenqueued = this.orchestrator.list("pending").length;
|
|
1273
|
+
const interrupted = this.orchestrator.list("interrupted").length;
|
|
1274
|
+
if (reenqueued > 0 || interrupted > 0) {
|
|
1275
|
+
this.logger.info(`Restored from previous run: ${reenqueued} task(s) re-enqueued, ${interrupted} task(s) interrupted`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// 5. Enable activity log disk persistence
|
|
1279
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
1280
|
+
this.activityLog.setPersistPath(path.join(configDir, "ide", `activity-${port}.jsonl`));
|
|
1281
|
+
// 6. Check for recent checkpoint from previous run and restore openedFiles
|
|
1282
|
+
const prevCheckpoint = SessionCheckpoint.loadLatest(5 * 60 * 1000, this.config.workspace);
|
|
1283
|
+
if (prevCheckpoint) {
|
|
1284
|
+
const ageSec = Math.round((Date.now() - prevCheckpoint.savedAt) / 1000);
|
|
1285
|
+
const allFiles = extractRestoredFiles(prevCheckpoint, this.config.workspace);
|
|
1286
|
+
if (allFiles.size > 0) {
|
|
1287
|
+
this.restoredOpenedFiles = allFiles;
|
|
1288
|
+
this.checkpointRestored = { fileCount: allFiles.size, ageSec };
|
|
1289
|
+
this.logger.info(`Restored ${allFiles.size} tracked file(s) from previous session (${ageSec}s ago, port ${prevCheckpoint.port})`);
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
this.logger.info(`Previous session checkpoint found (${ageSec}s ago, port ${prevCheckpoint.port}) — no files to restore`);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// 7. Write lock file
|
|
1296
|
+
const lockPath = this.lockFile.write(port, this.authToken, [this.config.workspace], this.config.ideName);
|
|
1297
|
+
// 8. Start session checkpoint (write every 30s)
|
|
1298
|
+
this.checkpoint = new SessionCheckpoint(port, this.config.workspace);
|
|
1299
|
+
this.checkpoint.start(() => this._buildCheckpoint(port));
|
|
1300
|
+
// Register shutdown handlers
|
|
1301
|
+
let shuttingDown = false;
|
|
1302
|
+
const shutdown = async (signal, exitCode) => {
|
|
1303
|
+
if (shuttingDown)
|
|
1304
|
+
return;
|
|
1305
|
+
shuttingDown = true;
|
|
1306
|
+
if (signal === "uncaughtException") {
|
|
1307
|
+
// Tool names go to stderr only (not the activity log) to avoid
|
|
1308
|
+
// leaking operational detail into activity-log consumers.
|
|
1309
|
+
const inFlightTools = [...this.sessions.values()].flatMap((s) => s.transport.getStats().inFlightTools);
|
|
1310
|
+
if (inFlightTools.length > 0) {
|
|
1311
|
+
this.logger.error(`In-flight tools at crash: ${inFlightTools.join(", ")}`);
|
|
1312
|
+
}
|
|
1313
|
+
this.activityLog?.recordEvent("crash_detected", {
|
|
1314
|
+
signal,
|
|
1315
|
+
sessions: this.sessions.size,
|
|
1316
|
+
inFlightToolCount: inFlightTools.length,
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
this.logger.info(`Shutdown initiated by ${signal}`);
|
|
1320
|
+
const forceTimer = setTimeout(() => {
|
|
1321
|
+
process.exit(exitCode);
|
|
1322
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
1323
|
+
forceTimer.unref();
|
|
1324
|
+
await this.stop();
|
|
1325
|
+
await shutdownTelemetry();
|
|
1326
|
+
process.exit(exitCode);
|
|
1327
|
+
};
|
|
1328
|
+
// All process-level signal handlers are guarded by globalHandlersRegistered so
|
|
1329
|
+
// that repeated start() calls (e.g. in tests or --watch restarts) don't
|
|
1330
|
+
// accumulate process.once listeners and trigger MaxListenersExceededWarning.
|
|
1331
|
+
if (!globalHandlersRegistered) {
|
|
1332
|
+
globalHandlersRegistered = true;
|
|
1333
|
+
process.once("SIGINT", () => shutdown("SIGINT", 130));
|
|
1334
|
+
process.once("SIGTERM", () => shutdown("SIGTERM", 143));
|
|
1335
|
+
process.once("SIGHUP", () => shutdown("SIGHUP", 143));
|
|
1336
|
+
process.on("unhandledRejection", (reason) => {
|
|
1337
|
+
for (const [sid, session] of this.sessions) {
|
|
1338
|
+
const stats = session.transport.getStats();
|
|
1339
|
+
if (stats.inFlightTools.length > 0) {
|
|
1340
|
+
this.logger.error(`Session ${sid.slice(0, 8)} had ${stats.inFlightTools.length} in-flight tool(s): ${stats.inFlightTools.join(", ")}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
this.logger.error(`Unhandled rejection: ${reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)}`);
|
|
1344
|
+
});
|
|
1345
|
+
process.once("uncaughtException", (err) => {
|
|
1346
|
+
for (const [sid, session] of this.sessions) {
|
|
1347
|
+
const stats = session.transport.getStats();
|
|
1348
|
+
if (stats.inFlightTools.length > 0) {
|
|
1349
|
+
this.logger.error(`Session ${sid.slice(0, 8)} had ${stats.inFlightTools.length} in-flight tool(s) at crash: ${stats.inFlightTools.join(", ")}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
this.logger.error(`Uncaught exception: ${err.stack ?? err.message}`);
|
|
1353
|
+
shutdown("uncaughtException", 1);
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
// Startup banner to stderr (not stdout) to avoid capture by parent processes
|
|
1357
|
+
this.logger.info("claude-ide-bridge ready");
|
|
1358
|
+
this.logger.info(` Port: ${port}`);
|
|
1359
|
+
this.logger.info(` Workspace: ${this.config.workspace}`);
|
|
1360
|
+
this.logger.info(` Editor: ${this.config.ideName || "none"}`);
|
|
1361
|
+
this.logger.info(` Lock file: ${lockPath}`);
|
|
1362
|
+
this.logger.info(this.config.fullMode
|
|
1363
|
+
? " Tools: full (~140 tools — IDE + git + terminal + file ops + HTTP + GitHub) [default]"
|
|
1364
|
+
: " Tools: slim (~60 IDE-exclusive tools — pass --slim)");
|
|
1365
|
+
if (isInGitWorktree(this.config.workspace)) {
|
|
1366
|
+
this.logger.info(" Worktree: cwd is a linked git worktree. If this is a Cowork session, bridge MCP tools are unreachable from inside Cowork itself — see docs/cowork.md.");
|
|
1367
|
+
}
|
|
1368
|
+
this.logger.info(" Connect: run `claude` in a new terminal, then /ide");
|
|
1369
|
+
if (this.config.gracePeriodMs !== 30_000) {
|
|
1370
|
+
this.logger.info(` Grace: ${this.config.gracePeriodMs / 1000}s reconnect window`);
|
|
1371
|
+
}
|
|
1372
|
+
if (!process.env.TMUX &&
|
|
1373
|
+
!process.env.STY &&
|
|
1374
|
+
!process.env.ZELLIJ &&
|
|
1375
|
+
!process.env.ZELLIJ_SESSION_NAME) {
|
|
1376
|
+
this.logger.warn("WARNING: Not running inside tmux, screen, or zellij. SSH disconnection will kill this process.");
|
|
1377
|
+
this.logger.warn(" Recommended: use 'npm run start-all' or wrap in tmux/screen.");
|
|
1378
|
+
}
|
|
1379
|
+
this.logger.event("bridge_started", {
|
|
1380
|
+
port,
|
|
1381
|
+
workspace: this.config.workspace,
|
|
1382
|
+
editor: this.config.editorCommand,
|
|
1383
|
+
});
|
|
1384
|
+
if (this.config.verbose) {
|
|
1385
|
+
this.logger.debug(`Resolved config: ${JSON.stringify({
|
|
1386
|
+
workspace: this.config.workspace,
|
|
1387
|
+
editor: this.config.editorCommand,
|
|
1388
|
+
linters: this.config.linters,
|
|
1389
|
+
commandTimeout: this.config.commandTimeout,
|
|
1390
|
+
maxResultSize: this.config.maxResultSize,
|
|
1391
|
+
commandAllowlist: this.config.commandAllowlist,
|
|
1392
|
+
})}`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
/** Start the bridge-level WebSocket keepalive heartbeat. Idempotent. */
|
|
1396
|
+
_startWsHeartbeat() {
|
|
1397
|
+
if (this.wsHeartbeatInterval || this.config.wsPingIntervalMs === 0)
|
|
1398
|
+
return;
|
|
1399
|
+
this.wsHeartbeatInterval = setInterval(() => {
|
|
1400
|
+
for (const [id, session] of this.sessions) {
|
|
1401
|
+
if (session.graceTimer)
|
|
1402
|
+
continue; // WS already closed, grace pending
|
|
1403
|
+
const { ws } = session;
|
|
1404
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
1405
|
+
continue;
|
|
1406
|
+
if (!session.wsAlive) {
|
|
1407
|
+
this.logger.warn(`Session ${id.slice(0, 8)} missed pong — terminating stale WebSocket`);
|
|
1408
|
+
ws.terminate(); // triggers 'close' → grace timer
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
session.wsAlive = false;
|
|
1412
|
+
try {
|
|
1413
|
+
ws.ping();
|
|
1414
|
+
}
|
|
1415
|
+
catch {
|
|
1416
|
+
// Socket already broken; close event will fire and start the grace timer
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}, this.config.wsPingIntervalMs);
|
|
1420
|
+
this.wsHeartbeatInterval.unref(); // don't prevent Node exit when idle
|
|
1421
|
+
}
|
|
1422
|
+
async stop() {
|
|
1423
|
+
if (this.stopped)
|
|
1424
|
+
return;
|
|
1425
|
+
this.stopped = true;
|
|
1426
|
+
this.logger.info("Shutting down...");
|
|
1427
|
+
this._stopPeriodicSnapshots();
|
|
1428
|
+
this.automationHooks?.destroy();
|
|
1429
|
+
this.recipeScheduler?.stop();
|
|
1430
|
+
this.recipeScheduler = null;
|
|
1431
|
+
this.pluginWatcher?.stop();
|
|
1432
|
+
this.pluginWatcher = null;
|
|
1433
|
+
this.httpMcpHandler?.close();
|
|
1434
|
+
if (this.wsHeartbeatInterval) {
|
|
1435
|
+
clearInterval(this.wsHeartbeatInterval);
|
|
1436
|
+
this.wsHeartbeatInterval = null;
|
|
1437
|
+
}
|
|
1438
|
+
if (this.listChangedTimer) {
|
|
1439
|
+
clearTimeout(this.listChangedTimer);
|
|
1440
|
+
this.listChangedTimer = null;
|
|
1441
|
+
}
|
|
1442
|
+
// Snapshot aggregate stats before cleanup removes sessions from the map
|
|
1443
|
+
let totalSessions = 0;
|
|
1444
|
+
let totalCalls = 0;
|
|
1445
|
+
let totalErrors = 0;
|
|
1446
|
+
let maxDurationMs = 0;
|
|
1447
|
+
for (const session of this.sessions.values()) {
|
|
1448
|
+
totalSessions++;
|
|
1449
|
+
const stats = session.transport.getStats();
|
|
1450
|
+
totalCalls += stats.callCount;
|
|
1451
|
+
totalErrors += stats.errorCount;
|
|
1452
|
+
maxDurationMs = Math.max(maxDurationMs, Date.now() - session.connectedAt);
|
|
1453
|
+
}
|
|
1454
|
+
// Flush checkpoint with current session state before cleaning up sessions.
|
|
1455
|
+
// The periodic checkpoint writer runs every 30s, so up to 30s of editor
|
|
1456
|
+
// state can be lost on SIGTERM without this. Awaited with a 3s timeout so
|
|
1457
|
+
// a slow/blocked filesystem write never hangs shutdown indefinitely.
|
|
1458
|
+
if (this.checkpoint && this.port > 0) {
|
|
1459
|
+
try {
|
|
1460
|
+
await Promise.race([
|
|
1461
|
+
Promise.resolve().then(() => this.checkpoint.write(this._buildCheckpoint(this.port))),
|
|
1462
|
+
new Promise((resolve) => setTimeout(resolve, 3000)),
|
|
1463
|
+
]);
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
// best-effort — don't prevent clean shutdown
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
// Clean up all active sessions (cleanupSession skips the extension notification
|
|
1470
|
+
// during shutdown because this.stopped is already true)
|
|
1471
|
+
for (const id of [...this.sessions.keys()]) {
|
|
1472
|
+
this.cleanupSession(id);
|
|
1473
|
+
}
|
|
1474
|
+
const shutdownErrorPart = totalErrors > 0
|
|
1475
|
+
? `, ${totalErrors} error${totalErrors === 1 ? "" : "s"}`
|
|
1476
|
+
: "";
|
|
1477
|
+
this.logger.info(`Shutdown complete — ${totalSessions} session${totalSessions === 1 ? "" : "s"}, ${totalCalls} tool call${totalCalls === 1 ? "" : "s"}${shutdownErrorPart}`);
|
|
1478
|
+
// Send analytics if opted in — awaited with 2s timeout so it completes before process.exit()
|
|
1479
|
+
const analyticsOn = this.config.analyticsEnabled !== null
|
|
1480
|
+
? this.config.analyticsEnabled
|
|
1481
|
+
: getAnalyticsPref();
|
|
1482
|
+
if (analyticsOn === true && totalSessions > 0) {
|
|
1483
|
+
try {
|
|
1484
|
+
const entries = this.activityLog.query({ last: 500 });
|
|
1485
|
+
const summary = buildSummary(entries, maxDurationMs, PACKAGE_VERSION);
|
|
1486
|
+
await Promise.race([
|
|
1487
|
+
sendAnalytics(summary),
|
|
1488
|
+
new Promise((resolve) => setTimeout(resolve, 2000)),
|
|
1489
|
+
]);
|
|
1490
|
+
}
|
|
1491
|
+
catch {
|
|
1492
|
+
// Swallow all errors — analytics must never affect shutdown
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
// Send aggregate session-end notification to the extension before disconnecting
|
|
1496
|
+
if (totalSessions > 0) {
|
|
1497
|
+
this.extensionClient.notifyClaudeConnectionState(false, {
|
|
1498
|
+
callCount: totalCalls,
|
|
1499
|
+
errorCount: totalErrors,
|
|
1500
|
+
durationMs: maxDurationMs,
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
this.extensionClient.disconnect();
|
|
1504
|
+
// Clear any listChanged timer that may have been set during concurrent extension disconnect
|
|
1505
|
+
if (this.listChangedTimer) {
|
|
1506
|
+
clearTimeout(this.listChangedTimer);
|
|
1507
|
+
this.listChangedTimer = null;
|
|
1508
|
+
}
|
|
1509
|
+
// Flush tasks to disk BEFORE cancelling them so the file captures the true
|
|
1510
|
+
// pre-shutdown state (pending = still pending, running = interrupted).
|
|
1511
|
+
// Cancel AFTER flush so in-flight handlers receive their signal while the
|
|
1512
|
+
// transport is still reachable.
|
|
1513
|
+
if (this.orchestrator && this.port > 0) {
|
|
1514
|
+
this.orchestrator.flushTasksToDisk(this.port);
|
|
1515
|
+
}
|
|
1516
|
+
if (this.orchestrator) {
|
|
1517
|
+
for (const task of [
|
|
1518
|
+
...this.orchestrator.list("pending"),
|
|
1519
|
+
...this.orchestrator.list("running"),
|
|
1520
|
+
]) {
|
|
1521
|
+
this.orchestrator.cancel(task.id, "shutdown");
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
await this.server.close();
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
// Server may already be closed
|
|
1529
|
+
}
|
|
1530
|
+
this.lockFile.delete();
|
|
1531
|
+
this.checkpoint?.stop();
|
|
1532
|
+
cleanupTempDirs();
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
//# sourceMappingURL=bridge.js.map
|