@xelauvas/xela-cli 0.1.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/README.md +200 -0
- package/bin/xela +100 -0
- package/package.json +88 -0
- package/src/QueryEngine.ts +1295 -0
- package/src/Task.ts +125 -0
- package/src/Tool.ts +792 -0
- package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
- package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
- package/src/_shims/bun_bundle.js +9 -0
- package/src/_shims/cjs_stub.cjs +23 -0
- package/src/_shims/empty_stub.js +33 -0
- package/src/_shims/loader.js +352 -0
- package/src/_shims/openai_adapter.ts +486 -0
- package/src/_shims/react_compiler_runtime.js +17 -0
- package/src/_shims/register.js +148 -0
- package/src/assistant/sessionHistory.ts +87 -0
- package/src/bootstrap/state.ts +1759 -0
- package/src/bridge/bridgeApi.ts +539 -0
- package/src/bridge/bridgeConfig.ts +48 -0
- package/src/bridge/bridgeDebug.ts +135 -0
- package/src/bridge/bridgeEnabled.ts +202 -0
- package/src/bridge/bridgeMain.ts +2999 -0
- package/src/bridge/bridgeMessaging.ts +461 -0
- package/src/bridge/bridgePermissionCallbacks.ts +43 -0
- package/src/bridge/bridgePointer.ts +210 -0
- package/src/bridge/bridgeStatusUtil.ts +163 -0
- package/src/bridge/bridgeUI.ts +530 -0
- package/src/bridge/capacityWake.ts +56 -0
- package/src/bridge/codeSessionApi.ts +168 -0
- package/src/bridge/createSession.ts +384 -0
- package/src/bridge/debugUtils.ts +141 -0
- package/src/bridge/envLessBridgeConfig.ts +165 -0
- package/src/bridge/flushGate.ts +71 -0
- package/src/bridge/inboundAttachments.ts +175 -0
- package/src/bridge/inboundMessages.ts +80 -0
- package/src/bridge/initReplBridge.ts +569 -0
- package/src/bridge/jwtUtils.ts +256 -0
- package/src/bridge/pollConfig.ts +110 -0
- package/src/bridge/pollConfigDefaults.ts +82 -0
- package/src/bridge/remoteBridgeCore.ts +1008 -0
- package/src/bridge/replBridge.ts +2406 -0
- package/src/bridge/replBridgeHandle.ts +36 -0
- package/src/bridge/replBridgeTransport.ts +370 -0
- package/src/bridge/sessionIdCompat.ts +57 -0
- package/src/bridge/sessionRunner.ts +550 -0
- package/src/bridge/trustedDevice.ts +210 -0
- package/src/bridge/types.ts +262 -0
- package/src/bridge/workSecret.ts +127 -0
- package/src/buddy/CompanionSprite.tsx +371 -0
- package/src/buddy/companion.ts +133 -0
- package/src/buddy/prompt.ts +36 -0
- package/src/buddy/sprites.ts +514 -0
- package/src/buddy/types.ts +148 -0
- package/src/buddy/useBuddyNotification.tsx +98 -0
- package/src/cli/exit.ts +31 -0
- package/src/cli/handlers/agents.ts +70 -0
- package/src/cli/handlers/auth.ts +330 -0
- package/src/cli/handlers/autoMode.ts +170 -0
- package/src/cli/handlers/mcp.tsx +362 -0
- package/src/cli/handlers/plugins.ts +878 -0
- package/src/cli/handlers/util.tsx +110 -0
- package/src/cli/ndjsonSafeStringify.ts +32 -0
- package/src/cli/print.ts +5594 -0
- package/src/cli/remoteIO.ts +255 -0
- package/src/cli/structuredIO.ts +859 -0
- package/src/cli/transports/HybridTransport.ts +282 -0
- package/src/cli/transports/SSETransport.ts +711 -0
- package/src/cli/transports/SerialBatchEventUploader.ts +275 -0
- package/src/cli/transports/WebSocketTransport.ts +800 -0
- package/src/cli/transports/WorkerStateUploader.ts +131 -0
- package/src/cli/transports/ccrClient.ts +998 -0
- package/src/cli/transports/transportUtils.ts +45 -0
- package/src/cli/update.ts +422 -0
- package/src/commands/add-dir/add-dir.tsx +126 -0
- package/src/commands/add-dir/index.ts +11 -0
- package/src/commands/add-dir/validation.ts +110 -0
- package/src/commands/advisor.ts +109 -0
- package/src/commands/agents/agents.tsx +12 -0
- package/src/commands/agents/index.ts +10 -0
- package/src/commands/ant-trace/index.js +1 -0
- package/src/commands/autofix-pr/index.js +1 -0
- package/src/commands/backfill-sessions/index.js +1 -0
- package/src/commands/branch/branch.ts +296 -0
- package/src/commands/branch/index.ts +14 -0
- package/src/commands/break-cache/index.js +1 -0
- package/src/commands/bridge/bridge.tsx +509 -0
- package/src/commands/bridge/index.ts +26 -0
- package/src/commands/bridge-kick.ts +200 -0
- package/src/commands/brief.ts +130 -0
- package/src/commands/btw/btw.tsx +243 -0
- package/src/commands/btw/index.ts +13 -0
- package/src/commands/bughunter/index.js +1 -0
- package/src/commands/chrome/chrome.tsx +285 -0
- package/src/commands/chrome/index.ts +13 -0
- package/src/commands/clear/caches.ts +144 -0
- package/src/commands/clear/clear.ts +7 -0
- package/src/commands/clear/conversation.ts +251 -0
- package/src/commands/clear/index.ts +19 -0
- package/src/commands/color/color.ts +93 -0
- package/src/commands/color/index.ts +16 -0
- package/src/commands/commit-push-pr.ts +158 -0
- package/src/commands/commit.ts +92 -0
- package/src/commands/compact/compact.ts +287 -0
- package/src/commands/compact/index.ts +15 -0
- package/src/commands/config/config.tsx +7 -0
- package/src/commands/config/index.ts +11 -0
- package/src/commands/context/context-noninteractive.ts +325 -0
- package/src/commands/context/context.tsx +64 -0
- package/src/commands/context/index.ts +24 -0
- package/src/commands/copy/copy.tsx +371 -0
- package/src/commands/copy/index.ts +15 -0
- package/src/commands/cost/cost.ts +24 -0
- package/src/commands/cost/index.ts +23 -0
- package/src/commands/createMovedToPluginCommand.ts +65 -0
- package/src/commands/ctx_viz/index.js +1 -0
- package/src/commands/debug-tool-call/index.js +1 -0
- package/src/commands/desktop/desktop.tsx +9 -0
- package/src/commands/desktop/index.ts +26 -0
- package/src/commands/diff/diff.tsx +9 -0
- package/src/commands/diff/index.ts +8 -0
- package/src/commands/doctor/doctor.tsx +7 -0
- package/src/commands/doctor/index.ts +12 -0
- package/src/commands/effort/effort.tsx +183 -0
- package/src/commands/effort/index.ts +13 -0
- package/src/commands/env/index.js +1 -0
- package/src/commands/exit/exit.tsx +33 -0
- package/src/commands/exit/index.ts +12 -0
- package/src/commands/export/export.tsx +91 -0
- package/src/commands/export/index.ts +11 -0
- package/src/commands/extra-usage/extra-usage-core.ts +118 -0
- package/src/commands/extra-usage/extra-usage-noninteractive.ts +16 -0
- package/src/commands/extra-usage/extra-usage.tsx +17 -0
- package/src/commands/extra-usage/index.ts +31 -0
- package/src/commands/fast/fast.tsx +269 -0
- package/src/commands/fast/index.ts +26 -0
- package/src/commands/feedback/feedback.tsx +25 -0
- package/src/commands/feedback/index.ts +26 -0
- package/src/commands/files/files.ts +19 -0
- package/src/commands/files/index.ts +12 -0
- package/src/commands/good-claude/index.js +1 -0
- package/src/commands/heapdump/heapdump.ts +17 -0
- package/src/commands/heapdump/index.ts +12 -0
- package/src/commands/help/help.tsx +11 -0
- package/src/commands/help/index.ts +10 -0
- package/src/commands/hooks/hooks.tsx +13 -0
- package/src/commands/hooks/index.ts +11 -0
- package/src/commands/ide/ide.tsx +646 -0
- package/src/commands/ide/index.ts +11 -0
- package/src/commands/init-verifiers.ts +262 -0
- package/src/commands/init.ts +256 -0
- package/src/commands/insights.ts +3200 -0
- package/src/commands/install-github-app/ApiKeyStep.tsx +231 -0
- package/src/commands/install-github-app/CheckExistingSecretStep.tsx +190 -0
- package/src/commands/install-github-app/CheckGitHubStep.tsx +15 -0
- package/src/commands/install-github-app/ChooseRepoStep.tsx +211 -0
- package/src/commands/install-github-app/CreatingStep.tsx +65 -0
- package/src/commands/install-github-app/ErrorStep.tsx +85 -0
- package/src/commands/install-github-app/ExistingWorkflowStep.tsx +103 -0
- package/src/commands/install-github-app/InstallAppStep.tsx +94 -0
- package/src/commands/install-github-app/OAuthFlowStep.tsx +276 -0
- package/src/commands/install-github-app/SuccessStep.tsx +96 -0
- package/src/commands/install-github-app/WarningsStep.tsx +73 -0
- package/src/commands/install-github-app/index.ts +13 -0
- package/src/commands/install-github-app/install-github-app.tsx +587 -0
- package/src/commands/install-github-app/setupGitHubActions.ts +325 -0
- package/src/commands/install-slack-app/index.ts +12 -0
- package/src/commands/install-slack-app/install-slack-app.ts +30 -0
- package/src/commands/install.tsx +300 -0
- package/src/commands/issue/index.js +1 -0
- package/src/commands/keybindings/index.ts +13 -0
- package/src/commands/keybindings/keybindings.ts +53 -0
- package/src/commands/login/index.ts +14 -0
- package/src/commands/login/login.tsx +104 -0
- package/src/commands/logout/index.ts +10 -0
- package/src/commands/logout/logout.tsx +82 -0
- package/src/commands/mcp/addCommand.ts +280 -0
- package/src/commands/mcp/index.ts +12 -0
- package/src/commands/mcp/mcp.tsx +85 -0
- package/src/commands/mcp/xaaIdpCommand.ts +266 -0
- package/src/commands/memory/index.ts +10 -0
- package/src/commands/memory/memory.tsx +90 -0
- package/src/commands/mobile/index.ts +11 -0
- package/src/commands/mobile/mobile.tsx +274 -0
- package/src/commands/mock-limits/index.js +1 -0
- package/src/commands/model/index.ts +16 -0
- package/src/commands/model/model.tsx +297 -0
- package/src/commands/oauth-refresh/index.js +1 -0
- package/src/commands/onboarding/index.js +1 -0
- package/src/commands/output-style/index.ts +11 -0
- package/src/commands/output-style/output-style.tsx +7 -0
- package/src/commands/passes/index.ts +22 -0
- package/src/commands/passes/passes.tsx +24 -0
- package/src/commands/perf-issue/index.js +1 -0
- package/src/commands/permissions/index.ts +11 -0
- package/src/commands/permissions/permissions.tsx +10 -0
- package/src/commands/plan/index.ts +11 -0
- package/src/commands/plan/plan.tsx +122 -0
- package/src/commands/plugin/AddMarketplace.tsx +162 -0
- package/src/commands/plugin/BrowseMarketplace.tsx +802 -0
- package/src/commands/plugin/DiscoverPlugins.tsx +781 -0
- package/src/commands/plugin/ManageMarketplaces.tsx +838 -0
- package/src/commands/plugin/ManagePlugins.tsx +2215 -0
- package/src/commands/plugin/PluginErrors.tsx +124 -0
- package/src/commands/plugin/PluginOptionsDialog.tsx +357 -0
- package/src/commands/plugin/PluginOptionsFlow.tsx +135 -0
- package/src/commands/plugin/PluginSettings.tsx +1072 -0
- package/src/commands/plugin/PluginTrustWarning.tsx +32 -0
- package/src/commands/plugin/UnifiedInstalledCell.tsx +565 -0
- package/src/commands/plugin/ValidatePlugin.tsx +98 -0
- package/src/commands/plugin/index.tsx +11 -0
- package/src/commands/plugin/parseArgs.ts +103 -0
- package/src/commands/plugin/plugin.tsx +7 -0
- package/src/commands/plugin/pluginDetailsHelpers.tsx +117 -0
- package/src/commands/plugin/usePagination.ts +171 -0
- package/src/commands/pr_comments/index.ts +50 -0
- package/src/commands/privacy-settings/index.ts +14 -0
- package/src/commands/privacy-settings/privacy-settings.tsx +58 -0
- package/src/commands/rate-limit-options/index.ts +19 -0
- package/src/commands/rate-limit-options/rate-limit-options.tsx +210 -0
- package/src/commands/release-notes/index.ts +11 -0
- package/src/commands/release-notes/release-notes.ts +50 -0
- package/src/commands/reload-plugins/index.ts +18 -0
- package/src/commands/reload-plugins/reload-plugins.ts +61 -0
- package/src/commands/remote-env/index.ts +15 -0
- package/src/commands/remote-env/remote-env.tsx +7 -0
- package/src/commands/remote-setup/api.ts +182 -0
- package/src/commands/remote-setup/index.ts +20 -0
- package/src/commands/remote-setup/remote-setup.tsx +187 -0
- package/src/commands/rename/generateSessionName.ts +67 -0
- package/src/commands/rename/index.ts +12 -0
- package/src/commands/rename/rename.ts +87 -0
- package/src/commands/reset-limits/index.js +4 -0
- package/src/commands/resume/index.ts +12 -0
- package/src/commands/resume/resume.tsx +275 -0
- package/src/commands/review/UltrareviewOverageDialog.tsx +96 -0
- package/src/commands/review/reviewRemote.ts +316 -0
- package/src/commands/review/ultrareviewCommand.tsx +58 -0
- package/src/commands/review/ultrareviewEnabled.ts +14 -0
- package/src/commands/review.ts +57 -0
- package/src/commands/rewind/index.ts +13 -0
- package/src/commands/rewind/rewind.ts +13 -0
- package/src/commands/sandbox-toggle/index.ts +50 -0
- package/src/commands/sandbox-toggle/sandbox-toggle.tsx +83 -0
- package/src/commands/security-review.ts +243 -0
- package/src/commands/session/index.ts +16 -0
- package/src/commands/session/session.tsx +140 -0
- package/src/commands/share/index.js +1 -0
- package/src/commands/skills/index.ts +10 -0
- package/src/commands/skills/skills.tsx +8 -0
- package/src/commands/stats/index.ts +10 -0
- package/src/commands/stats/stats.tsx +7 -0
- package/src/commands/status/index.ts +12 -0
- package/src/commands/status/status.tsx +8 -0
- package/src/commands/statusline.tsx +24 -0
- package/src/commands/stickers/index.ts +11 -0
- package/src/commands/stickers/stickers.ts +16 -0
- package/src/commands/summary/index.js +1 -0
- package/src/commands/tag/index.ts +12 -0
- package/src/commands/tag/tag.tsx +215 -0
- package/src/commands/tasks/index.ts +11 -0
- package/src/commands/tasks/tasks.tsx +8 -0
- package/src/commands/teleport/index.js +1 -0
- package/src/commands/terminalSetup/index.ts +23 -0
- package/src/commands/terminalSetup/terminalSetup.tsx +531 -0
- package/src/commands/theme/index.ts +10 -0
- package/src/commands/theme/theme.tsx +57 -0
- package/src/commands/thinkback/index.ts +13 -0
- package/src/commands/thinkback/thinkback.tsx +554 -0
- package/src/commands/thinkback-play/index.ts +17 -0
- package/src/commands/thinkback-play/thinkback-play.ts +43 -0
- package/src/commands/ultraplan.tsx +471 -0
- package/src/commands/upgrade/index.ts +16 -0
- package/src/commands/upgrade/upgrade.tsx +38 -0
- package/src/commands/usage/index.ts +9 -0
- package/src/commands/usage/usage.tsx +7 -0
- package/src/commands/version.ts +22 -0
- package/src/commands/vim/index.ts +11 -0
- package/src/commands/vim/vim.ts +38 -0
- package/src/commands/voice/index.ts +20 -0
- package/src/commands/voice/voice.ts +150 -0
- package/src/commands.ts +754 -0
- package/src/components/AgentProgressLine.tsx +136 -0
- package/src/components/App.tsx +56 -0
- package/src/components/ApproveApiKey.tsx +123 -0
- package/src/components/AutoModeOptInDialog.tsx +142 -0
- package/src/components/AutoUpdater.tsx +198 -0
- package/src/components/AutoUpdaterWrapper.tsx +91 -0
- package/src/components/AwsAuthStatusBox.tsx +82 -0
- package/src/components/BaseTextInput.tsx +136 -0
- package/src/components/BashModeProgress.tsx +56 -0
- package/src/components/BridgeDialog.tsx +401 -0
- package/src/components/BypassPermissionsModeDialog.tsx +87 -0
- package/src/components/ChannelDowngradeDialog.tsx +102 -0
- package/src/components/ClaudeCodeHint/PluginHintMenu.tsx +78 -0
- package/src/components/ClaudeInChromeOnboarding.tsx +121 -0
- package/src/components/ClaudeMdExternalIncludesDialog.tsx +137 -0
- package/src/components/ClickableImageRef.tsx +73 -0
- package/src/components/CompactSummary.tsx +118 -0
- package/src/components/ConfigurableShortcutHint.tsx +57 -0
- package/src/components/ConsoleOAuthFlow.tsx +631 -0
- package/src/components/ContextSuggestions.tsx +47 -0
- package/src/components/ContextVisualization.tsx +489 -0
- package/src/components/CoordinatorAgentStatus.tsx +273 -0
- package/src/components/CostThresholdDialog.tsx +50 -0
- package/src/components/CtrlOToExpand.tsx +51 -0
- package/src/components/CustomSelect/SelectMulti.tsx +213 -0
- package/src/components/CustomSelect/index.ts +3 -0
- package/src/components/CustomSelect/option-map.ts +50 -0
- package/src/components/CustomSelect/select-input-option.tsx +488 -0
- package/src/components/CustomSelect/select-option.tsx +68 -0
- package/src/components/CustomSelect/select.tsx +690 -0
- package/src/components/CustomSelect/use-multi-select-state.ts +414 -0
- package/src/components/CustomSelect/use-select-input.ts +287 -0
- package/src/components/CustomSelect/use-select-navigation.ts +653 -0
- package/src/components/CustomSelect/use-select-state.ts +157 -0
- package/src/components/DesktopHandoff.tsx +193 -0
- package/src/components/DesktopUpsell/DesktopUpsellStartup.tsx +171 -0
- package/src/components/DevBar.tsx +49 -0
- package/src/components/DevChannelsDialog.tsx +105 -0
- package/src/components/DiagnosticsDisplay.tsx +95 -0
- package/src/components/EffortCallout.tsx +265 -0
- package/src/components/EffortIndicator.ts +42 -0
- package/src/components/ExitFlow.tsx +48 -0
- package/src/components/ExportDialog.tsx +128 -0
- package/src/components/FallbackToolUseErrorMessage.tsx +116 -0
- package/src/components/FallbackToolUseRejectedMessage.tsx +16 -0
- package/src/components/FastIcon.tsx +46 -0
- package/src/components/Feedback.tsx +592 -0
- package/src/components/FeedbackSurvey/FeedbackSurvey.tsx +174 -0
- package/src/components/FeedbackSurvey/FeedbackSurveyView.tsx +108 -0
- package/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx +88 -0
- package/src/components/FeedbackSurvey/submitTranscriptShare.ts +112 -0
- package/src/components/FeedbackSurvey/useDebouncedDigitInput.ts +82 -0
- package/src/components/FeedbackSurvey/useFeedbackSurvey.tsx +296 -0
- package/src/components/FeedbackSurvey/useMemorySurvey.tsx +213 -0
- package/src/components/FeedbackSurvey/usePostCompactSurvey.tsx +206 -0
- package/src/components/FeedbackSurvey/useSurveyState.tsx +100 -0
- package/src/components/FileEditToolDiff.tsx +181 -0
- package/src/components/FileEditToolUpdatedMessage.tsx +124 -0
- package/src/components/FileEditToolUseRejectedMessage.tsx +170 -0
- package/src/components/FilePathLink.tsx +43 -0
- package/src/components/FullscreenLayout.tsx +637 -0
- package/src/components/GlobalSearchDialog.tsx +343 -0
- package/src/components/HelpV2/Commands.tsx +82 -0
- package/src/components/HelpV2/General.tsx +23 -0
- package/src/components/HelpV2/HelpV2.tsx +184 -0
- package/src/components/HighlightedCode/Fallback.tsx +193 -0
- package/src/components/HighlightedCode.tsx +190 -0
- package/src/components/HistorySearchDialog.tsx +118 -0
- package/src/components/IdeAutoConnectDialog.tsx +154 -0
- package/src/components/IdeOnboardingDialog.tsx +167 -0
- package/src/components/IdeStatusIndicator.tsx +58 -0
- package/src/components/IdleReturnDialog.tsx +118 -0
- package/src/components/InterruptedByUser.tsx +15 -0
- package/src/components/InvalidConfigDialog.tsx +156 -0
- package/src/components/InvalidSettingsDialog.tsx +89 -0
- package/src/components/KeybindingWarnings.tsx +55 -0
- package/src/components/LanguagePicker.tsx +86 -0
- package/src/components/LogSelector.tsx +1575 -0
- package/src/components/LogoV2/AnimatedAsterisk.tsx +50 -0
- package/src/components/LogoV2/AnimatedClawd.tsx +124 -0
- package/src/components/LogoV2/ChannelsNotice.tsx +266 -0
- package/src/components/LogoV2/Clawd.tsx +240 -0
- package/src/components/LogoV2/CondensedLogo.tsx +161 -0
- package/src/components/LogoV2/EmergencyTip.tsx +58 -0
- package/src/components/LogoV2/Feed.tsx +112 -0
- package/src/components/LogoV2/FeedColumn.tsx +59 -0
- package/src/components/LogoV2/GuestPassesUpsell.tsx +70 -0
- package/src/components/LogoV2/LogoV2.tsx +543 -0
- package/src/components/LogoV2/Opus1mMergeNotice.tsx +55 -0
- package/src/components/LogoV2/OverageCreditUpsell.tsx +166 -0
- package/src/components/LogoV2/VoiceModeNotice.tsx +68 -0
- package/src/components/LogoV2/WelcomeV2.tsx +433 -0
- package/src/components/LogoV2/feedConfigs.tsx +92 -0
- package/src/components/LspRecommendation/LspRecommendationMenu.tsx +88 -0
- package/src/components/MCPServerApprovalDialog.tsx +115 -0
- package/src/components/MCPServerDesktopImportDialog.tsx +203 -0
- package/src/components/MCPServerDialogCopy.tsx +15 -0
- package/src/components/MCPServerMultiselectDialog.tsx +133 -0
- package/src/components/ManagedSettingsSecurityDialog/ManagedSettingsSecurityDialog.tsx +149 -0
- package/src/components/ManagedSettingsSecurityDialog/utils.ts +144 -0
- package/src/components/Markdown.tsx +236 -0
- package/src/components/MarkdownTable.tsx +322 -0
- package/src/components/MemoryUsageIndicator.tsx +37 -0
- package/src/components/Message.tsx +627 -0
- package/src/components/MessageModel.tsx +43 -0
- package/src/components/MessageResponse.tsx +78 -0
- package/src/components/MessageRow.tsx +383 -0
- package/src/components/MessageSelector.tsx +831 -0
- package/src/components/MessageTimestamp.tsx +63 -0
- package/src/components/Messages.tsx +834 -0
- package/src/components/ModelPicker.tsx +448 -0
- package/src/components/NativeAutoUpdater.tsx +193 -0
- package/src/components/NotebookEditToolUseRejectedMessage.tsx +92 -0
- package/src/components/OffscreenFreeze.tsx +44 -0
- package/src/components/Onboarding.tsx +244 -0
- package/src/components/OutputStylePicker.tsx +112 -0
- package/src/components/PackageManagerAutoUpdater.tsx +104 -0
- package/src/components/Passes/Passes.tsx +184 -0
- package/src/components/PrBadge.tsx +97 -0
- package/src/components/PressEnterToContinue.tsx +15 -0
- package/src/components/PromptInput/HistorySearchInput.tsx +51 -0
- package/src/components/PromptInput/IssueFlagBanner.tsx +12 -0
- package/src/components/PromptInput/Notifications.tsx +332 -0
- package/src/components/PromptInput/PromptInput.tsx +2339 -0
- package/src/components/PromptInput/PromptInputFooter.tsx +191 -0
- package/src/components/PromptInput/PromptInputFooterLeftSide.tsx +517 -0
- package/src/components/PromptInput/PromptInputFooterSuggestions.tsx +293 -0
- package/src/components/PromptInput/PromptInputHelpMenu.tsx +358 -0
- package/src/components/PromptInput/PromptInputModeIndicator.tsx +93 -0
- package/src/components/PromptInput/PromptInputQueuedCommands.tsx +117 -0
- package/src/components/PromptInput/PromptInputStashNotice.tsx +25 -0
- package/src/components/PromptInput/SandboxPromptFooterHint.tsx +64 -0
- package/src/components/PromptInput/ShimmeredInput.tsx +143 -0
- package/src/components/PromptInput/VoiceIndicator.tsx +137 -0
- package/src/components/PromptInput/inputModes.ts +33 -0
- package/src/components/PromptInput/inputPaste.ts +90 -0
- package/src/components/PromptInput/useMaybeTruncateInput.ts +58 -0
- package/src/components/PromptInput/usePromptInputPlaceholder.ts +76 -0
- package/src/components/PromptInput/useShowFastIconHint.ts +31 -0
- package/src/components/PromptInput/useSwarmBanner.ts +155 -0
- package/src/components/PromptInput/utils.ts +60 -0
- package/src/components/QuickOpenDialog.tsx +244 -0
- package/src/components/RemoteCallout.tsx +76 -0
- package/src/components/RemoteEnvironmentDialog.tsx +340 -0
- package/src/components/ResumeTask.tsx +268 -0
- package/src/components/SandboxViolationExpandedView.tsx +99 -0
- package/src/components/ScrollKeybindingHandler.tsx +1012 -0
- package/src/components/SearchBox.tsx +72 -0
- package/src/components/SentryErrorBoundary.ts +28 -0
- package/src/components/SessionBackgroundHint.tsx +108 -0
- package/src/components/SessionPreview.tsx +194 -0
- package/src/components/Settings/Config.tsx +1822 -0
- package/src/components/Settings/Settings.tsx +137 -0
- package/src/components/Settings/Status.tsx +241 -0
- package/src/components/Settings/Usage.tsx +377 -0
- package/src/components/ShowInIDEPrompt.tsx +170 -0
- package/src/components/SkillImprovementSurvey.tsx +152 -0
- package/src/components/Spinner/FlashingChar.tsx +61 -0
- package/src/components/Spinner/GlimmerMessage.tsx +328 -0
- package/src/components/Spinner/ShimmerChar.tsx +36 -0
- package/src/components/Spinner/SpinnerAnimationRow.tsx +265 -0
- package/src/components/Spinner/SpinnerGlyph.tsx +80 -0
- package/src/components/Spinner/TeammateSpinnerLine.tsx +233 -0
- package/src/components/Spinner/TeammateSpinnerTree.tsx +272 -0
- package/src/components/Spinner/index.ts +10 -0
- package/src/components/Spinner/teammateSelectHint.ts +1 -0
- package/src/components/Spinner/useShimmerAnimation.ts +31 -0
- package/src/components/Spinner/useStalledAnimation.ts +75 -0
- package/src/components/Spinner/utils.ts +84 -0
- package/src/components/Spinner.tsx +562 -0
- package/src/components/Stats.tsx +1228 -0
- package/src/components/StatusLine.tsx +324 -0
- package/src/components/StatusNotices.tsx +55 -0
- package/src/components/StructuredDiff/Fallback.tsx +487 -0
- package/src/components/StructuredDiff/colorDiff.ts +37 -0
- package/src/components/StructuredDiff.tsx +190 -0
- package/src/components/StructuredDiffList.tsx +30 -0
- package/src/components/TagTabs.tsx +139 -0
- package/src/components/TaskListV2.tsx +378 -0
- package/src/components/TeammateViewHeader.tsx +82 -0
- package/src/components/TeleportError.tsx +189 -0
- package/src/components/TeleportProgress.tsx +140 -0
- package/src/components/TeleportRepoMismatchDialog.tsx +104 -0
- package/src/components/TeleportResumeWrapper.tsx +167 -0
- package/src/components/TeleportStash.tsx +116 -0
- package/src/components/TextInput.tsx +124 -0
- package/src/components/ThemePicker.tsx +333 -0
- package/src/components/ThinkingToggle.tsx +153 -0
- package/src/components/TokenWarning.tsx +179 -0
- package/src/components/ToolUseLoader.tsx +42 -0
- package/src/components/TrustDialog/TrustDialog.tsx +290 -0
- package/src/components/TrustDialog/utils.ts +245 -0
- package/src/components/ValidationErrorsList.tsx +148 -0
- package/src/components/VimTextInput.tsx +140 -0
- package/src/components/VirtualMessageList.tsx +1082 -0
- package/src/components/WorkflowMultiselectDialog.tsx +128 -0
- package/src/components/WorktreeExitDialog.tsx +231 -0
- package/src/components/agents/AgentDetail.tsx +220 -0
- package/src/components/agents/AgentEditor.tsx +178 -0
- package/src/components/agents/AgentNavigationFooter.tsx +26 -0
- package/src/components/agents/AgentsList.tsx +440 -0
- package/src/components/agents/AgentsMenu.tsx +800 -0
- package/src/components/agents/ColorPicker.tsx +112 -0
- package/src/components/agents/ModelSelector.tsx +68 -0
- package/src/components/agents/ToolSelector.tsx +562 -0
- package/src/components/agents/agentFileUtils.ts +272 -0
- package/src/components/agents/generateAgent.ts +197 -0
- package/src/components/agents/new-agent-creation/CreateAgentWizard.tsx +97 -0
- package/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx +84 -0
- package/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx +378 -0
- package/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx +74 -0
- package/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx +123 -0
- package/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx +143 -0
- package/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx +80 -0
- package/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx +113 -0
- package/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx +80 -0
- package/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx +52 -0
- package/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx +128 -0
- package/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx +61 -0
- package/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx +103 -0
- package/src/components/agents/types.ts +27 -0
- package/src/components/agents/utils.ts +18 -0
- package/src/components/agents/validateAgent.ts +109 -0
- package/src/components/design-system/Byline.tsx +77 -0
- package/src/components/design-system/Dialog.tsx +138 -0
- package/src/components/design-system/Divider.tsx +149 -0
- package/src/components/design-system/FuzzyPicker.tsx +312 -0
- package/src/components/design-system/KeyboardShortcutHint.tsx +81 -0
- package/src/components/design-system/ListItem.tsx +244 -0
- package/src/components/design-system/LoadingState.tsx +94 -0
- package/src/components/design-system/Pane.tsx +77 -0
- package/src/components/design-system/ProgressBar.tsx +86 -0
- package/src/components/design-system/Ratchet.tsx +80 -0
- package/src/components/design-system/StatusIcon.tsx +95 -0
- package/src/components/design-system/Tabs.tsx +340 -0
- package/src/components/design-system/ThemeProvider.tsx +174 -0
- package/src/components/design-system/ThemedBox.tsx +156 -0
- package/src/components/design-system/ThemedText.tsx +124 -0
- package/src/components/design-system/color.ts +30 -0
- package/src/components/diff/DiffDetailView.tsx +281 -0
- package/src/components/diff/DiffDialog.tsx +383 -0
- package/src/components/diff/DiffFileList.tsx +292 -0
- package/src/components/grove/Grove.tsx +463 -0
- package/src/components/hooks/HooksConfigMenu.tsx +578 -0
- package/src/components/hooks/PromptDialog.tsx +90 -0
- package/src/components/hooks/SelectEventMode.tsx +127 -0
- package/src/components/hooks/SelectHookMode.tsx +112 -0
- package/src/components/hooks/SelectMatcherMode.tsx +144 -0
- package/src/components/hooks/ViewHookMode.tsx +199 -0
- package/src/components/mcp/CapabilitiesSection.tsx +61 -0
- package/src/components/mcp/ElicitationDialog.tsx +1169 -0
- package/src/components/mcp/MCPAgentServerMenu.tsx +183 -0
- package/src/components/mcp/MCPListPanel.tsx +504 -0
- package/src/components/mcp/MCPReconnect.tsx +167 -0
- package/src/components/mcp/MCPRemoteServerMenu.tsx +649 -0
- package/src/components/mcp/MCPSettings.tsx +398 -0
- package/src/components/mcp/MCPStdioServerMenu.tsx +177 -0
- package/src/components/mcp/MCPToolDetailView.tsx +212 -0
- package/src/components/mcp/MCPToolListView.tsx +141 -0
- package/src/components/mcp/McpParsingWarnings.tsx +213 -0
- package/src/components/mcp/index.ts +9 -0
- package/src/components/mcp/utils/reconnectHelpers.tsx +49 -0
- package/src/components/memory/MemoryFileSelector.tsx +438 -0
- package/src/components/memory/MemoryUpdateNotification.tsx +45 -0
- package/src/components/messageActions.tsx +450 -0
- package/src/components/messages/AdvisorMessage.tsx +158 -0
- package/src/components/messages/AssistantRedactedThinkingMessage.tsx +31 -0
- package/src/components/messages/AssistantTextMessage.tsx +270 -0
- package/src/components/messages/AssistantThinkingMessage.tsx +86 -0
- package/src/components/messages/AssistantToolUseMessage.tsx +368 -0
- package/src/components/messages/AttachmentMessage.tsx +536 -0
- package/src/components/messages/CollapsedReadSearchContent.tsx +484 -0
- package/src/components/messages/CompactBoundaryMessage.tsx +18 -0
- package/src/components/messages/GroupedToolUseContent.tsx +58 -0
- package/src/components/messages/HighlightedThinkingText.tsx +162 -0
- package/src/components/messages/HookProgressMessage.tsx +116 -0
- package/src/components/messages/PlanApprovalMessage.tsx +222 -0
- package/src/components/messages/RateLimitMessage.tsx +161 -0
- package/src/components/messages/ShutdownMessage.tsx +132 -0
- package/src/components/messages/SystemAPIErrorMessage.tsx +141 -0
- package/src/components/messages/SystemTextMessage.tsx +827 -0
- package/src/components/messages/TaskAssignmentMessage.tsx +76 -0
- package/src/components/messages/UserAgentNotificationMessage.tsx +83 -0
- package/src/components/messages/UserBashInputMessage.tsx +58 -0
- package/src/components/messages/UserBashOutputMessage.tsx +54 -0
- package/src/components/messages/UserChannelMessage.tsx +137 -0
- package/src/components/messages/UserCommandMessage.tsx +108 -0
- package/src/components/messages/UserImageMessage.tsx +59 -0
- package/src/components/messages/UserLocalCommandOutputMessage.tsx +167 -0
- package/src/components/messages/UserMemoryInputMessage.tsx +75 -0
- package/src/components/messages/UserPlanMessage.tsx +42 -0
- package/src/components/messages/UserPromptMessage.tsx +80 -0
- package/src/components/messages/UserResourceUpdateMessage.tsx +121 -0
- package/src/components/messages/UserTeammateMessage.tsx +206 -0
- package/src/components/messages/UserTextMessage.tsx +275 -0
- package/src/components/messages/UserToolResultMessage/RejectedPlanMessage.tsx +31 -0
- package/src/components/messages/UserToolResultMessage/RejectedToolUseMessage.tsx +16 -0
- package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +16 -0
- package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +103 -0
- package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +95 -0
- package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +106 -0
- package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +104 -0
- package/src/components/messages/UserToolResultMessage/utils.tsx +44 -0
- package/src/components/messages/nullRenderingAttachments.ts +70 -0
- package/src/components/messages/teamMemCollapsed.tsx +140 -0
- package/src/components/messages/teamMemSaved.ts +19 -0
- package/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx +645 -0
- package/src/components/permissions/AskUserQuestionPermissionRequest/PreviewBox.tsx +229 -0
- package/src/components/permissions/AskUserQuestionPermissionRequest/PreviewQuestionView.tsx +328 -0
- package/src/components/permissions/AskUserQuestionPermissionRequest/QuestionNavigationBar.tsx +178 -0
- package/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx +465 -0
- package/src/components/permissions/AskUserQuestionPermissionRequest/SubmitQuestionsView.tsx +144 -0
- package/src/components/permissions/AskUserQuestionPermissionRequest/use-multiple-choice-state.ts +179 -0
- package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +482 -0
- package/src/components/permissions/BashPermissionRequest/bashToolUseOptions.tsx +147 -0
- package/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx +441 -0
- package/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx +122 -0
- package/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx +768 -0
- package/src/components/permissions/FallbackPermissionRequest.tsx +333 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
- package/src/components/permissions/FilePermissionDialog/FilePermissionDialog.tsx +204 -0
- package/src/components/permissions/FilePermissionDialog/ideDiffConfig.ts +42 -0
- package/src/components/permissions/FilePermissionDialog/permissionOptions.tsx +177 -0
- package/src/components/permissions/FilePermissionDialog/useFilePermissionDialog.ts +212 -0
- package/src/components/permissions/FilePermissionDialog/usePermissionHandler.ts +185 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +161 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +89 -0
- package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +115 -0
- package/src/components/permissions/NotebookEditPermissionRequest/NotebookEditPermissionRequest.tsx +166 -0
- package/src/components/permissions/NotebookEditPermissionRequest/NotebookEditToolDiff.tsx +235 -0
- package/src/components/permissions/PermissionDecisionDebugInfo.tsx +460 -0
- package/src/components/permissions/PermissionDialog.tsx +72 -0
- package/src/components/permissions/PermissionExplanation.tsx +272 -0
- package/src/components/permissions/PermissionPrompt.tsx +336 -0
- package/src/components/permissions/PermissionRequest.tsx +217 -0
- package/src/components/permissions/PermissionRequestTitle.tsx +66 -0
- package/src/components/permissions/PermissionRuleExplanation.tsx +121 -0
- package/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx +235 -0
- package/src/components/permissions/PowerShellPermissionRequest/powershellToolUseOptions.tsx +91 -0
- package/src/components/permissions/SandboxPermissionRequest.tsx +163 -0
- package/src/components/permissions/SedEditPermissionRequest/SedEditPermissionRequest.tsx +230 -0
- package/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx +369 -0
- package/src/components/permissions/WebFetchPermissionRequest/WebFetchPermissionRequest.tsx +258 -0
- package/src/components/permissions/WorkerBadge.tsx +49 -0
- package/src/components/permissions/WorkerPendingPermission.tsx +105 -0
- package/src/components/permissions/hooks.ts +209 -0
- package/src/components/permissions/rules/AddPermissionRules.tsx +180 -0
- package/src/components/permissions/rules/AddWorkspaceDirectory.tsx +340 -0
- package/src/components/permissions/rules/PermissionRuleDescription.tsx +76 -0
- package/src/components/permissions/rules/PermissionRuleInput.tsx +138 -0
- package/src/components/permissions/rules/PermissionRuleList.tsx +1179 -0
- package/src/components/permissions/rules/RecentDenialsTab.tsx +207 -0
- package/src/components/permissions/rules/RemoveWorkspaceDirectory.tsx +110 -0
- package/src/components/permissions/rules/WorkspaceTab.tsx +150 -0
- package/src/components/permissions/shellPermissionHelpers.tsx +164 -0
- package/src/components/permissions/useShellPermissionFeedback.ts +148 -0
- package/src/components/permissions/utils.ts +25 -0
- package/src/components/sandbox/SandboxConfigTab.tsx +45 -0
- package/src/components/sandbox/SandboxDependenciesTab.tsx +120 -0
- package/src/components/sandbox/SandboxDoctorSection.tsx +46 -0
- package/src/components/sandbox/SandboxOverridesTab.tsx +193 -0
- package/src/components/sandbox/SandboxSettings.tsx +296 -0
- package/src/components/shell/ExpandShellOutputContext.tsx +36 -0
- package/src/components/shell/OutputLine.tsx +118 -0
- package/src/components/shell/ShellProgressMessage.tsx +150 -0
- package/src/components/shell/ShellTimeDisplay.tsx +74 -0
- package/src/components/skills/SkillsMenu.tsx +237 -0
- package/src/components/tasks/AsyncAgentDetailDialog.tsx +229 -0
- package/src/components/tasks/BackgroundTask.tsx +345 -0
- package/src/components/tasks/BackgroundTaskStatus.tsx +429 -0
- package/src/components/tasks/BackgroundTasksDialog.tsx +652 -0
- package/src/components/tasks/DreamDetailDialog.tsx +251 -0
- package/src/components/tasks/InProcessTeammateDetailDialog.tsx +266 -0
- package/src/components/tasks/RemoteSessionDetailDialog.tsx +904 -0
- package/src/components/tasks/RemoteSessionProgress.tsx +243 -0
- package/src/components/tasks/ShellDetailDialog.tsx +404 -0
- package/src/components/tasks/ShellProgress.tsx +87 -0
- package/src/components/tasks/renderToolActivity.tsx +33 -0
- package/src/components/tasks/taskStatusUtils.tsx +107 -0
- package/src/components/teams/TeamStatus.tsx +80 -0
- package/src/components/teams/TeamsDialog.tsx +715 -0
- package/src/components/ui/OrderedList.tsx +71 -0
- package/src/components/ui/OrderedListItem.tsx +45 -0
- package/src/components/ui/TreeSelect.tsx +397 -0
- package/src/components/wizard/WizardDialogLayout.tsx +65 -0
- package/src/components/wizard/WizardNavigationFooter.tsx +24 -0
- package/src/components/wizard/WizardProvider.tsx +213 -0
- package/src/components/wizard/index.ts +9 -0
- package/src/components/wizard/useWizard.ts +13 -0
- package/src/constants/apiLimits.ts +94 -0
- package/src/constants/betas.ts +52 -0
- package/src/constants/common.ts +33 -0
- package/src/constants/cyberRiskInstruction.ts +24 -0
- package/src/constants/errorIds.ts +15 -0
- package/src/constants/figures.ts +45 -0
- package/src/constants/files.ts +156 -0
- package/src/constants/github-app.ts +144 -0
- package/src/constants/keys.ts +11 -0
- package/src/constants/messages.ts +1 -0
- package/src/constants/oauth.ts +234 -0
- package/src/constants/outputStyles.ts +216 -0
- package/src/constants/product.ts +76 -0
- package/src/constants/prompts.ts +914 -0
- package/src/constants/spinnerVerbs.ts +204 -0
- package/src/constants/system.ts +95 -0
- package/src/constants/systemPromptSections.ts +68 -0
- package/src/constants/toolLimits.ts +56 -0
- package/src/constants/tools.ts +112 -0
- package/src/constants/turnCompletionVerbs.ts +12 -0
- package/src/constants/xml.ts +86 -0
- package/src/context/QueuedMessageContext.tsx +63 -0
- package/src/context/fpsMetrics.tsx +30 -0
- package/src/context/mailbox.tsx +38 -0
- package/src/context/modalContext.tsx +58 -0
- package/src/context/notifications.tsx +240 -0
- package/src/context/overlayContext.tsx +151 -0
- package/src/context/promptOverlayContext.tsx +125 -0
- package/src/context/stats.tsx +220 -0
- package/src/context/voice.tsx +88 -0
- package/src/context.ts +189 -0
- package/src/coordinator/coordinatorMode.ts +369 -0
- package/src/cost-tracker.ts +323 -0
- package/src/costHook.ts +22 -0
- package/src/dialogLaunchers.tsx +133 -0
- package/src/entrypoints/agentSdkTypes.ts +443 -0
- package/src/entrypoints/cli.tsx +303 -0
- package/src/entrypoints/init.ts +340 -0
- package/src/entrypoints/mcp.ts +196 -0
- package/src/entrypoints/sandboxTypes.ts +156 -0
- package/src/entrypoints/sdk/controlSchemas.ts +663 -0
- package/src/entrypoints/sdk/coreSchemas.ts +1889 -0
- package/src/entrypoints/sdk/coreTypes.ts +62 -0
- package/src/history.ts +464 -0
- package/src/hooks/fileSuggestions.ts +811 -0
- package/src/hooks/notifs/useAutoModeUnavailableNotification.ts +56 -0
- package/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx +60 -0
- package/src/hooks/notifs/useDeprecationWarningNotification.tsx +44 -0
- package/src/hooks/notifs/useFastModeNotification.tsx +162 -0
- package/src/hooks/notifs/useIDEStatusIndicator.tsx +186 -0
- package/src/hooks/notifs/useInstallMessages.tsx +26 -0
- package/src/hooks/notifs/useLspInitializationNotification.tsx +143 -0
- package/src/hooks/notifs/useMcpConnectivityStatus.tsx +88 -0
- package/src/hooks/notifs/useModelMigrationNotifications.tsx +52 -0
- package/src/hooks/notifs/useNpmDeprecationNotification.tsx +25 -0
- package/src/hooks/notifs/usePluginAutoupdateNotification.tsx +83 -0
- package/src/hooks/notifs/usePluginInstallationStatus.tsx +128 -0
- package/src/hooks/notifs/useRateLimitWarningNotification.tsx +114 -0
- package/src/hooks/notifs/useSettingsErrors.tsx +69 -0
- package/src/hooks/notifs/useStartupNotification.ts +41 -0
- package/src/hooks/notifs/useTeammateShutdownNotification.ts +78 -0
- package/src/hooks/renderPlaceholder.ts +51 -0
- package/src/hooks/toolPermission/PermissionContext.ts +388 -0
- package/src/hooks/toolPermission/handlers/coordinatorHandler.ts +65 -0
- package/src/hooks/toolPermission/handlers/interactiveHandler.ts +536 -0
- package/src/hooks/toolPermission/handlers/swarmWorkerHandler.ts +159 -0
- package/src/hooks/toolPermission/permissionLogging.ts +238 -0
- package/src/hooks/unifiedSuggestions.ts +202 -0
- package/src/hooks/useAfterFirstRender.ts +17 -0
- package/src/hooks/useApiKeyVerification.ts +84 -0
- package/src/hooks/useArrowKeyHistory.tsx +229 -0
- package/src/hooks/useAssistantHistory.ts +250 -0
- package/src/hooks/useAwaySummary.ts +125 -0
- package/src/hooks/useBackgroundTaskNavigation.ts +251 -0
- package/src/hooks/useBlink.ts +34 -0
- package/src/hooks/useCanUseTool.tsx +204 -0
- package/src/hooks/useCancelRequest.ts +276 -0
- package/src/hooks/useChromeExtensionNotification.tsx +50 -0
- package/src/hooks/useClaudeCodeHintRecommendation.tsx +129 -0
- package/src/hooks/useClipboardImageHint.ts +77 -0
- package/src/hooks/useCommandKeybindings.tsx +108 -0
- package/src/hooks/useCommandQueue.ts +15 -0
- package/src/hooks/useCopyOnSelect.ts +98 -0
- package/src/hooks/useDeferredHookMessages.ts +46 -0
- package/src/hooks/useDiffData.ts +110 -0
- package/src/hooks/useDiffInIDE.ts +379 -0
- package/src/hooks/useDirectConnect.ts +229 -0
- package/src/hooks/useDoublePress.ts +62 -0
- package/src/hooks/useDynamicConfig.ts +22 -0
- package/src/hooks/useElapsedTime.ts +37 -0
- package/src/hooks/useExitOnCtrlCD.ts +95 -0
- package/src/hooks/useExitOnCtrlCDWithKeybindings.ts +24 -0
- package/src/hooks/useFileHistorySnapshotInit.ts +25 -0
- package/src/hooks/useGlobalKeybindings.tsx +249 -0
- package/src/hooks/useHistorySearch.ts +303 -0
- package/src/hooks/useIDEIntegration.tsx +70 -0
- package/src/hooks/useIdeAtMentioned.ts +76 -0
- package/src/hooks/useIdeConnectionStatus.ts +33 -0
- package/src/hooks/useIdeLogging.ts +41 -0
- package/src/hooks/useIdeSelection.ts +150 -0
- package/src/hooks/useInboxPoller.ts +969 -0
- package/src/hooks/useInputBuffer.ts +132 -0
- package/src/hooks/useIssueFlagBanner.ts +133 -0
- package/src/hooks/useLogMessages.ts +119 -0
- package/src/hooks/useLspPluginRecommendation.tsx +194 -0
- package/src/hooks/useMailboxBridge.ts +21 -0
- package/src/hooks/useMainLoopModel.ts +34 -0
- package/src/hooks/useManagePlugins.ts +304 -0
- package/src/hooks/useMemoryUsage.ts +39 -0
- package/src/hooks/useMergedClients.ts +23 -0
- package/src/hooks/useMergedCommands.ts +15 -0
- package/src/hooks/useMergedTools.ts +44 -0
- package/src/hooks/useMinDisplayTime.ts +35 -0
- package/src/hooks/useNotifyAfterTimeout.ts +65 -0
- package/src/hooks/useOfficialMarketplaceNotification.tsx +48 -0
- package/src/hooks/usePasteHandler.ts +285 -0
- package/src/hooks/usePluginRecommendationBase.tsx +105 -0
- package/src/hooks/usePrStatus.ts +106 -0
- package/src/hooks/usePromptSuggestion.ts +177 -0
- package/src/hooks/usePromptsFromClaudeInChrome.tsx +71 -0
- package/src/hooks/useQueueProcessor.ts +68 -0
- package/src/hooks/useRemoteSession.ts +605 -0
- package/src/hooks/useReplBridge.tsx +723 -0
- package/src/hooks/useSSHSession.ts +241 -0
- package/src/hooks/useScheduledTasks.ts +139 -0
- package/src/hooks/useSearchInput.ts +364 -0
- package/src/hooks/useSessionBackgrounding.ts +158 -0
- package/src/hooks/useSettings.ts +17 -0
- package/src/hooks/useSettingsChange.ts +25 -0
- package/src/hooks/useSkillImprovementSurvey.ts +105 -0
- package/src/hooks/useSkillsChange.ts +62 -0
- package/src/hooks/useSwarmInitialization.ts +81 -0
- package/src/hooks/useSwarmPermissionPoller.ts +330 -0
- package/src/hooks/useTaskListWatcher.ts +221 -0
- package/src/hooks/useTasksV2.ts +250 -0
- package/src/hooks/useTeammateViewAutoExit.ts +63 -0
- package/src/hooks/useTeleportResume.tsx +85 -0
- package/src/hooks/useTerminalSize.ts +15 -0
- package/src/hooks/useTextInput.ts +529 -0
- package/src/hooks/useTimeout.ts +14 -0
- package/src/hooks/useTurnDiffs.ts +213 -0
- package/src/hooks/useTypeahead.tsx +1385 -0
- package/src/hooks/useUpdateNotification.ts +34 -0
- package/src/hooks/useVimInput.ts +316 -0
- package/src/hooks/useVirtualScroll.ts +721 -0
- package/src/hooks/useVoice.ts +1144 -0
- package/src/hooks/useVoiceEnabled.ts +25 -0
- package/src/hooks/useVoiceIntegration.tsx +677 -0
- package/src/ink/Ansi.tsx +292 -0
- package/src/ink/bidi.ts +139 -0
- package/src/ink/clearTerminal.ts +74 -0
- package/src/ink/colorize.ts +231 -0
- package/src/ink/components/AlternateScreen.tsx +80 -0
- package/src/ink/components/App.tsx +659 -0
- package/src/ink/components/AppContext.ts +21 -0
- package/src/ink/components/Box.tsx +214 -0
- package/src/ink/components/Button.tsx +192 -0
- package/src/ink/components/ClockContext.tsx +112 -0
- package/src/ink/components/CursorDeclarationContext.ts +32 -0
- package/src/ink/components/ErrorOverview.tsx +109 -0
- package/src/ink/components/Link.tsx +42 -0
- package/src/ink/components/Newline.tsx +39 -0
- package/src/ink/components/NoSelect.tsx +68 -0
- package/src/ink/components/RawAnsi.tsx +57 -0
- package/src/ink/components/ScrollBox.tsx +237 -0
- package/src/ink/components/Spacer.tsx +20 -0
- package/src/ink/components/StdinContext.ts +49 -0
- package/src/ink/components/TerminalFocusContext.tsx +52 -0
- package/src/ink/components/TerminalSizeContext.tsx +7 -0
- package/src/ink/components/Text.tsx +254 -0
- package/src/ink/constants.ts +2 -0
- package/src/ink/dom.ts +484 -0
- package/src/ink/events/click-event.ts +38 -0
- package/src/ink/events/dispatcher.ts +233 -0
- package/src/ink/events/emitter.ts +39 -0
- package/src/ink/events/event-handlers.ts +73 -0
- package/src/ink/events/event.ts +11 -0
- package/src/ink/events/focus-event.ts +21 -0
- package/src/ink/events/input-event.ts +205 -0
- package/src/ink/events/keyboard-event.ts +51 -0
- package/src/ink/events/terminal-event.ts +107 -0
- package/src/ink/events/terminal-focus-event.ts +19 -0
- package/src/ink/focus.ts +181 -0
- package/src/ink/frame.ts +124 -0
- package/src/ink/get-max-width.ts +27 -0
- package/src/ink/hit-test.ts +130 -0
- package/src/ink/hooks/use-animation-frame.ts +57 -0
- package/src/ink/hooks/use-app.ts +8 -0
- package/src/ink/hooks/use-declared-cursor.ts +73 -0
- package/src/ink/hooks/use-input.ts +92 -0
- package/src/ink/hooks/use-interval.ts +67 -0
- package/src/ink/hooks/use-search-highlight.ts +53 -0
- package/src/ink/hooks/use-selection.ts +104 -0
- package/src/ink/hooks/use-stdin.ts +8 -0
- package/src/ink/hooks/use-tab-status.ts +72 -0
- package/src/ink/hooks/use-terminal-focus.ts +16 -0
- package/src/ink/hooks/use-terminal-title.ts +31 -0
- package/src/ink/hooks/use-terminal-viewport.ts +96 -0
- package/src/ink/ink.tsx +1728 -0
- package/src/ink/instances.ts +10 -0
- package/src/ink/layout/engine.ts +6 -0
- package/src/ink/layout/geometry.ts +97 -0
- package/src/ink/layout/node.ts +152 -0
- package/src/ink/layout/yoga.ts +308 -0
- package/src/ink/line-width-cache.ts +24 -0
- package/src/ink/log-update.ts +773 -0
- package/src/ink/measure-element.ts +23 -0
- package/src/ink/measure-text.ts +47 -0
- package/src/ink/node-cache.ts +54 -0
- package/src/ink/optimizer.ts +93 -0
- package/src/ink/output.ts +797 -0
- package/src/ink/parse-keypress.ts +801 -0
- package/src/ink/reconciler.ts +512 -0
- package/src/ink/render-border.ts +231 -0
- package/src/ink/render-node-to-output.ts +1462 -0
- package/src/ink/render-to-screen.ts +231 -0
- package/src/ink/renderer.ts +178 -0
- package/src/ink/root.ts +184 -0
- package/src/ink/screen.ts +1486 -0
- package/src/ink/searchHighlight.ts +93 -0
- package/src/ink/selection.ts +917 -0
- package/src/ink/squash-text-nodes.ts +92 -0
- package/src/ink/stringWidth.ts +222 -0
- package/src/ink/styles.ts +771 -0
- package/src/ink/supports-hyperlinks.ts +57 -0
- package/src/ink/tabstops.ts +46 -0
- package/src/ink/terminal-focus-state.ts +47 -0
- package/src/ink/terminal-querier.ts +212 -0
- package/src/ink/terminal.ts +248 -0
- package/src/ink/termio/ansi.ts +75 -0
- package/src/ink/termio/csi.ts +319 -0
- package/src/ink/termio/dec.ts +60 -0
- package/src/ink/termio/esc.ts +67 -0
- package/src/ink/termio/osc.ts +493 -0
- package/src/ink/termio/parser.ts +394 -0
- package/src/ink/termio/sgr.ts +308 -0
- package/src/ink/termio/tokenize.ts +319 -0
- package/src/ink/termio/types.ts +236 -0
- package/src/ink/termio.ts +42 -0
- package/src/ink/useTerminalNotification.ts +126 -0
- package/src/ink/warn.ts +9 -0
- package/src/ink/widest-line.ts +19 -0
- package/src/ink/wrap-text.ts +74 -0
- package/src/ink/wrapAnsi.ts +20 -0
- package/src/ink.ts +85 -0
- package/src/interactiveHelpers.tsx +367 -0
- package/src/keybindings/KeybindingContext.tsx +243 -0
- package/src/keybindings/KeybindingProviderSetup.tsx +308 -0
- package/src/keybindings/defaultBindings.ts +340 -0
- package/src/keybindings/loadUserBindings.ts +472 -0
- package/src/keybindings/match.ts +120 -0
- package/src/keybindings/parser.ts +203 -0
- package/src/keybindings/reservedShortcuts.ts +127 -0
- package/src/keybindings/resolver.ts +244 -0
- package/src/keybindings/schema.ts +236 -0
- package/src/keybindings/shortcutFormat.ts +63 -0
- package/src/keybindings/template.ts +52 -0
- package/src/keybindings/useKeybinding.ts +196 -0
- package/src/keybindings/useShortcutDisplay.ts +59 -0
- package/src/keybindings/validate.ts +498 -0
- package/src/main.tsx +4684 -0
- package/src/memdir/findRelevantMemories.ts +141 -0
- package/src/memdir/memdir.ts +507 -0
- package/src/memdir/memoryAge.ts +53 -0
- package/src/memdir/memoryScan.ts +94 -0
- package/src/memdir/memoryTypes.ts +271 -0
- package/src/memdir/paths.ts +278 -0
- package/src/memdir/teamMemPaths.ts +292 -0
- package/src/memdir/teamMemPrompts.ts +100 -0
- package/src/migrations/migrateAutoUpdatesToSettings.ts +61 -0
- package/src/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
- package/src/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
- package/src/migrations/migrateFennecToOpus.ts +45 -0
- package/src/migrations/migrateLegacyOpusToCurrent.ts +57 -0
- package/src/migrations/migrateOpusToOpus1m.ts +43 -0
- package/src/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
- package/src/migrations/migrateSonnet1mToSonnet45.ts +48 -0
- package/src/migrations/migrateSonnet45ToSonnet46.ts +67 -0
- package/src/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
- package/src/migrations/resetProToOpusDefault.ts +51 -0
- package/src/moreright/useMoreRight.tsx +26 -0
- package/src/native-ts/color-diff/index.ts +999 -0
- package/src/native-ts/file-index/index.ts +370 -0
- package/src/native-ts/yoga-layout/enums.ts +134 -0
- package/src/native-ts/yoga-layout/index.ts +2578 -0
- package/src/outputStyles/loadOutputStylesDir.ts +98 -0
- package/src/plugins/builtinPlugins.ts +159 -0
- package/src/plugins/bundled/index.ts +23 -0
- package/src/projectOnboardingState.ts +83 -0
- package/src/query/config.ts +46 -0
- package/src/query/deps.ts +40 -0
- package/src/query/stopHooks.ts +473 -0
- package/src/query/tokenBudget.ts +93 -0
- package/src/query.ts +1729 -0
- package/src/remote/RemoteSessionManager.ts +343 -0
- package/src/remote/SessionsWebSocket.ts +404 -0
- package/src/remote/remotePermissionBridge.ts +78 -0
- package/src/remote/sdkMessageAdapter.ts +302 -0
- package/src/replLauncher.tsx +23 -0
- package/src/schemas/hooks.ts +222 -0
- package/src/screens/Doctor.tsx +575 -0
- package/src/screens/REPL.tsx +5006 -0
- package/src/screens/ResumeConversation.tsx +399 -0
- package/src/server/createDirectConnectSession.ts +88 -0
- package/src/server/directConnectManager.ts +213 -0
- package/src/server/types.ts +57 -0
- package/src/services/AgentSummary/agentSummary.ts +179 -0
- package/src/services/MagicDocs/magicDocs.ts +254 -0
- package/src/services/MagicDocs/prompts.ts +127 -0
- package/src/services/PromptSuggestion/promptSuggestion.ts +523 -0
- package/src/services/PromptSuggestion/speculation.ts +991 -0
- package/src/services/SessionMemory/prompts.ts +324 -0
- package/src/services/SessionMemory/sessionMemory.ts +495 -0
- package/src/services/SessionMemory/sessionMemoryUtils.ts +207 -0
- package/src/services/analytics/config.ts +38 -0
- package/src/services/analytics/datadog.ts +307 -0
- package/src/services/analytics/firstPartyEventLogger.ts +449 -0
- package/src/services/analytics/firstPartyEventLoggingExporter.ts +806 -0
- package/src/services/analytics/growthbook.ts +1157 -0
- package/src/services/analytics/index.ts +173 -0
- package/src/services/analytics/metadata.ts +973 -0
- package/src/services/analytics/sink.ts +114 -0
- package/src/services/analytics/sinkKillswitch.ts +25 -0
- package/src/services/api/adminRequests.ts +119 -0
- package/src/services/api/bootstrap.ts +142 -0
- package/src/services/api/claude.ts +3433 -0
- package/src/services/api/client.ts +395 -0
- package/src/services/api/dumpPrompts.ts +226 -0
- package/src/services/api/emptyUsage.ts +22 -0
- package/src/services/api/errorUtils.ts +260 -0
- package/src/services/api/errors.ts +1207 -0
- package/src/services/api/filesApi.ts +748 -0
- package/src/services/api/firstTokenDate.ts +60 -0
- package/src/services/api/grove.ts +357 -0
- package/src/services/api/logging.ts +788 -0
- package/src/services/api/metricsOptOut.ts +159 -0
- package/src/services/api/overageCreditGrant.ts +137 -0
- package/src/services/api/promptCacheBreakDetection.ts +727 -0
- package/src/services/api/referral.ts +281 -0
- package/src/services/api/sessionIngress.ts +514 -0
- package/src/services/api/ultrareviewQuota.ts +38 -0
- package/src/services/api/usage.ts +63 -0
- package/src/services/api/withRetry.ts +826 -0
- package/src/services/autoDream/autoDream.ts +324 -0
- package/src/services/autoDream/config.ts +21 -0
- package/src/services/autoDream/consolidationLock.ts +140 -0
- package/src/services/autoDream/consolidationPrompt.ts +65 -0
- package/src/services/awaySummary.ts +74 -0
- package/src/services/claudeAiLimits.ts +515 -0
- package/src/services/claudeAiLimitsHook.ts +23 -0
- package/src/services/compact/apiMicrocompact.ts +153 -0
- package/src/services/compact/autoCompact.ts +351 -0
- package/src/services/compact/compact.ts +1705 -0
- package/src/services/compact/compactWarningHook.ts +16 -0
- package/src/services/compact/compactWarningState.ts +18 -0
- package/src/services/compact/grouping.ts +63 -0
- package/src/services/compact/microCompact.ts +530 -0
- package/src/services/compact/postCompactCleanup.ts +77 -0
- package/src/services/compact/prompt.ts +374 -0
- package/src/services/compact/sessionMemoryCompact.ts +630 -0
- package/src/services/compact/timeBasedMCConfig.ts +43 -0
- package/src/services/diagnosticTracking.ts +397 -0
- package/src/services/extractMemories/extractMemories.ts +615 -0
- package/src/services/extractMemories/prompts.ts +154 -0
- package/src/services/internalLogging.ts +90 -0
- package/src/services/lsp/LSPClient.ts +447 -0
- package/src/services/lsp/LSPDiagnosticRegistry.ts +386 -0
- package/src/services/lsp/LSPServerInstance.ts +511 -0
- package/src/services/lsp/LSPServerManager.ts +420 -0
- package/src/services/lsp/config.ts +79 -0
- package/src/services/lsp/manager.ts +289 -0
- package/src/services/lsp/passiveFeedback.ts +328 -0
- package/src/services/mcp/InProcessTransport.ts +63 -0
- package/src/services/mcp/MCPConnectionManager.tsx +73 -0
- package/src/services/mcp/SdkControlTransport.ts +136 -0
- package/src/services/mcp/auth.ts +2465 -0
- package/src/services/mcp/channelAllowlist.ts +76 -0
- package/src/services/mcp/channelNotification.ts +316 -0
- package/src/services/mcp/channelPermissions.ts +240 -0
- package/src/services/mcp/claudeai.ts +164 -0
- package/src/services/mcp/client.ts +3348 -0
- package/src/services/mcp/config.ts +1578 -0
- package/src/services/mcp/elicitationHandler.ts +313 -0
- package/src/services/mcp/envExpansion.ts +38 -0
- package/src/services/mcp/headersHelper.ts +138 -0
- package/src/services/mcp/mcpStringUtils.ts +106 -0
- package/src/services/mcp/normalization.ts +23 -0
- package/src/services/mcp/oauthPort.ts +78 -0
- package/src/services/mcp/officialRegistry.ts +72 -0
- package/src/services/mcp/types.ts +258 -0
- package/src/services/mcp/useManageMCPConnections.ts +1141 -0
- package/src/services/mcp/utils.ts +575 -0
- package/src/services/mcp/vscodeSdkMcp.ts +112 -0
- package/src/services/mcp/xaa.ts +511 -0
- package/src/services/mcp/xaaIdpLogin.ts +487 -0
- package/src/services/mcpServerApproval.tsx +41 -0
- package/src/services/mockRateLimits.ts +882 -0
- package/src/services/notifier.ts +156 -0
- package/src/services/oauth/auth-code-listener.ts +211 -0
- package/src/services/oauth/client.ts +566 -0
- package/src/services/oauth/crypto.ts +23 -0
- package/src/services/oauth/getOauthProfile.ts +53 -0
- package/src/services/oauth/index.ts +198 -0
- package/src/services/plugins/PluginInstallationManager.ts +184 -0
- package/src/services/plugins/pluginCliCommands.ts +344 -0
- package/src/services/plugins/pluginOperations.ts +1088 -0
- package/src/services/policyLimits/index.ts +664 -0
- package/src/services/policyLimits/types.ts +27 -0
- package/src/services/preventSleep.ts +165 -0
- package/src/services/rateLimitMessages.ts +344 -0
- package/src/services/rateLimitMocking.ts +144 -0
- package/src/services/remoteManagedSettings/index.ts +639 -0
- package/src/services/remoteManagedSettings/securityCheck.tsx +74 -0
- package/src/services/remoteManagedSettings/syncCache.ts +112 -0
- package/src/services/remoteManagedSettings/syncCacheState.ts +96 -0
- package/src/services/remoteManagedSettings/types.ts +31 -0
- package/src/services/settingsSync/index.ts +581 -0
- package/src/services/settingsSync/types.ts +67 -0
- package/src/services/teamMemorySync/index.ts +1256 -0
- package/src/services/teamMemorySync/secretScanner.ts +324 -0
- package/src/services/teamMemorySync/teamMemSecretGuard.ts +44 -0
- package/src/services/teamMemorySync/types.ts +156 -0
- package/src/services/teamMemorySync/watcher.ts +387 -0
- package/src/services/tips/tipHistory.ts +17 -0
- package/src/services/tips/tipRegistry.ts +686 -0
- package/src/services/tips/tipScheduler.ts +58 -0
- package/src/services/tokenEstimation.ts +495 -0
- package/src/services/toolUseSummary/toolUseSummaryGenerator.ts +112 -0
- package/src/services/tools/StreamingToolExecutor.ts +530 -0
- package/src/services/tools/toolExecution.ts +1745 -0
- package/src/services/tools/toolHooks.ts +650 -0
- package/src/services/tools/toolOrchestration.ts +188 -0
- package/src/services/vcr.ts +406 -0
- package/src/services/voice.ts +525 -0
- package/src/services/voiceKeyterms.ts +106 -0
- package/src/services/voiceStreamSTT.ts +544 -0
- package/src/setup.ts +477 -0
- package/src/skills/bundled/batch.ts +124 -0
- package/src/skills/bundled/claudeApi.ts +196 -0
- package/src/skills/bundled/claudeApiContent.ts +75 -0
- package/src/skills/bundled/claudeInChrome.ts +34 -0
- package/src/skills/bundled/debug.ts +103 -0
- package/src/skills/bundled/index.ts +79 -0
- package/src/skills/bundled/keybindings.ts +339 -0
- package/src/skills/bundled/loop.ts +92 -0
- package/src/skills/bundled/loremIpsum.ts +282 -0
- package/src/skills/bundled/remember.ts +82 -0
- package/src/skills/bundled/scheduleRemoteAgents.ts +447 -0
- package/src/skills/bundled/simplify.ts +69 -0
- package/src/skills/bundled/skillify.ts +197 -0
- package/src/skills/bundled/stuck.ts +79 -0
- package/src/skills/bundled/updateConfig.ts +475 -0
- package/src/skills/bundled/verify.ts +30 -0
- package/src/skills/bundled/verifyContent.ts +13 -0
- package/src/skills/bundledSkills.ts +220 -0
- package/src/skills/loadSkillsDir.ts +1086 -0
- package/src/skills/mcpSkillBuilders.ts +44 -0
- package/src/state/AppState.tsx +200 -0
- package/src/state/AppStateStore.ts +569 -0
- package/src/state/onChangeAppState.ts +171 -0
- package/src/state/selectors.ts +76 -0
- package/src/state/store.ts +34 -0
- package/src/state/teammateViewHelpers.ts +141 -0
- package/src/tasks/DreamTask/DreamTask.ts +157 -0
- package/src/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
- package/src/tasks/InProcessTeammateTask/types.ts +121 -0
- package/src/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
- package/src/tasks/LocalMainSessionTask.ts +479 -0
- package/src/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
- package/src/tasks/LocalShellTask/guards.ts +41 -0
- package/src/tasks/LocalShellTask/killShellTasks.ts +76 -0
- package/src/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
- package/src/tasks/pillLabel.ts +82 -0
- package/src/tasks/stopTask.ts +100 -0
- package/src/tasks/types.ts +46 -0
- package/src/tasks.ts +39 -0
- package/src/tools/AgentTool/AgentTool.tsx +1398 -0
- package/src/tools/AgentTool/UI.tsx +872 -0
- package/src/tools/AgentTool/agentColorManager.ts +66 -0
- package/src/tools/AgentTool/agentDisplay.ts +104 -0
- package/src/tools/AgentTool/agentMemory.ts +177 -0
- package/src/tools/AgentTool/agentMemorySnapshot.ts +197 -0
- package/src/tools/AgentTool/agentToolUtils.ts +686 -0
- package/src/tools/AgentTool/built-in/claudeCodeGuideAgent.ts +205 -0
- package/src/tools/AgentTool/built-in/exploreAgent.ts +83 -0
- package/src/tools/AgentTool/built-in/generalPurposeAgent.ts +34 -0
- package/src/tools/AgentTool/built-in/planAgent.ts +92 -0
- package/src/tools/AgentTool/built-in/statuslineSetup.ts +144 -0
- package/src/tools/AgentTool/built-in/verificationAgent.ts +152 -0
- package/src/tools/AgentTool/builtInAgents.ts +72 -0
- package/src/tools/AgentTool/constants.ts +12 -0
- package/src/tools/AgentTool/forkSubagent.ts +210 -0
- package/src/tools/AgentTool/loadAgentsDir.ts +755 -0
- package/src/tools/AgentTool/prompt.ts +287 -0
- package/src/tools/AgentTool/resumeAgent.ts +265 -0
- package/src/tools/AgentTool/runAgent.ts +973 -0
- package/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx +266 -0
- package/src/tools/AskUserQuestionTool/prompt.ts +44 -0
- package/src/tools/BashTool/BashTool.tsx +1144 -0
- package/src/tools/BashTool/BashToolResultMessage.tsx +191 -0
- package/src/tools/BashTool/UI.tsx +185 -0
- package/src/tools/BashTool/bashCommandHelpers.ts +265 -0
- package/src/tools/BashTool/bashPermissions.ts +2621 -0
- package/src/tools/BashTool/bashSecurity.ts +2592 -0
- package/src/tools/BashTool/commandSemantics.ts +140 -0
- package/src/tools/BashTool/commentLabel.ts +13 -0
- package/src/tools/BashTool/destructiveCommandWarning.ts +102 -0
- package/src/tools/BashTool/modeValidation.ts +115 -0
- package/src/tools/BashTool/pathValidation.ts +1303 -0
- package/src/tools/BashTool/prompt.ts +369 -0
- package/src/tools/BashTool/readOnlyValidation.ts +1990 -0
- package/src/tools/BashTool/sedEditParser.ts +322 -0
- package/src/tools/BashTool/sedValidation.ts +684 -0
- package/src/tools/BashTool/shouldUseSandbox.ts +153 -0
- package/src/tools/BashTool/toolName.ts +2 -0
- package/src/tools/BashTool/utils.ts +223 -0
- package/src/tools/BriefTool/BriefTool.ts +204 -0
- package/src/tools/BriefTool/UI.tsx +101 -0
- package/src/tools/BriefTool/attachments.ts +110 -0
- package/src/tools/BriefTool/prompt.ts +22 -0
- package/src/tools/BriefTool/upload.ts +174 -0
- package/src/tools/ConfigTool/ConfigTool.ts +467 -0
- package/src/tools/ConfigTool/UI.tsx +38 -0
- package/src/tools/ConfigTool/constants.ts +1 -0
- package/src/tools/ConfigTool/prompt.ts +93 -0
- package/src/tools/ConfigTool/supportedSettings.ts +211 -0
- package/src/tools/EnterPlanModeTool/EnterPlanModeTool.ts +126 -0
- package/src/tools/EnterPlanModeTool/UI.tsx +33 -0
- package/src/tools/EnterPlanModeTool/constants.ts +1 -0
- package/src/tools/EnterPlanModeTool/prompt.ts +170 -0
- package/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts +127 -0
- package/src/tools/EnterWorktreeTool/UI.tsx +20 -0
- package/src/tools/EnterWorktreeTool/constants.ts +1 -0
- package/src/tools/EnterWorktreeTool/prompt.ts +30 -0
- package/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts +493 -0
- package/src/tools/ExitPlanModeTool/UI.tsx +82 -0
- package/src/tools/ExitPlanModeTool/constants.ts +2 -0
- package/src/tools/ExitPlanModeTool/prompt.ts +29 -0
- package/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts +329 -0
- package/src/tools/ExitWorktreeTool/UI.tsx +25 -0
- package/src/tools/ExitWorktreeTool/constants.ts +1 -0
- package/src/tools/ExitWorktreeTool/prompt.ts +32 -0
- package/src/tools/FileEditTool/FileEditTool.ts +625 -0
- package/src/tools/FileEditTool/UI.tsx +289 -0
- package/src/tools/FileEditTool/constants.ts +11 -0
- package/src/tools/FileEditTool/prompt.ts +28 -0
- package/src/tools/FileEditTool/types.ts +85 -0
- package/src/tools/FileEditTool/utils.ts +775 -0
- package/src/tools/FileReadTool/FileReadTool.ts +1183 -0
- package/src/tools/FileReadTool/UI.tsx +185 -0
- package/src/tools/FileReadTool/imageProcessor.ts +94 -0
- package/src/tools/FileReadTool/limits.ts +92 -0
- package/src/tools/FileReadTool/prompt.ts +49 -0
- package/src/tools/FileWriteTool/FileWriteTool.ts +434 -0
- package/src/tools/FileWriteTool/UI.tsx +405 -0
- package/src/tools/FileWriteTool/prompt.ts +18 -0
- package/src/tools/GlobTool/GlobTool.ts +198 -0
- package/src/tools/GlobTool/UI.tsx +63 -0
- package/src/tools/GlobTool/prompt.ts +7 -0
- package/src/tools/GrepTool/GrepTool.ts +577 -0
- package/src/tools/GrepTool/UI.tsx +201 -0
- package/src/tools/GrepTool/prompt.ts +18 -0
- package/src/tools/LSPTool/LSPTool.ts +860 -0
- package/src/tools/LSPTool/UI.tsx +228 -0
- package/src/tools/LSPTool/formatters.ts +592 -0
- package/src/tools/LSPTool/prompt.ts +21 -0
- package/src/tools/LSPTool/schemas.ts +215 -0
- package/src/tools/LSPTool/symbolContext.ts +90 -0
- package/src/tools/ListMcpResourcesTool/ListMcpResourcesTool.ts +123 -0
- package/src/tools/ListMcpResourcesTool/UI.tsx +29 -0
- package/src/tools/ListMcpResourcesTool/prompt.ts +20 -0
- package/src/tools/MCPTool/MCPTool.ts +77 -0
- package/src/tools/MCPTool/UI.tsx +403 -0
- package/src/tools/MCPTool/classifyForCollapse.ts +604 -0
- package/src/tools/MCPTool/prompt.ts +3 -0
- package/src/tools/McpAuthTool/McpAuthTool.ts +215 -0
- package/src/tools/NotebookEditTool/NotebookEditTool.ts +490 -0
- package/src/tools/NotebookEditTool/UI.tsx +93 -0
- package/src/tools/NotebookEditTool/constants.ts +2 -0
- package/src/tools/NotebookEditTool/prompt.ts +3 -0
- package/src/tools/PowerShellTool/PowerShellTool.tsx +1001 -0
- package/src/tools/PowerShellTool/UI.tsx +131 -0
- package/src/tools/PowerShellTool/clmTypes.ts +211 -0
- package/src/tools/PowerShellTool/commandSemantics.ts +142 -0
- package/src/tools/PowerShellTool/commonParameters.ts +30 -0
- package/src/tools/PowerShellTool/destructiveCommandWarning.ts +109 -0
- package/src/tools/PowerShellTool/gitSafety.ts +176 -0
- package/src/tools/PowerShellTool/modeValidation.ts +404 -0
- package/src/tools/PowerShellTool/pathValidation.ts +2049 -0
- package/src/tools/PowerShellTool/powershellPermissions.ts +1648 -0
- package/src/tools/PowerShellTool/powershellSecurity.ts +1090 -0
- package/src/tools/PowerShellTool/prompt.ts +145 -0
- package/src/tools/PowerShellTool/readOnlyValidation.ts +1823 -0
- package/src/tools/PowerShellTool/toolName.ts +2 -0
- package/src/tools/REPLTool/constants.ts +46 -0
- package/src/tools/REPLTool/primitiveTools.ts +39 -0
- package/src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts +158 -0
- package/src/tools/ReadMcpResourceTool/UI.tsx +37 -0
- package/src/tools/ReadMcpResourceTool/prompt.ts +16 -0
- package/src/tools/RemoteTriggerTool/RemoteTriggerTool.ts +161 -0
- package/src/tools/RemoteTriggerTool/UI.tsx +17 -0
- package/src/tools/RemoteTriggerTool/prompt.ts +15 -0
- package/src/tools/ScheduleCronTool/CronCreateTool.ts +157 -0
- package/src/tools/ScheduleCronTool/CronDeleteTool.ts +95 -0
- package/src/tools/ScheduleCronTool/CronListTool.ts +97 -0
- package/src/tools/ScheduleCronTool/UI.tsx +60 -0
- package/src/tools/ScheduleCronTool/prompt.ts +135 -0
- package/src/tools/SendMessageTool/SendMessageTool.ts +917 -0
- package/src/tools/SendMessageTool/UI.tsx +31 -0
- package/src/tools/SendMessageTool/constants.ts +1 -0
- package/src/tools/SendMessageTool/prompt.ts +49 -0
- package/src/tools/SkillTool/SkillTool.ts +1108 -0
- package/src/tools/SkillTool/UI.tsx +128 -0
- package/src/tools/SkillTool/constants.ts +1 -0
- package/src/tools/SkillTool/prompt.ts +241 -0
- package/src/tools/SleepTool/prompt.ts +17 -0
- package/src/tools/SyntheticOutputTool/SyntheticOutputTool.ts +163 -0
- package/src/tools/TaskCreateTool/TaskCreateTool.ts +138 -0
- package/src/tools/TaskCreateTool/constants.ts +1 -0
- package/src/tools/TaskCreateTool/prompt.ts +56 -0
- package/src/tools/TaskGetTool/TaskGetTool.ts +128 -0
- package/src/tools/TaskGetTool/constants.ts +1 -0
- package/src/tools/TaskGetTool/prompt.ts +24 -0
- package/src/tools/TaskListTool/TaskListTool.ts +116 -0
- package/src/tools/TaskListTool/constants.ts +1 -0
- package/src/tools/TaskListTool/prompt.ts +49 -0
- package/src/tools/TaskOutputTool/TaskOutputTool.tsx +584 -0
- package/src/tools/TaskOutputTool/constants.ts +1 -0
- package/src/tools/TaskStopTool/TaskStopTool.ts +131 -0
- package/src/tools/TaskStopTool/UI.tsx +41 -0
- package/src/tools/TaskStopTool/prompt.ts +8 -0
- package/src/tools/TaskUpdateTool/TaskUpdateTool.ts +406 -0
- package/src/tools/TaskUpdateTool/constants.ts +1 -0
- package/src/tools/TaskUpdateTool/prompt.ts +77 -0
- package/src/tools/TeamCreateTool/TeamCreateTool.ts +240 -0
- package/src/tools/TeamCreateTool/UI.tsx +6 -0
- package/src/tools/TeamCreateTool/constants.ts +1 -0
- package/src/tools/TeamCreateTool/prompt.ts +113 -0
- package/src/tools/TeamDeleteTool/TeamDeleteTool.ts +139 -0
- package/src/tools/TeamDeleteTool/UI.tsx +20 -0
- package/src/tools/TeamDeleteTool/constants.ts +1 -0
- package/src/tools/TeamDeleteTool/prompt.ts +16 -0
- package/src/tools/TodoWriteTool/TodoWriteTool.ts +115 -0
- package/src/tools/TodoWriteTool/constants.ts +1 -0
- package/src/tools/TodoWriteTool/prompt.ts +184 -0
- package/src/tools/ToolSearchTool/ToolSearchTool.ts +471 -0
- package/src/tools/ToolSearchTool/constants.ts +1 -0
- package/src/tools/ToolSearchTool/prompt.ts +121 -0
- package/src/tools/TungstenTool/TungstenTool.js +2 -0
- package/src/tools/TungstenTool/TungstenTool.ts +1 -0
- package/src/tools/WebFetchTool/UI.tsx +72 -0
- package/src/tools/WebFetchTool/WebFetchTool.ts +318 -0
- package/src/tools/WebFetchTool/preapproved.ts +166 -0
- package/src/tools/WebFetchTool/prompt.ts +46 -0
- package/src/tools/WebFetchTool/utils.ts +530 -0
- package/src/tools/WebSearchTool/UI.tsx +101 -0
- package/src/tools/WebSearchTool/WebSearchTool.ts +435 -0
- package/src/tools/WebSearchTool/prompt.ts +34 -0
- package/src/tools/shared/gitOperationTracking.ts +277 -0
- package/src/tools/shared/spawnMultiAgent.ts +1093 -0
- package/src/tools/testing/TestingPermissionTool.tsx +74 -0
- package/src/tools/utils.ts +40 -0
- package/src/tools.ts +389 -0
- package/src/types/command.ts +216 -0
- package/src/types/connectorText.js +5 -0
- package/src/types/connectorText.ts +1 -0
- package/src/types/generated/events_mono/claude_code/v1/claude_code_internal_event.ts +865 -0
- package/src/types/generated/events_mono/common/v1/auth.ts +100 -0
- package/src/types/generated/events_mono/growthbook/v1/growthbook_experiment_event.ts +223 -0
- package/src/types/generated/google/protobuf/timestamp.ts +187 -0
- package/src/types/hooks.ts +290 -0
- package/src/types/ids.ts +44 -0
- package/src/types/logs.ts +330 -0
- package/src/types/permissions.ts +441 -0
- package/src/types/plugin.ts +363 -0
- package/src/types/textInputTypes.ts +387 -0
- package/src/upstreamproxy/relay.ts +455 -0
- package/src/upstreamproxy/upstreamproxy.ts +285 -0
- package/src/utils/CircularBuffer.ts +84 -0
- package/src/utils/Cursor.ts +1530 -0
- package/src/utils/QueryGuard.ts +121 -0
- package/src/utils/Shell.ts +474 -0
- package/src/utils/ShellCommand.ts +465 -0
- package/src/utils/abortController.ts +99 -0
- package/src/utils/activityManager.ts +164 -0
- package/src/utils/advisor.ts +145 -0
- package/src/utils/agentContext.ts +178 -0
- package/src/utils/agentId.ts +99 -0
- package/src/utils/agentSwarmsEnabled.ts +44 -0
- package/src/utils/agenticSessionSearch.ts +307 -0
- package/src/utils/analyzeContext.ts +1382 -0
- package/src/utils/ansiToPng.ts +334 -0
- package/src/utils/ansiToSvg.ts +272 -0
- package/src/utils/api.ts +718 -0
- package/src/utils/apiPreconnect.ts +72 -0
- package/src/utils/appleTerminalBackup.ts +124 -0
- package/src/utils/argumentSubstitution.ts +145 -0
- package/src/utils/array.ts +13 -0
- package/src/utils/asciicast.ts +239 -0
- package/src/utils/attachments.ts +3997 -0
- package/src/utils/attribution.ts +393 -0
- package/src/utils/auth.ts +2007 -0
- package/src/utils/authFileDescriptor.ts +196 -0
- package/src/utils/authPortable.ts +19 -0
- package/src/utils/autoModeDenials.ts +26 -0
- package/src/utils/autoRunIssue.tsx +122 -0
- package/src/utils/autoUpdater.ts +562 -0
- package/src/utils/aws.ts +74 -0
- package/src/utils/awsAuthStatusManager.ts +81 -0
- package/src/utils/background/remote/preconditions.ts +235 -0
- package/src/utils/background/remote/remoteSession.ts +98 -0
- package/src/utils/backgroundHousekeeping.ts +94 -0
- package/src/utils/bash/ParsedCommand.ts +318 -0
- package/src/utils/bash/ShellSnapshot.ts +582 -0
- package/src/utils/bash/ast.ts +2679 -0
- package/src/utils/bash/bashParser.ts +4436 -0
- package/src/utils/bash/bashPipeCommand.ts +294 -0
- package/src/utils/bash/commands.ts +1339 -0
- package/src/utils/bash/heredoc.ts +733 -0
- package/src/utils/bash/parser.ts +230 -0
- package/src/utils/bash/prefix.ts +204 -0
- package/src/utils/bash/registry.ts +53 -0
- package/src/utils/bash/shellCompletion.ts +259 -0
- package/src/utils/bash/shellPrefix.ts +28 -0
- package/src/utils/bash/shellQuote.ts +304 -0
- package/src/utils/bash/shellQuoting.ts +128 -0
- package/src/utils/bash/specs/alias.ts +14 -0
- package/src/utils/bash/specs/index.ts +18 -0
- package/src/utils/bash/specs/nohup.ts +13 -0
- package/src/utils/bash/specs/pyright.ts +91 -0
- package/src/utils/bash/specs/sleep.ts +13 -0
- package/src/utils/bash/specs/srun.ts +31 -0
- package/src/utils/bash/specs/time.ts +13 -0
- package/src/utils/bash/specs/timeout.ts +20 -0
- package/src/utils/bash/treeSitterAnalysis.ts +506 -0
- package/src/utils/betas.ts +438 -0
- package/src/utils/billing.ts +78 -0
- package/src/utils/binaryCheck.ts +53 -0
- package/src/utils/browser.ts +68 -0
- package/src/utils/bufferedWriter.ts +100 -0
- package/src/utils/bundledMode.ts +22 -0
- package/src/utils/caCerts.ts +115 -0
- package/src/utils/caCertsConfig.ts +88 -0
- package/src/utils/cachePaths.ts +38 -0
- package/src/utils/classifierApprovals.ts +88 -0
- package/src/utils/classifierApprovalsHook.ts +17 -0
- package/src/utils/claudeCodeHints.ts +193 -0
- package/src/utils/claudeDesktop.ts +152 -0
- package/src/utils/claudeInChrome/chromeNativeHost.ts +527 -0
- package/src/utils/claudeInChrome/common.ts +540 -0
- package/src/utils/claudeInChrome/mcpServer.ts +293 -0
- package/src/utils/claudeInChrome/prompt.ts +83 -0
- package/src/utils/claudeInChrome/setup.ts +400 -0
- package/src/utils/claudeInChrome/setupPortable.ts +233 -0
- package/src/utils/claudeInChrome/toolRendering.tsx +262 -0
- package/src/utils/claudemd.ts +1479 -0
- package/src/utils/cleanup.ts +602 -0
- package/src/utils/cleanupRegistry.ts +25 -0
- package/src/utils/cliArgs.ts +60 -0
- package/src/utils/cliHighlight.ts +54 -0
- package/src/utils/codeIndexing.ts +206 -0
- package/src/utils/collapseBackgroundBashNotifications.ts +84 -0
- package/src/utils/collapseHookSummaries.ts +59 -0
- package/src/utils/collapseReadSearch.ts +1109 -0
- package/src/utils/collapseTeammateShutdowns.ts +55 -0
- package/src/utils/combinedAbortSignal.ts +47 -0
- package/src/utils/commandLifecycle.ts +21 -0
- package/src/utils/commitAttribution.ts +961 -0
- package/src/utils/completionCache.ts +166 -0
- package/src/utils/computerUse/appNames.ts +196 -0
- package/src/utils/computerUse/cleanup.ts +86 -0
- package/src/utils/computerUse/common.ts +61 -0
- package/src/utils/computerUse/computerUseLock.ts +215 -0
- package/src/utils/computerUse/drainRunLoop.ts +79 -0
- package/src/utils/computerUse/escHotkey.ts +54 -0
- package/src/utils/computerUse/executor.ts +658 -0
- package/src/utils/computerUse/gates.ts +72 -0
- package/src/utils/computerUse/hostAdapter.ts +69 -0
- package/src/utils/computerUse/inputLoader.ts +30 -0
- package/src/utils/computerUse/mcpServer.ts +106 -0
- package/src/utils/computerUse/setup.ts +53 -0
- package/src/utils/computerUse/swiftLoader.ts +23 -0
- package/src/utils/computerUse/toolRendering.tsx +125 -0
- package/src/utils/computerUse/wrapper.tsx +336 -0
- package/src/utils/concurrentSessions.ts +204 -0
- package/src/utils/config.ts +1817 -0
- package/src/utils/configConstants.ts +21 -0
- package/src/utils/contentArray.ts +51 -0
- package/src/utils/context.ts +221 -0
- package/src/utils/contextAnalysis.ts +272 -0
- package/src/utils/contextSuggestions.ts +235 -0
- package/src/utils/controlMessageCompat.ts +32 -0
- package/src/utils/conversationRecovery.ts +597 -0
- package/src/utils/cron.ts +308 -0
- package/src/utils/cronJitterConfig.ts +75 -0
- package/src/utils/cronScheduler.ts +565 -0
- package/src/utils/cronTasks.ts +458 -0
- package/src/utils/cronTasksLock.ts +195 -0
- package/src/utils/crossProjectResume.ts +75 -0
- package/src/utils/crypto.ts +13 -0
- package/src/utils/cwd.ts +32 -0
- package/src/utils/debug.ts +268 -0
- package/src/utils/debugFilter.ts +157 -0
- package/src/utils/deepLink/banner.ts +123 -0
- package/src/utils/deepLink/parseDeepLink.ts +170 -0
- package/src/utils/deepLink/protocolHandler.ts +136 -0
- package/src/utils/deepLink/registerProtocol.ts +348 -0
- package/src/utils/deepLink/terminalLauncher.ts +557 -0
- package/src/utils/deepLink/terminalPreference.ts +54 -0
- package/src/utils/desktopDeepLink.ts +236 -0
- package/src/utils/detectRepository.ts +178 -0
- package/src/utils/diagLogs.ts +94 -0
- package/src/utils/diff.ts +177 -0
- package/src/utils/directMemberMessage.ts +69 -0
- package/src/utils/displayTags.ts +51 -0
- package/src/utils/doctorContextWarnings.ts +265 -0
- package/src/utils/doctorDiagnostic.ts +625 -0
- package/src/utils/dxt/helpers.ts +88 -0
- package/src/utils/dxt/zip.ts +226 -0
- package/src/utils/earlyInput.ts +191 -0
- package/src/utils/editor.ts +183 -0
- package/src/utils/effort.ts +329 -0
- package/src/utils/embeddedTools.ts +29 -0
- package/src/utils/env.ts +347 -0
- package/src/utils/envDynamic.ts +151 -0
- package/src/utils/envUtils.ts +183 -0
- package/src/utils/envValidation.ts +38 -0
- package/src/utils/errorLogSink.ts +235 -0
- package/src/utils/errors.ts +238 -0
- package/src/utils/exampleCommands.ts +184 -0
- package/src/utils/execFileNoThrow.ts +150 -0
- package/src/utils/execFileNoThrowPortable.ts +89 -0
- package/src/utils/execSyncWrapper.ts +38 -0
- package/src/utils/exportRenderer.tsx +98 -0
- package/src/utils/extraUsage.ts +23 -0
- package/src/utils/fastMode.ts +532 -0
- package/src/utils/file.ts +584 -0
- package/src/utils/fileHistory.ts +1115 -0
- package/src/utils/fileOperationAnalytics.ts +71 -0
- package/src/utils/filePersistence/filePersistence.ts +287 -0
- package/src/utils/filePersistence/outputsScanner.ts +126 -0
- package/src/utils/fileRead.ts +102 -0
- package/src/utils/fileReadCache.ts +96 -0
- package/src/utils/fileStateCache.ts +142 -0
- package/src/utils/findExecutable.ts +17 -0
- package/src/utils/fingerprint.ts +76 -0
- package/src/utils/forkedAgent.ts +689 -0
- package/src/utils/format.ts +308 -0
- package/src/utils/formatBriefTimestamp.ts +81 -0
- package/src/utils/fpsTracker.ts +47 -0
- package/src/utils/frontmatterParser.ts +370 -0
- package/src/utils/fsOperations.ts +770 -0
- package/src/utils/fullscreen.ts +202 -0
- package/src/utils/generatedFiles.ts +136 -0
- package/src/utils/generators.ts +88 -0
- package/src/utils/genericProcessUtils.ts +184 -0
- package/src/utils/getWorktreePaths.ts +70 -0
- package/src/utils/getWorktreePathsPortable.ts +27 -0
- package/src/utils/ghPrStatus.ts +106 -0
- package/src/utils/git/gitConfigParser.ts +277 -0
- package/src/utils/git/gitFilesystem.ts +699 -0
- package/src/utils/git/gitignore.ts +99 -0
- package/src/utils/git.ts +926 -0
- package/src/utils/gitDiff.ts +532 -0
- package/src/utils/gitSettings.ts +18 -0
- package/src/utils/github/ghAuthStatus.ts +29 -0
- package/src/utils/githubRepoPathMapping.ts +162 -0
- package/src/utils/glob.ts +130 -0
- package/src/utils/gracefulShutdown.ts +529 -0
- package/src/utils/groupToolUses.ts +182 -0
- package/src/utils/handlePromptSubmit.ts +610 -0
- package/src/utils/hash.ts +46 -0
- package/src/utils/headlessProfiler.ts +178 -0
- package/src/utils/heapDumpService.ts +303 -0
- package/src/utils/heatmap.ts +198 -0
- package/src/utils/highlightMatch.tsx +28 -0
- package/src/utils/hooks/AsyncHookRegistry.ts +309 -0
- package/src/utils/hooks/apiQueryHookHelper.ts +141 -0
- package/src/utils/hooks/execAgentHook.ts +339 -0
- package/src/utils/hooks/execHttpHook.ts +242 -0
- package/src/utils/hooks/execPromptHook.ts +211 -0
- package/src/utils/hooks/fileChangedWatcher.ts +191 -0
- package/src/utils/hooks/hookEvents.ts +192 -0
- package/src/utils/hooks/hookHelpers.ts +83 -0
- package/src/utils/hooks/hooksConfigManager.ts +400 -0
- package/src/utils/hooks/hooksConfigSnapshot.ts +133 -0
- package/src/utils/hooks/hooksSettings.ts +271 -0
- package/src/utils/hooks/postSamplingHooks.ts +70 -0
- package/src/utils/hooks/registerFrontmatterHooks.ts +67 -0
- package/src/utils/hooks/registerSkillHooks.ts +64 -0
- package/src/utils/hooks/sessionHooks.ts +447 -0
- package/src/utils/hooks/skillImprovement.ts +267 -0
- package/src/utils/hooks/ssrfGuard.ts +294 -0
- package/src/utils/hooks.ts +5022 -0
- package/src/utils/horizontalScroll.ts +137 -0
- package/src/utils/http.ts +136 -0
- package/src/utils/hyperlink.ts +39 -0
- package/src/utils/iTermBackup.ts +73 -0
- package/src/utils/ide.ts +1494 -0
- package/src/utils/idePathConversion.ts +90 -0
- package/src/utils/idleTimeout.ts +53 -0
- package/src/utils/imagePaste.ts +416 -0
- package/src/utils/imageResizer.ts +880 -0
- package/src/utils/imageStore.ts +167 -0
- package/src/utils/imageValidation.ts +104 -0
- package/src/utils/immediateCommand.ts +15 -0
- package/src/utils/inProcessTeammateHelpers.ts +102 -0
- package/src/utils/ink.ts +26 -0
- package/src/utils/intl.ts +94 -0
- package/src/utils/jetbrains.ts +191 -0
- package/src/utils/json.ts +277 -0
- package/src/utils/jsonRead.ts +16 -0
- package/src/utils/keyboardShortcuts.ts +14 -0
- package/src/utils/lazySchema.ts +8 -0
- package/src/utils/listSessionsImpl.ts +454 -0
- package/src/utils/localInstaller.ts +162 -0
- package/src/utils/lockfile.ts +43 -0
- package/src/utils/log.ts +362 -0
- package/src/utils/logoV2Utils.ts +350 -0
- package/src/utils/mailbox.ts +73 -0
- package/src/utils/managedEnv.ts +199 -0
- package/src/utils/managedEnvConstants.ts +191 -0
- package/src/utils/markdown.ts +381 -0
- package/src/utils/markdownConfigLoader.ts +600 -0
- package/src/utils/mcp/dateTimeParser.ts +121 -0
- package/src/utils/mcp/elicitationValidation.ts +336 -0
- package/src/utils/mcpInstructionsDelta.ts +130 -0
- package/src/utils/mcpOutputStorage.ts +189 -0
- package/src/utils/mcpValidation.ts +208 -0
- package/src/utils/mcpWebSocketTransport.ts +200 -0
- package/src/utils/memoize.ts +269 -0
- package/src/utils/memory/types.ts +12 -0
- package/src/utils/memory/versions.ts +8 -0
- package/src/utils/memoryFileDetection.ts +289 -0
- package/src/utils/messagePredicates.ts +8 -0
- package/src/utils/messageQueueManager.ts +547 -0
- package/src/utils/messages/mappers.ts +290 -0
- package/src/utils/messages/systemInit.ts +96 -0
- package/src/utils/messages.ts +5512 -0
- package/src/utils/model/agent.ts +157 -0
- package/src/utils/model/aliases.ts +25 -0
- package/src/utils/model/antModels.ts +64 -0
- package/src/utils/model/bedrock.ts +265 -0
- package/src/utils/model/check1mAccess.ts +72 -0
- package/src/utils/model/configs.ts +118 -0
- package/src/utils/model/contextWindowUpgradeCheck.ts +47 -0
- package/src/utils/model/deprecation.ts +101 -0
- package/src/utils/model/model.ts +634 -0
- package/src/utils/model/modelAllowlist.ts +170 -0
- package/src/utils/model/modelCapabilities.ts +118 -0
- package/src/utils/model/modelOptions.ts +540 -0
- package/src/utils/model/modelStrings.ts +166 -0
- package/src/utils/model/modelSupportOverrides.ts +50 -0
- package/src/utils/model/providers.ts +46 -0
- package/src/utils/model/validateModel.ts +159 -0
- package/src/utils/modelCost.ts +235 -0
- package/src/utils/modifiers.ts +36 -0
- package/src/utils/mtls.ts +179 -0
- package/src/utils/nativeInstaller/download.ts +523 -0
- package/src/utils/nativeInstaller/index.ts +18 -0
- package/src/utils/nativeInstaller/installer.ts +1708 -0
- package/src/utils/nativeInstaller/packageManagers.ts +336 -0
- package/src/utils/nativeInstaller/pidLock.ts +433 -0
- package/src/utils/notebook.ts +224 -0
- package/src/utils/objectGroupBy.ts +18 -0
- package/src/utils/pasteStore.ts +104 -0
- package/src/utils/path.ts +155 -0
- package/src/utils/pdf.ts +300 -0
- package/src/utils/pdfUtils.ts +70 -0
- package/src/utils/peerAddress.ts +21 -0
- package/src/utils/permissions/PermissionMode.ts +141 -0
- package/src/utils/permissions/PermissionPromptToolResultSchema.ts +127 -0
- package/src/utils/permissions/PermissionResult.ts +35 -0
- package/src/utils/permissions/PermissionRule.ts +40 -0
- package/src/utils/permissions/PermissionUpdate.ts +389 -0
- package/src/utils/permissions/PermissionUpdateSchema.ts +78 -0
- package/src/utils/permissions/autoModeState.ts +39 -0
- package/src/utils/permissions/bashClassifier.ts +61 -0
- package/src/utils/permissions/bypassPermissionsKillswitch.ts +155 -0
- package/src/utils/permissions/classifierDecision.ts +98 -0
- package/src/utils/permissions/classifierShared.ts +39 -0
- package/src/utils/permissions/dangerousPatterns.ts +80 -0
- package/src/utils/permissions/denialTracking.ts +45 -0
- package/src/utils/permissions/filesystem.ts +1777 -0
- package/src/utils/permissions/getNextPermissionMode.ts +101 -0
- package/src/utils/permissions/pathValidation.ts +485 -0
- package/src/utils/permissions/permissionExplainer.ts +250 -0
- package/src/utils/permissions/permissionRuleParser.ts +198 -0
- package/src/utils/permissions/permissionSetup.ts +1532 -0
- package/src/utils/permissions/permissions.ts +1486 -0
- package/src/utils/permissions/permissionsLoader.ts +296 -0
- package/src/utils/permissions/shadowedRuleDetection.ts +234 -0
- package/src/utils/permissions/shellRuleMatching.ts +228 -0
- package/src/utils/permissions/yoloClassifier.ts +1495 -0
- package/src/utils/planModeV2.ts +95 -0
- package/src/utils/plans.ts +397 -0
- package/src/utils/platform.ts +150 -0
- package/src/utils/plugins/addDirPluginSettings.ts +71 -0
- package/src/utils/plugins/cacheUtils.ts +196 -0
- package/src/utils/plugins/dependencyResolver.ts +305 -0
- package/src/utils/plugins/fetchTelemetry.ts +135 -0
- package/src/utils/plugins/gitAvailability.ts +69 -0
- package/src/utils/plugins/headlessPluginInstall.ts +174 -0
- package/src/utils/plugins/hintRecommendation.ts +164 -0
- package/src/utils/plugins/installCounts.ts +292 -0
- package/src/utils/plugins/installedPluginsManager.ts +1268 -0
- package/src/utils/plugins/loadPluginAgents.ts +348 -0
- package/src/utils/plugins/loadPluginCommands.ts +946 -0
- package/src/utils/plugins/loadPluginHooks.ts +287 -0
- package/src/utils/plugins/loadPluginOutputStyles.ts +178 -0
- package/src/utils/plugins/lspPluginIntegration.ts +387 -0
- package/src/utils/plugins/lspRecommendation.ts +374 -0
- package/src/utils/plugins/managedPlugins.ts +27 -0
- package/src/utils/plugins/marketplaceHelpers.ts +592 -0
- package/src/utils/plugins/marketplaceManager.ts +2643 -0
- package/src/utils/plugins/mcpPluginIntegration.ts +634 -0
- package/src/utils/plugins/mcpbHandler.ts +968 -0
- package/src/utils/plugins/officialMarketplace.ts +25 -0
- package/src/utils/plugins/officialMarketplaceGcs.ts +216 -0
- package/src/utils/plugins/officialMarketplaceStartupCheck.ts +439 -0
- package/src/utils/plugins/orphanedPluginFilter.ts +114 -0
- package/src/utils/plugins/parseMarketplaceInput.ts +162 -0
- package/src/utils/plugins/performStartupChecks.tsx +70 -0
- package/src/utils/plugins/pluginAutoupdate.ts +284 -0
- package/src/utils/plugins/pluginBlocklist.ts +127 -0
- package/src/utils/plugins/pluginDirectories.ts +178 -0
- package/src/utils/plugins/pluginFlagging.ts +208 -0
- package/src/utils/plugins/pluginIdentifier.ts +123 -0
- package/src/utils/plugins/pluginInstallationHelpers.ts +595 -0
- package/src/utils/plugins/pluginLoader.ts +3302 -0
- package/src/utils/plugins/pluginOptionsStorage.ts +400 -0
- package/src/utils/plugins/pluginPolicy.ts +20 -0
- package/src/utils/plugins/pluginStartupCheck.ts +341 -0
- package/src/utils/plugins/pluginVersioning.ts +157 -0
- package/src/utils/plugins/reconciler.ts +265 -0
- package/src/utils/plugins/refresh.ts +215 -0
- package/src/utils/plugins/schemas.ts +1681 -0
- package/src/utils/plugins/validatePlugin.ts +903 -0
- package/src/utils/plugins/walkPluginMarkdown.ts +69 -0
- package/src/utils/plugins/zipCache.ts +406 -0
- package/src/utils/plugins/zipCacheAdapters.ts +164 -0
- package/src/utils/powershell/dangerousCmdlets.ts +185 -0
- package/src/utils/powershell/parser.ts +1804 -0
- package/src/utils/powershell/staticPrefix.ts +316 -0
- package/src/utils/preflightChecks.tsx +151 -0
- package/src/utils/privacyLevel.ts +55 -0
- package/src/utils/process.ts +68 -0
- package/src/utils/processUserInput/processBashCommand.tsx +140 -0
- package/src/utils/processUserInput/processSlashCommand.tsx +922 -0
- package/src/utils/processUserInput/processTextPrompt.ts +100 -0
- package/src/utils/processUserInput/processUserInput.ts +605 -0
- package/src/utils/profilerBase.ts +46 -0
- package/src/utils/promptCategory.ts +49 -0
- package/src/utils/promptEditor.ts +188 -0
- package/src/utils/promptShellExecution.ts +183 -0
- package/src/utils/proxy.ts +426 -0
- package/src/utils/queryContext.ts +179 -0
- package/src/utils/queryHelpers.ts +552 -0
- package/src/utils/queryProfiler.ts +301 -0
- package/src/utils/queueProcessor.ts +95 -0
- package/src/utils/readEditContext.ts +227 -0
- package/src/utils/readFileInRange.ts +383 -0
- package/src/utils/releaseNotes.ts +360 -0
- package/src/utils/renderOptions.ts +77 -0
- package/src/utils/ripgrep.ts +679 -0
- package/src/utils/sandbox/sandbox-adapter.ts +985 -0
- package/src/utils/sandbox/sandbox-ui-utils.ts +12 -0
- package/src/utils/sanitization.ts +91 -0
- package/src/utils/screenshotClipboard.ts +121 -0
- package/src/utils/sdkEventQueue.ts +134 -0
- package/src/utils/secureStorage/fallbackStorage.ts +70 -0
- package/src/utils/secureStorage/index.ts +17 -0
- package/src/utils/secureStorage/keychainPrefetch.ts +116 -0
- package/src/utils/secureStorage/macOsKeychainHelpers.ts +111 -0
- package/src/utils/secureStorage/macOsKeychainStorage.ts +231 -0
- package/src/utils/secureStorage/plainTextStorage.ts +84 -0
- package/src/utils/semanticBoolean.ts +29 -0
- package/src/utils/semanticNumber.ts +36 -0
- package/src/utils/semver.ts +59 -0
- package/src/utils/sequential.ts +56 -0
- package/src/utils/sessionActivity.ts +133 -0
- package/src/utils/sessionEnvVars.ts +22 -0
- package/src/utils/sessionEnvironment.ts +166 -0
- package/src/utils/sessionFileAccessHooks.ts +250 -0
- package/src/utils/sessionIngressAuth.ts +140 -0
- package/src/utils/sessionRestore.ts +551 -0
- package/src/utils/sessionStart.ts +232 -0
- package/src/utils/sessionState.ts +150 -0
- package/src/utils/sessionStorage.ts +5105 -0
- package/src/utils/sessionStoragePortable.ts +793 -0
- package/src/utils/sessionTitle.ts +129 -0
- package/src/utils/sessionUrl.ts +64 -0
- package/src/utils/set.ts +53 -0
- package/src/utils/settings/allErrors.ts +32 -0
- package/src/utils/settings/applySettingsChange.ts +92 -0
- package/src/utils/settings/changeDetector.ts +488 -0
- package/src/utils/settings/constants.ts +202 -0
- package/src/utils/settings/internalWrites.ts +37 -0
- package/src/utils/settings/managedPath.ts +34 -0
- package/src/utils/settings/mdm/constants.ts +81 -0
- package/src/utils/settings/mdm/rawRead.ts +130 -0
- package/src/utils/settings/mdm/settings.ts +316 -0
- package/src/utils/settings/permissionValidation.ts +262 -0
- package/src/utils/settings/pluginOnlyPolicy.ts +60 -0
- package/src/utils/settings/schemaOutput.ts +8 -0
- package/src/utils/settings/settings.ts +1015 -0
- package/src/utils/settings/settingsCache.ts +80 -0
- package/src/utils/settings/toolValidationConfig.ts +103 -0
- package/src/utils/settings/types.ts +1148 -0
- package/src/utils/settings/validateEditTool.ts +45 -0
- package/src/utils/settings/validation.ts +265 -0
- package/src/utils/settings/validationTips.ts +164 -0
- package/src/utils/shell/bashProvider.ts +255 -0
- package/src/utils/shell/outputLimits.ts +14 -0
- package/src/utils/shell/powershellDetection.ts +107 -0
- package/src/utils/shell/powershellProvider.ts +123 -0
- package/src/utils/shell/prefix.ts +367 -0
- package/src/utils/shell/readOnlyCommandValidation.ts +1893 -0
- package/src/utils/shell/resolveDefaultShell.ts +14 -0
- package/src/utils/shell/shellProvider.ts +33 -0
- package/src/utils/shell/shellToolUtils.ts +22 -0
- package/src/utils/shell/specPrefix.ts +241 -0
- package/src/utils/shellConfig.ts +167 -0
- package/src/utils/sideQuery.ts +222 -0
- package/src/utils/sideQuestion.ts +155 -0
- package/src/utils/signal.ts +43 -0
- package/src/utils/sinks.ts +16 -0
- package/src/utils/skills/skillChangeDetector.ts +311 -0
- package/src/utils/slashCommandParsing.ts +60 -0
- package/src/utils/sleep.ts +84 -0
- package/src/utils/sliceAnsi.ts +91 -0
- package/src/utils/slowOperations.ts +286 -0
- package/src/utils/standaloneAgent.ts +23 -0
- package/src/utils/startupProfiler.ts +194 -0
- package/src/utils/staticRender.tsx +116 -0
- package/src/utils/stats.ts +1061 -0
- package/src/utils/statsCache.ts +434 -0
- package/src/utils/status.tsx +362 -0
- package/src/utils/statusNoticeDefinitions.tsx +198 -0
- package/src/utils/statusNoticeHelpers.ts +20 -0
- package/src/utils/stream.ts +76 -0
- package/src/utils/streamJsonStdoutGuard.ts +123 -0
- package/src/utils/streamlinedTransform.ts +201 -0
- package/src/utils/stringUtils.ts +235 -0
- package/src/utils/subprocessEnv.ts +99 -0
- package/src/utils/suggestions/commandSuggestions.ts +567 -0
- package/src/utils/suggestions/directoryCompletion.ts +263 -0
- package/src/utils/suggestions/shellHistoryCompletion.ts +119 -0
- package/src/utils/suggestions/skillUsageTracking.ts +55 -0
- package/src/utils/suggestions/slackChannelSuggestions.ts +209 -0
- package/src/utils/swarm/It2SetupPrompt.tsx +380 -0
- package/src/utils/swarm/backends/ITermBackend.ts +370 -0
- package/src/utils/swarm/backends/InProcessBackend.ts +339 -0
- package/src/utils/swarm/backends/PaneBackendExecutor.ts +354 -0
- package/src/utils/swarm/backends/TmuxBackend.ts +764 -0
- package/src/utils/swarm/backends/detection.ts +128 -0
- package/src/utils/swarm/backends/it2Setup.ts +245 -0
- package/src/utils/swarm/backends/registry.ts +464 -0
- package/src/utils/swarm/backends/teammateModeSnapshot.ts +87 -0
- package/src/utils/swarm/backends/types.ts +311 -0
- package/src/utils/swarm/constants.ts +33 -0
- package/src/utils/swarm/inProcessRunner.ts +1552 -0
- package/src/utils/swarm/leaderPermissionBridge.ts +54 -0
- package/src/utils/swarm/permissionSync.ts +928 -0
- package/src/utils/swarm/reconnection.ts +119 -0
- package/src/utils/swarm/spawnInProcess.ts +328 -0
- package/src/utils/swarm/spawnUtils.ts +146 -0
- package/src/utils/swarm/teamHelpers.ts +683 -0
- package/src/utils/swarm/teammateInit.ts +129 -0
- package/src/utils/swarm/teammateLayoutManager.ts +107 -0
- package/src/utils/swarm/teammateModel.ts +10 -0
- package/src/utils/swarm/teammatePromptAddendum.ts +18 -0
- package/src/utils/systemDirectories.ts +74 -0
- package/src/utils/systemPrompt.ts +123 -0
- package/src/utils/systemPromptType.ts +14 -0
- package/src/utils/systemTheme.ts +119 -0
- package/src/utils/taggedId.ts +54 -0
- package/src/utils/task/TaskOutput.ts +390 -0
- package/src/utils/task/diskOutput.ts +451 -0
- package/src/utils/task/framework.ts +308 -0
- package/src/utils/task/outputFormatting.ts +38 -0
- package/src/utils/task/sdkProgress.ts +36 -0
- package/src/utils/tasks.ts +862 -0
- package/src/utils/teamDiscovery.ts +81 -0
- package/src/utils/teamMemoryOps.ts +88 -0
- package/src/utils/teammate.ts +292 -0
- package/src/utils/teammateContext.ts +96 -0
- package/src/utils/teammateMailbox.ts +1183 -0
- package/src/utils/telemetry/betaSessionTracing.ts +491 -0
- package/src/utils/telemetry/bigqueryExporter.ts +252 -0
- package/src/utils/telemetry/events.ts +75 -0
- package/src/utils/telemetry/instrumentation.ts +825 -0
- package/src/utils/telemetry/logger.ts +26 -0
- package/src/utils/telemetry/perfettoTracing.ts +1120 -0
- package/src/utils/telemetry/pluginTelemetry.ts +289 -0
- package/src/utils/telemetry/sessionTracing.ts +927 -0
- package/src/utils/telemetry/skillLoadedEvent.ts +39 -0
- package/src/utils/telemetryAttributes.ts +71 -0
- package/src/utils/teleport/api.ts +466 -0
- package/src/utils/teleport/environmentSelection.ts +77 -0
- package/src/utils/teleport/environments.ts +120 -0
- package/src/utils/teleport/gitBundle.ts +292 -0
- package/src/utils/teleport.tsx +1226 -0
- package/src/utils/tempfile.ts +31 -0
- package/src/utils/terminal.ts +131 -0
- package/src/utils/terminalPanel.ts +191 -0
- package/src/utils/textHighlighting.ts +166 -0
- package/src/utils/theme.ts +639 -0
- package/src/utils/thinking.ts +162 -0
- package/src/utils/timeouts.ts +39 -0
- package/src/utils/tmuxSocket.ts +427 -0
- package/src/utils/todo/types.ts +18 -0
- package/src/utils/tokenBudget.ts +73 -0
- package/src/utils/tokens.ts +261 -0
- package/src/utils/toolErrors.ts +132 -0
- package/src/utils/toolPool.ts +79 -0
- package/src/utils/toolResultStorage.ts +1040 -0
- package/src/utils/toolSchemaCache.ts +26 -0
- package/src/utils/toolSearch.ts +756 -0
- package/src/utils/transcriptSearch.ts +202 -0
- package/src/utils/treeify.ts +170 -0
- package/src/utils/truncate.ts +179 -0
- package/src/utils/ultraplan/ccrSession.ts +349 -0
- package/src/utils/ultraplan/keyword.ts +127 -0
- package/src/utils/ultraplan/prompt.txt +1 -0
- package/src/utils/unaryLogging.ts +39 -0
- package/src/utils/undercover.ts +89 -0
- package/src/utils/user.ts +194 -0
- package/src/utils/userAgent.ts +10 -0
- package/src/utils/userPromptKeywords.ts +27 -0
- package/src/utils/uuid.ts +27 -0
- package/src/utils/warningHandler.ts +121 -0
- package/src/utils/which.ts +82 -0
- package/src/utils/windowsPaths.ts +173 -0
- package/src/utils/withResolvers.ts +13 -0
- package/src/utils/words.ts +800 -0
- package/src/utils/workloadContext.ts +57 -0
- package/src/utils/worktree.ts +1519 -0
- package/src/utils/worktreeModeEnabled.ts +11 -0
- package/src/utils/xdg.ts +65 -0
- package/src/utils/xml.ts +16 -0
- package/src/utils/yaml.ts +15 -0
- package/src/utils/zodToJsonSchema.ts +23 -0
- package/src/vim/motions.ts +82 -0
- package/src/vim/operators.ts +556 -0
- package/src/vim/textObjects.ts +186 -0
- package/src/vim/transitions.ts +490 -0
- package/src/vim/types.ts +199 -0
- package/src/voice/voiceModeEnabled.ts +54 -0
- package/start.js +1 -0
|
@@ -0,0 +1,5022 @@
|
|
|
1
|
+
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
|
2
|
+
/**
|
|
3
|
+
* Hooks are user-defined shell commands that can be executed at various points
|
|
4
|
+
* in Claude Code's lifecycle.
|
|
5
|
+
*/
|
|
6
|
+
import { basename } from 'path'
|
|
7
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
|
|
8
|
+
import { pathExists } from './file.js'
|
|
9
|
+
import { wrapSpawn } from './ShellCommand.js'
|
|
10
|
+
import { TaskOutput } from './task/TaskOutput.js'
|
|
11
|
+
import { getCwd } from './cwd.js'
|
|
12
|
+
import { randomUUID } from 'crypto'
|
|
13
|
+
import { formatShellPrefixCommand } from './bash/shellPrefix.js'
|
|
14
|
+
import {
|
|
15
|
+
getHookEnvFilePath,
|
|
16
|
+
invalidateSessionEnvCache,
|
|
17
|
+
} from './sessionEnvironment.js'
|
|
18
|
+
import { subprocessEnv } from './subprocessEnv.js'
|
|
19
|
+
import { getPlatform } from './platform.js'
|
|
20
|
+
import { findGitBashPath, windowsPathToPosixPath } from './windowsPaths.js'
|
|
21
|
+
import { getCachedPowerShellPath } from './shell/powershellDetection.js'
|
|
22
|
+
import { DEFAULT_HOOK_SHELL } from './shell/shellProvider.js'
|
|
23
|
+
import { buildPowerShellArgs } from './shell/powershellProvider.js'
|
|
24
|
+
import {
|
|
25
|
+
loadPluginOptions,
|
|
26
|
+
substituteUserConfigVariables,
|
|
27
|
+
} from './plugins/pluginOptionsStorage.js'
|
|
28
|
+
import { getPluginDataDir } from './plugins/pluginDirectories.js'
|
|
29
|
+
import {
|
|
30
|
+
getSessionId,
|
|
31
|
+
getProjectRoot,
|
|
32
|
+
getIsNonInteractiveSession,
|
|
33
|
+
getRegisteredHooks,
|
|
34
|
+
getStatsStore,
|
|
35
|
+
addToTurnHookDuration,
|
|
36
|
+
getOriginalCwd,
|
|
37
|
+
getMainThreadAgentType,
|
|
38
|
+
} from '../bootstrap/state.js'
|
|
39
|
+
import { checkHasTrustDialogAccepted } from './config.js'
|
|
40
|
+
import {
|
|
41
|
+
getHooksConfigFromSnapshot,
|
|
42
|
+
shouldAllowManagedHooksOnly,
|
|
43
|
+
shouldDisableAllHooksIncludingManaged,
|
|
44
|
+
} from './hooks/hooksConfigSnapshot.js'
|
|
45
|
+
import {
|
|
46
|
+
getTranscriptPathForSession,
|
|
47
|
+
getAgentTranscriptPath,
|
|
48
|
+
} from './sessionStorage.js'
|
|
49
|
+
import type { AgentId } from '../types/ids.js'
|
|
50
|
+
import {
|
|
51
|
+
getSettings_DEPRECATED,
|
|
52
|
+
getSettingsForSource,
|
|
53
|
+
} from './settings/settings.js'
|
|
54
|
+
import {
|
|
55
|
+
logEvent,
|
|
56
|
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
57
|
+
} from 'src/services/analytics/index.js'
|
|
58
|
+
import { logOTelEvent } from './telemetry/events.js'
|
|
59
|
+
import { ALLOWED_OFFICIAL_MARKETPLACE_NAMES } from './plugins/schemas.js'
|
|
60
|
+
import {
|
|
61
|
+
startHookSpan,
|
|
62
|
+
endHookSpan,
|
|
63
|
+
isBetaTracingEnabled,
|
|
64
|
+
} from './telemetry/sessionTracing.js'
|
|
65
|
+
import {
|
|
66
|
+
hookJSONOutputSchema,
|
|
67
|
+
promptRequestSchema,
|
|
68
|
+
type HookCallback,
|
|
69
|
+
type HookCallbackMatcher,
|
|
70
|
+
type PromptRequest,
|
|
71
|
+
type PromptResponse,
|
|
72
|
+
isAsyncHookJSONOutput,
|
|
73
|
+
isSyncHookJSONOutput,
|
|
74
|
+
type PermissionRequestResult,
|
|
75
|
+
} from '../types/hooks.js'
|
|
76
|
+
import type {
|
|
77
|
+
HookEvent,
|
|
78
|
+
HookInput,
|
|
79
|
+
HookJSONOutput,
|
|
80
|
+
NotificationHookInput,
|
|
81
|
+
PostToolUseHookInput,
|
|
82
|
+
PostToolUseFailureHookInput,
|
|
83
|
+
PermissionDeniedHookInput,
|
|
84
|
+
PreCompactHookInput,
|
|
85
|
+
PostCompactHookInput,
|
|
86
|
+
PreToolUseHookInput,
|
|
87
|
+
SessionStartHookInput,
|
|
88
|
+
SessionEndHookInput,
|
|
89
|
+
SetupHookInput,
|
|
90
|
+
StopHookInput,
|
|
91
|
+
StopFailureHookInput,
|
|
92
|
+
SubagentStartHookInput,
|
|
93
|
+
SubagentStopHookInput,
|
|
94
|
+
TeammateIdleHookInput,
|
|
95
|
+
TaskCreatedHookInput,
|
|
96
|
+
TaskCompletedHookInput,
|
|
97
|
+
ConfigChangeHookInput,
|
|
98
|
+
CwdChangedHookInput,
|
|
99
|
+
FileChangedHookInput,
|
|
100
|
+
InstructionsLoadedHookInput,
|
|
101
|
+
UserPromptSubmitHookInput,
|
|
102
|
+
PermissionRequestHookInput,
|
|
103
|
+
ElicitationHookInput,
|
|
104
|
+
ElicitationResultHookInput,
|
|
105
|
+
PermissionUpdate,
|
|
106
|
+
ExitReason,
|
|
107
|
+
SyncHookJSONOutput,
|
|
108
|
+
AsyncHookJSONOutput,
|
|
109
|
+
} from 'src/entrypoints/agentSdkTypes.js'
|
|
110
|
+
import type { StatusLineCommandInput } from '../types/statusLine.js'
|
|
111
|
+
import type { ElicitResult } from '@modelcontextprotocol/sdk/types.js'
|
|
112
|
+
import type { FileSuggestionCommandInput } from '../types/fileSuggestion.js'
|
|
113
|
+
import type { HookResultMessage } from 'src/types/message.js'
|
|
114
|
+
import chalk from 'chalk'
|
|
115
|
+
import type {
|
|
116
|
+
HookMatcher,
|
|
117
|
+
HookCommand,
|
|
118
|
+
PluginHookMatcher,
|
|
119
|
+
SkillHookMatcher,
|
|
120
|
+
} from './settings/types.js'
|
|
121
|
+
import { getHookDisplayText } from './hooks/hooksSettings.js'
|
|
122
|
+
import { logForDebugging } from './debug.js'
|
|
123
|
+
import { logForDiagnosticsNoPII } from './diagLogs.js'
|
|
124
|
+
import { firstLineOf } from './stringUtils.js'
|
|
125
|
+
import {
|
|
126
|
+
normalizeLegacyToolName,
|
|
127
|
+
getLegacyToolNames,
|
|
128
|
+
permissionRuleValueFromString,
|
|
129
|
+
} from './permissions/permissionRuleParser.js'
|
|
130
|
+
import { logError } from './log.js'
|
|
131
|
+
import { createCombinedAbortSignal } from './combinedAbortSignal.js'
|
|
132
|
+
import type { PermissionResult } from './permissions/PermissionResult.js'
|
|
133
|
+
import { registerPendingAsyncHook } from './hooks/AsyncHookRegistry.js'
|
|
134
|
+
import { enqueuePendingNotification } from './messageQueueManager.js'
|
|
135
|
+
import {
|
|
136
|
+
extractTextContent,
|
|
137
|
+
getLastAssistantMessage,
|
|
138
|
+
wrapInSystemReminder,
|
|
139
|
+
} from './messages.js'
|
|
140
|
+
import {
|
|
141
|
+
emitHookStarted,
|
|
142
|
+
emitHookResponse,
|
|
143
|
+
startHookProgressInterval,
|
|
144
|
+
} from './hooks/hookEvents.js'
|
|
145
|
+
import { createAttachmentMessage } from './attachments.js'
|
|
146
|
+
import { all } from './generators.js'
|
|
147
|
+
import { findToolByName, type Tools, type ToolUseContext } from '../Tool.js'
|
|
148
|
+
import { execPromptHook } from './hooks/execPromptHook.js'
|
|
149
|
+
import type { Message, AssistantMessage } from '../types/message.js'
|
|
150
|
+
import { execAgentHook } from './hooks/execAgentHook.js'
|
|
151
|
+
import { execHttpHook } from './hooks/execHttpHook.js'
|
|
152
|
+
import type { ShellCommand } from './ShellCommand.js'
|
|
153
|
+
import {
|
|
154
|
+
getSessionHooks,
|
|
155
|
+
getSessionFunctionHooks,
|
|
156
|
+
getSessionHookCallback,
|
|
157
|
+
clearSessionHooks,
|
|
158
|
+
type SessionDerivedHookMatcher,
|
|
159
|
+
type FunctionHook,
|
|
160
|
+
} from './hooks/sessionHooks.js'
|
|
161
|
+
import type { AppState } from '../state/AppState.js'
|
|
162
|
+
import { jsonStringify, jsonParse } from './slowOperations.js'
|
|
163
|
+
import { isEnvTruthy } from './envUtils.js'
|
|
164
|
+
import { errorMessage, getErrnoCode } from './errors.js'
|
|
165
|
+
|
|
166
|
+
const TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* SessionEnd hooks run during shutdown/clear and need a much tighter bound
|
|
170
|
+
* than TOOL_HOOK_EXECUTION_TIMEOUT_MS. This value is used by callers as both
|
|
171
|
+
* the per-hook default timeout AND the overall AbortSignal cap (hooks run in
|
|
172
|
+
* parallel, so one value suffices). Overridable via env var for users whose
|
|
173
|
+
* teardown scripts need more time.
|
|
174
|
+
*/
|
|
175
|
+
const SESSION_END_HOOK_TIMEOUT_MS_DEFAULT = 1500
|
|
176
|
+
export function getSessionEndHookTimeoutMs(): number {
|
|
177
|
+
const raw = process.env.CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS
|
|
178
|
+
const parsed = raw ? parseInt(raw, 10) : NaN
|
|
179
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
180
|
+
? parsed
|
|
181
|
+
: SESSION_END_HOOK_TIMEOUT_MS_DEFAULT
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function executeInBackground({
|
|
185
|
+
processId,
|
|
186
|
+
hookId,
|
|
187
|
+
shellCommand,
|
|
188
|
+
asyncResponse,
|
|
189
|
+
hookEvent,
|
|
190
|
+
hookName,
|
|
191
|
+
command,
|
|
192
|
+
asyncRewake,
|
|
193
|
+
pluginId,
|
|
194
|
+
}: {
|
|
195
|
+
processId: string
|
|
196
|
+
hookId: string
|
|
197
|
+
shellCommand: ShellCommand
|
|
198
|
+
asyncResponse: AsyncHookJSONOutput
|
|
199
|
+
hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion'
|
|
200
|
+
hookName: string
|
|
201
|
+
command: string
|
|
202
|
+
asyncRewake?: boolean
|
|
203
|
+
pluginId?: string
|
|
204
|
+
}): boolean {
|
|
205
|
+
if (asyncRewake) {
|
|
206
|
+
// asyncRewake hooks bypass the registry entirely. On completion, if exit
|
|
207
|
+
// code 2 (blocking error), enqueue as a task-notification so it wakes the
|
|
208
|
+
// model via useQueueProcessor (idle) or gets injected mid-query via
|
|
209
|
+
// queued_command attachments (busy).
|
|
210
|
+
//
|
|
211
|
+
// NOTE: We deliberately do NOT call shellCommand.background() here, because
|
|
212
|
+
// it calls taskOutput.spillToDisk() which breaks in-memory stdout/stderr
|
|
213
|
+
// capture (getStderr() returns '' in disk mode). The StreamWrappers stay
|
|
214
|
+
// attached and pipe data into the in-memory TaskOutput buffers. The abort
|
|
215
|
+
// handler already no-ops on 'interrupt' reason (user submitted a new
|
|
216
|
+
// message), so the hook survives new prompts. A hard cancel (Escape) WILL
|
|
217
|
+
// kill the hook via the abort handler, which is the desired behavior.
|
|
218
|
+
void shellCommand.result.then(async result => {
|
|
219
|
+
// result resolves on 'exit', but stdio 'data' events may still be
|
|
220
|
+
// pending. Yield to I/O so the StreamWrapper data handlers drain into
|
|
221
|
+
// TaskOutput before we read it.
|
|
222
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
223
|
+
const stdout = await shellCommand.taskOutput.getStdout()
|
|
224
|
+
const stderr = shellCommand.taskOutput.getStderr()
|
|
225
|
+
shellCommand.cleanup()
|
|
226
|
+
emitHookResponse({
|
|
227
|
+
hookId,
|
|
228
|
+
hookName,
|
|
229
|
+
hookEvent,
|
|
230
|
+
output: stdout + stderr,
|
|
231
|
+
stdout,
|
|
232
|
+
stderr,
|
|
233
|
+
exitCode: result.code,
|
|
234
|
+
outcome: result.code === 0 ? 'success' : 'error',
|
|
235
|
+
})
|
|
236
|
+
if (result.code === 2) {
|
|
237
|
+
enqueuePendingNotification({
|
|
238
|
+
value: wrapInSystemReminder(
|
|
239
|
+
`Stop hook blocking error from command "${hookName}": ${stderr || stdout}`,
|
|
240
|
+
),
|
|
241
|
+
mode: 'task-notification',
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
return true
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// TaskOutput on the ShellCommand accumulates data — no stream listeners needed
|
|
249
|
+
if (!shellCommand.background(processId)) {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
registerPendingAsyncHook({
|
|
254
|
+
processId,
|
|
255
|
+
hookId,
|
|
256
|
+
asyncResponse,
|
|
257
|
+
hookEvent,
|
|
258
|
+
hookName,
|
|
259
|
+
command,
|
|
260
|
+
shellCommand,
|
|
261
|
+
pluginId,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Checks if a hook should be skipped due to lack of workspace trust.
|
|
269
|
+
*
|
|
270
|
+
* ALL hooks require workspace trust because they execute arbitrary commands from
|
|
271
|
+
* .claude/settings.json. This is a defense-in-depth security measure.
|
|
272
|
+
*
|
|
273
|
+
* Context: Hooks are captured via captureHooksConfigSnapshot() before the trust
|
|
274
|
+
* dialog is shown. While most hooks won't execute until after trust is established
|
|
275
|
+
* through normal program flow, enforcing trust for ALL hooks prevents:
|
|
276
|
+
* - Future bugs where a hook might accidentally execute before trust
|
|
277
|
+
* - Any codepath that might trigger hooks before trust dialog
|
|
278
|
+
* - Security issues from hook execution in untrusted workspaces
|
|
279
|
+
*
|
|
280
|
+
* Historical vulnerabilities that prompted this check:
|
|
281
|
+
* - SessionEnd hooks executing when user declines trust dialog
|
|
282
|
+
* - SubagentStop hooks executing when subagent completes before trust
|
|
283
|
+
*
|
|
284
|
+
* @returns true if hook should be skipped, false if it should execute
|
|
285
|
+
*/
|
|
286
|
+
export function shouldSkipHookDueToTrust(): boolean {
|
|
287
|
+
// In non-interactive mode (SDK), trust is implicit - always execute
|
|
288
|
+
const isInteractive = !getIsNonInteractiveSession()
|
|
289
|
+
if (!isInteractive) {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// In interactive mode, ALL hooks require trust
|
|
294
|
+
const hasTrust = checkHasTrustDialogAccepted()
|
|
295
|
+
return !hasTrust
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Creates the base hook input that's common to all hook types
|
|
300
|
+
*/
|
|
301
|
+
export function createBaseHookInput(
|
|
302
|
+
permissionMode?: string,
|
|
303
|
+
sessionId?: string,
|
|
304
|
+
// Typed narrowly (not ToolUseContext) so callers can pass toolUseContext
|
|
305
|
+
// directly via structural typing without this function depending on Tool.ts.
|
|
306
|
+
agentInfo?: { agentId?: string; agentType?: string },
|
|
307
|
+
): {
|
|
308
|
+
session_id: string
|
|
309
|
+
transcript_path: string
|
|
310
|
+
cwd: string
|
|
311
|
+
permission_mode?: string
|
|
312
|
+
agent_id?: string
|
|
313
|
+
agent_type?: string
|
|
314
|
+
} {
|
|
315
|
+
const resolvedSessionId = sessionId ?? getSessionId()
|
|
316
|
+
// agent_type: subagent's type (from toolUseContext) takes precedence over
|
|
317
|
+
// the session's --agent flag. Hooks use agent_id presence to distinguish
|
|
318
|
+
// subagent calls from main-thread calls in a --agent session.
|
|
319
|
+
const resolvedAgentType = agentInfo?.agentType ?? getMainThreadAgentType()
|
|
320
|
+
return {
|
|
321
|
+
session_id: resolvedSessionId,
|
|
322
|
+
transcript_path: getTranscriptPathForSession(resolvedSessionId),
|
|
323
|
+
cwd: getCwd(),
|
|
324
|
+
permission_mode: permissionMode,
|
|
325
|
+
agent_id: agentInfo?.agentId,
|
|
326
|
+
agent_type: resolvedAgentType,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface HookBlockingError {
|
|
331
|
+
blockingError: string
|
|
332
|
+
command: string
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Re-export ElicitResult from MCP SDK as ElicitationResponse for backward compat. */
|
|
336
|
+
export type ElicitationResponse = ElicitResult
|
|
337
|
+
|
|
338
|
+
export interface HookResult {
|
|
339
|
+
message?: HookResultMessage
|
|
340
|
+
systemMessage?: string
|
|
341
|
+
blockingError?: HookBlockingError
|
|
342
|
+
outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled'
|
|
343
|
+
preventContinuation?: boolean
|
|
344
|
+
stopReason?: string
|
|
345
|
+
permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'
|
|
346
|
+
hookPermissionDecisionReason?: string
|
|
347
|
+
additionalContext?: string
|
|
348
|
+
initialUserMessage?: string
|
|
349
|
+
updatedInput?: Record<string, unknown>
|
|
350
|
+
updatedMCPToolOutput?: unknown
|
|
351
|
+
permissionRequestResult?: PermissionRequestResult
|
|
352
|
+
elicitationResponse?: ElicitationResponse
|
|
353
|
+
watchPaths?: string[]
|
|
354
|
+
elicitationResultResponse?: ElicitationResponse
|
|
355
|
+
retry?: boolean
|
|
356
|
+
hook: HookCommand | HookCallback | FunctionHook
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export type AggregatedHookResult = {
|
|
360
|
+
message?: HookResultMessage
|
|
361
|
+
blockingError?: HookBlockingError
|
|
362
|
+
preventContinuation?: boolean
|
|
363
|
+
stopReason?: string
|
|
364
|
+
hookPermissionDecisionReason?: string
|
|
365
|
+
hookSource?: string
|
|
366
|
+
permissionBehavior?: PermissionResult['behavior']
|
|
367
|
+
additionalContexts?: string[]
|
|
368
|
+
initialUserMessage?: string
|
|
369
|
+
updatedInput?: Record<string, unknown>
|
|
370
|
+
updatedMCPToolOutput?: unknown
|
|
371
|
+
permissionRequestResult?: PermissionRequestResult
|
|
372
|
+
watchPaths?: string[]
|
|
373
|
+
elicitationResponse?: ElicitationResponse
|
|
374
|
+
elicitationResultResponse?: ElicitationResponse
|
|
375
|
+
retry?: boolean
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Parse and validate a JSON string against the hook output Zod schema.
|
|
380
|
+
* Returns the validated output or formatted validation errors.
|
|
381
|
+
*/
|
|
382
|
+
function validateHookJson(
|
|
383
|
+
jsonString: string,
|
|
384
|
+
): { json: HookJSONOutput } | { validationError: string } {
|
|
385
|
+
const parsed = jsonParse(jsonString)
|
|
386
|
+
const validation = hookJSONOutputSchema().safeParse(parsed)
|
|
387
|
+
if (validation.success) {
|
|
388
|
+
logForDebugging('Successfully parsed and validated hook JSON output')
|
|
389
|
+
return { json: validation.data }
|
|
390
|
+
}
|
|
391
|
+
const errors = validation.error.issues
|
|
392
|
+
.map(err => ` - ${err.path.join('.')}: ${err.message}`)
|
|
393
|
+
.join('\n')
|
|
394
|
+
return {
|
|
395
|
+
validationError: `Hook JSON output validation failed:\n${errors}\n\nThe hook's output was: ${jsonStringify(parsed, null, 2)}`,
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parseHookOutput(stdout: string): {
|
|
400
|
+
json?: HookJSONOutput
|
|
401
|
+
plainText?: string
|
|
402
|
+
validationError?: string
|
|
403
|
+
} {
|
|
404
|
+
const trimmed = stdout.trim()
|
|
405
|
+
if (!trimmed.startsWith('{')) {
|
|
406
|
+
logForDebugging('Hook output does not start with {, treating as plain text')
|
|
407
|
+
return { plainText: stdout }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const result = validateHookJson(trimmed)
|
|
412
|
+
if ('json' in result) {
|
|
413
|
+
return result
|
|
414
|
+
}
|
|
415
|
+
// For command hooks, include the schema hint in the error message
|
|
416
|
+
const errorMessage = `${result.validationError}\n\nExpected schema:\n${jsonStringify(
|
|
417
|
+
{
|
|
418
|
+
continue: 'boolean (optional)',
|
|
419
|
+
suppressOutput: 'boolean (optional)',
|
|
420
|
+
stopReason: 'string (optional)',
|
|
421
|
+
decision: '"approve" | "block" (optional)',
|
|
422
|
+
reason: 'string (optional)',
|
|
423
|
+
systemMessage: 'string (optional)',
|
|
424
|
+
permissionDecision: '"allow" | "deny" | "ask" (optional)',
|
|
425
|
+
hookSpecificOutput: {
|
|
426
|
+
'for PreToolUse': {
|
|
427
|
+
hookEventName: '"PreToolUse"',
|
|
428
|
+
permissionDecision: '"allow" | "deny" | "ask" (optional)',
|
|
429
|
+
permissionDecisionReason: 'string (optional)',
|
|
430
|
+
updatedInput: 'object (optional) - Modified tool input to use',
|
|
431
|
+
},
|
|
432
|
+
'for UserPromptSubmit': {
|
|
433
|
+
hookEventName: '"UserPromptSubmit"',
|
|
434
|
+
additionalContext: 'string (required)',
|
|
435
|
+
},
|
|
436
|
+
'for PostToolUse': {
|
|
437
|
+
hookEventName: '"PostToolUse"',
|
|
438
|
+
additionalContext: 'string (optional)',
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
null,
|
|
443
|
+
2,
|
|
444
|
+
)}`
|
|
445
|
+
logForDebugging(errorMessage)
|
|
446
|
+
return { plainText: stdout, validationError: errorMessage }
|
|
447
|
+
} catch (e) {
|
|
448
|
+
logForDebugging(`Failed to parse hook output as JSON: ${e}`)
|
|
449
|
+
return { plainText: stdout }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function parseHttpHookOutput(body: string): {
|
|
454
|
+
json?: HookJSONOutput
|
|
455
|
+
validationError?: string
|
|
456
|
+
} {
|
|
457
|
+
const trimmed = body.trim()
|
|
458
|
+
|
|
459
|
+
if (trimmed === '') {
|
|
460
|
+
const validation = hookJSONOutputSchema().safeParse({})
|
|
461
|
+
if (validation.success) {
|
|
462
|
+
logForDebugging(
|
|
463
|
+
'HTTP hook returned empty body, treating as empty JSON object',
|
|
464
|
+
)
|
|
465
|
+
return { json: validation.data }
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!trimmed.startsWith('{')) {
|
|
470
|
+
const validationError = `HTTP hook must return JSON, but got non-JSON response body: ${trimmed.length > 200 ? trimmed.slice(0, 200) + '\u2026' : trimmed}`
|
|
471
|
+
logForDebugging(validationError)
|
|
472
|
+
return { validationError }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const result = validateHookJson(trimmed)
|
|
477
|
+
if ('json' in result) {
|
|
478
|
+
return result
|
|
479
|
+
}
|
|
480
|
+
logForDebugging(result.validationError)
|
|
481
|
+
return result
|
|
482
|
+
} catch (e) {
|
|
483
|
+
const validationError = `HTTP hook must return valid JSON, but parsing failed: ${e}`
|
|
484
|
+
logForDebugging(validationError)
|
|
485
|
+
return { validationError }
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function processHookJSONOutput({
|
|
490
|
+
json,
|
|
491
|
+
command,
|
|
492
|
+
hookName,
|
|
493
|
+
toolUseID,
|
|
494
|
+
hookEvent,
|
|
495
|
+
expectedHookEvent,
|
|
496
|
+
stdout,
|
|
497
|
+
stderr,
|
|
498
|
+
exitCode,
|
|
499
|
+
durationMs,
|
|
500
|
+
}: {
|
|
501
|
+
json: SyncHookJSONOutput
|
|
502
|
+
command: string
|
|
503
|
+
hookName: string
|
|
504
|
+
toolUseID: string
|
|
505
|
+
hookEvent: HookEvent
|
|
506
|
+
expectedHookEvent?: HookEvent
|
|
507
|
+
stdout?: string
|
|
508
|
+
stderr?: string
|
|
509
|
+
exitCode?: number
|
|
510
|
+
durationMs?: number
|
|
511
|
+
}): Partial<HookResult> {
|
|
512
|
+
const result: Partial<HookResult> = {}
|
|
513
|
+
|
|
514
|
+
// At this point we know it's a sync response
|
|
515
|
+
const syncJson = json
|
|
516
|
+
|
|
517
|
+
// Handle common elements
|
|
518
|
+
if (syncJson.continue === false) {
|
|
519
|
+
result.preventContinuation = true
|
|
520
|
+
if (syncJson.stopReason) {
|
|
521
|
+
result.stopReason = syncJson.stopReason
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (json.decision) {
|
|
526
|
+
switch (json.decision) {
|
|
527
|
+
case 'approve':
|
|
528
|
+
result.permissionBehavior = 'allow'
|
|
529
|
+
break
|
|
530
|
+
case 'block':
|
|
531
|
+
result.permissionBehavior = 'deny'
|
|
532
|
+
result.blockingError = {
|
|
533
|
+
blockingError: json.reason || 'Blocked by hook',
|
|
534
|
+
command,
|
|
535
|
+
}
|
|
536
|
+
break
|
|
537
|
+
default:
|
|
538
|
+
// Handle unknown decision types as errors
|
|
539
|
+
throw new Error(
|
|
540
|
+
`Unknown hook decision type: ${json.decision}. Valid types are: approve, block`,
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Handle systemMessage field
|
|
546
|
+
if (json.systemMessage) {
|
|
547
|
+
result.systemMessage = json.systemMessage
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Handle PreToolUse specific
|
|
551
|
+
if (
|
|
552
|
+
json.hookSpecificOutput?.hookEventName === 'PreToolUse' &&
|
|
553
|
+
json.hookSpecificOutput.permissionDecision
|
|
554
|
+
) {
|
|
555
|
+
switch (json.hookSpecificOutput.permissionDecision) {
|
|
556
|
+
case 'allow':
|
|
557
|
+
result.permissionBehavior = 'allow'
|
|
558
|
+
break
|
|
559
|
+
case 'deny':
|
|
560
|
+
result.permissionBehavior = 'deny'
|
|
561
|
+
result.blockingError = {
|
|
562
|
+
blockingError: json.reason || 'Blocked by hook',
|
|
563
|
+
command,
|
|
564
|
+
}
|
|
565
|
+
break
|
|
566
|
+
case 'ask':
|
|
567
|
+
result.permissionBehavior = 'ask'
|
|
568
|
+
break
|
|
569
|
+
default:
|
|
570
|
+
// Handle unknown decision types as errors
|
|
571
|
+
throw new Error(
|
|
572
|
+
`Unknown hook permissionDecision type: ${json.hookSpecificOutput.permissionDecision}. Valid types are: allow, deny, ask`,
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (result.permissionBehavior !== undefined && json.reason !== undefined) {
|
|
577
|
+
result.hookPermissionDecisionReason = json.reason
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Handle hookSpecificOutput
|
|
581
|
+
if (json.hookSpecificOutput) {
|
|
582
|
+
// Validate hook event name matches expected if provided
|
|
583
|
+
if (
|
|
584
|
+
expectedHookEvent &&
|
|
585
|
+
json.hookSpecificOutput.hookEventName !== expectedHookEvent
|
|
586
|
+
) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`Hook returned incorrect event name: expected '${expectedHookEvent}' but got '${json.hookSpecificOutput.hookEventName}'. Full stdout: ${jsonStringify(json, null, 2)}`,
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
switch (json.hookSpecificOutput.hookEventName) {
|
|
593
|
+
case 'PreToolUse':
|
|
594
|
+
// Override with more specific permission decision if provided
|
|
595
|
+
if (json.hookSpecificOutput.permissionDecision) {
|
|
596
|
+
switch (json.hookSpecificOutput.permissionDecision) {
|
|
597
|
+
case 'allow':
|
|
598
|
+
result.permissionBehavior = 'allow'
|
|
599
|
+
break
|
|
600
|
+
case 'deny':
|
|
601
|
+
result.permissionBehavior = 'deny'
|
|
602
|
+
result.blockingError = {
|
|
603
|
+
blockingError:
|
|
604
|
+
json.hookSpecificOutput.permissionDecisionReason ||
|
|
605
|
+
json.reason ||
|
|
606
|
+
'Blocked by hook',
|
|
607
|
+
command,
|
|
608
|
+
}
|
|
609
|
+
break
|
|
610
|
+
case 'ask':
|
|
611
|
+
result.permissionBehavior = 'ask'
|
|
612
|
+
break
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
result.hookPermissionDecisionReason =
|
|
616
|
+
json.hookSpecificOutput.permissionDecisionReason
|
|
617
|
+
// Extract updatedInput if provided
|
|
618
|
+
if (json.hookSpecificOutput.updatedInput) {
|
|
619
|
+
result.updatedInput = json.hookSpecificOutput.updatedInput
|
|
620
|
+
}
|
|
621
|
+
// Extract additionalContext if provided
|
|
622
|
+
result.additionalContext = json.hookSpecificOutput.additionalContext
|
|
623
|
+
break
|
|
624
|
+
case 'UserPromptSubmit':
|
|
625
|
+
result.additionalContext = json.hookSpecificOutput.additionalContext
|
|
626
|
+
break
|
|
627
|
+
case 'SessionStart':
|
|
628
|
+
result.additionalContext = json.hookSpecificOutput.additionalContext
|
|
629
|
+
result.initialUserMessage = json.hookSpecificOutput.initialUserMessage
|
|
630
|
+
if (
|
|
631
|
+
'watchPaths' in json.hookSpecificOutput &&
|
|
632
|
+
json.hookSpecificOutput.watchPaths
|
|
633
|
+
) {
|
|
634
|
+
result.watchPaths = json.hookSpecificOutput.watchPaths
|
|
635
|
+
}
|
|
636
|
+
break
|
|
637
|
+
case 'Setup':
|
|
638
|
+
result.additionalContext = json.hookSpecificOutput.additionalContext
|
|
639
|
+
break
|
|
640
|
+
case 'SubagentStart':
|
|
641
|
+
result.additionalContext = json.hookSpecificOutput.additionalContext
|
|
642
|
+
break
|
|
643
|
+
case 'PostToolUse':
|
|
644
|
+
result.additionalContext = json.hookSpecificOutput.additionalContext
|
|
645
|
+
// Extract updatedMCPToolOutput if provided
|
|
646
|
+
if (json.hookSpecificOutput.updatedMCPToolOutput) {
|
|
647
|
+
result.updatedMCPToolOutput =
|
|
648
|
+
json.hookSpecificOutput.updatedMCPToolOutput
|
|
649
|
+
}
|
|
650
|
+
break
|
|
651
|
+
case 'PostToolUseFailure':
|
|
652
|
+
result.additionalContext = json.hookSpecificOutput.additionalContext
|
|
653
|
+
break
|
|
654
|
+
case 'PermissionDenied':
|
|
655
|
+
result.retry = json.hookSpecificOutput.retry
|
|
656
|
+
break
|
|
657
|
+
case 'PermissionRequest':
|
|
658
|
+
// Extract the permission request decision
|
|
659
|
+
if (json.hookSpecificOutput.decision) {
|
|
660
|
+
result.permissionRequestResult = json.hookSpecificOutput.decision
|
|
661
|
+
// Also update permissionBehavior for consistency
|
|
662
|
+
result.permissionBehavior =
|
|
663
|
+
json.hookSpecificOutput.decision.behavior === 'allow'
|
|
664
|
+
? 'allow'
|
|
665
|
+
: 'deny'
|
|
666
|
+
if (
|
|
667
|
+
json.hookSpecificOutput.decision.behavior === 'allow' &&
|
|
668
|
+
json.hookSpecificOutput.decision.updatedInput
|
|
669
|
+
) {
|
|
670
|
+
result.updatedInput = json.hookSpecificOutput.decision.updatedInput
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
break
|
|
674
|
+
case 'Elicitation':
|
|
675
|
+
if (json.hookSpecificOutput.action) {
|
|
676
|
+
result.elicitationResponse = {
|
|
677
|
+
action: json.hookSpecificOutput.action,
|
|
678
|
+
content: json.hookSpecificOutput.content as
|
|
679
|
+
| ElicitationResponse['content']
|
|
680
|
+
| undefined,
|
|
681
|
+
}
|
|
682
|
+
if (json.hookSpecificOutput.action === 'decline') {
|
|
683
|
+
result.blockingError = {
|
|
684
|
+
blockingError: json.reason || 'Elicitation denied by hook',
|
|
685
|
+
command,
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
break
|
|
690
|
+
case 'ElicitationResult':
|
|
691
|
+
if (json.hookSpecificOutput.action) {
|
|
692
|
+
result.elicitationResultResponse = {
|
|
693
|
+
action: json.hookSpecificOutput.action,
|
|
694
|
+
content: json.hookSpecificOutput.content as
|
|
695
|
+
| ElicitationResponse['content']
|
|
696
|
+
| undefined,
|
|
697
|
+
}
|
|
698
|
+
if (json.hookSpecificOutput.action === 'decline') {
|
|
699
|
+
result.blockingError = {
|
|
700
|
+
blockingError:
|
|
701
|
+
json.reason || 'Elicitation result blocked by hook',
|
|
702
|
+
command,
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
...result,
|
|
712
|
+
message: result.blockingError
|
|
713
|
+
? createAttachmentMessage({
|
|
714
|
+
type: 'hook_blocking_error',
|
|
715
|
+
hookName,
|
|
716
|
+
toolUseID,
|
|
717
|
+
hookEvent,
|
|
718
|
+
blockingError: result.blockingError,
|
|
719
|
+
})
|
|
720
|
+
: createAttachmentMessage({
|
|
721
|
+
type: 'hook_success',
|
|
722
|
+
hookName,
|
|
723
|
+
toolUseID,
|
|
724
|
+
hookEvent,
|
|
725
|
+
// JSON-output hooks inject context via additionalContext →
|
|
726
|
+
// hook_additional_context, not this field. Empty content suppresses
|
|
727
|
+
// the trivial "X hook success: Success" system-reminder that
|
|
728
|
+
// otherwise pollutes every turn (messages.ts:3577 skips on '').
|
|
729
|
+
content: '',
|
|
730
|
+
stdout,
|
|
731
|
+
stderr,
|
|
732
|
+
exitCode,
|
|
733
|
+
command,
|
|
734
|
+
durationMs,
|
|
735
|
+
}),
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Execute a command-based hook using bash or PowerShell.
|
|
741
|
+
*
|
|
742
|
+
* Shell resolution: hook.shell → 'bash'. PowerShell hooks spawn pwsh
|
|
743
|
+
* with -NoProfile -NonInteractive -Command and skip bash-specific prep
|
|
744
|
+
* (POSIX path conversion, .sh auto-prepend, CLAUDE_CODE_SHELL_PREFIX).
|
|
745
|
+
* See docs/design/ps-shell-selection.md §5.1.
|
|
746
|
+
*/
|
|
747
|
+
async function execCommandHook(
|
|
748
|
+
hook: HookCommand & { type: 'command' },
|
|
749
|
+
hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion',
|
|
750
|
+
hookName: string,
|
|
751
|
+
jsonInput: string,
|
|
752
|
+
signal: AbortSignal,
|
|
753
|
+
hookId: string,
|
|
754
|
+
hookIndex?: number,
|
|
755
|
+
pluginRoot?: string,
|
|
756
|
+
pluginId?: string,
|
|
757
|
+
skillRoot?: string,
|
|
758
|
+
forceSyncExecution?: boolean,
|
|
759
|
+
requestPrompt?: (request: PromptRequest) => Promise<PromptResponse>,
|
|
760
|
+
): Promise<{
|
|
761
|
+
stdout: string
|
|
762
|
+
stderr: string
|
|
763
|
+
output: string
|
|
764
|
+
status: number
|
|
765
|
+
aborted?: boolean
|
|
766
|
+
backgrounded?: boolean
|
|
767
|
+
}> {
|
|
768
|
+
// Gated to once-per-session events to keep diag_log volume bounded.
|
|
769
|
+
// started/completed live inside the try/finally so setup-path throws
|
|
770
|
+
// don't orphan a started marker — that'd be indistinguishable from a hang.
|
|
771
|
+
const shouldEmitDiag =
|
|
772
|
+
hookEvent === 'SessionStart' ||
|
|
773
|
+
hookEvent === 'Setup' ||
|
|
774
|
+
hookEvent === 'SessionEnd'
|
|
775
|
+
const diagStartMs = Date.now()
|
|
776
|
+
let diagExitCode: number | undefined
|
|
777
|
+
let diagAborted = false
|
|
778
|
+
|
|
779
|
+
const isWindows = getPlatform() === 'windows'
|
|
780
|
+
|
|
781
|
+
// --
|
|
782
|
+
// Per-hook shell selection (phase 1 of docs/design/ps-shell-selection.md).
|
|
783
|
+
// Resolution order: hook.shell → DEFAULT_HOOK_SHELL. The defaultShell
|
|
784
|
+
// fallback (settings.defaultShell) is phase 2 — not wired yet.
|
|
785
|
+
//
|
|
786
|
+
// The bash path is the historical default and stays unchanged. The
|
|
787
|
+
// PowerShell path deliberately skips the Windows-specific bash
|
|
788
|
+
// accommodations (cygpath conversion, .sh auto-prepend, POSIX-quoted
|
|
789
|
+
// SHELL_PREFIX).
|
|
790
|
+
const shellType = hook.shell ?? DEFAULT_HOOK_SHELL
|
|
791
|
+
|
|
792
|
+
const isPowerShell = shellType === 'powershell'
|
|
793
|
+
|
|
794
|
+
// --
|
|
795
|
+
// Windows bash path: hooks run via Git Bash (Cygwin), NOT cmd.exe.
|
|
796
|
+
//
|
|
797
|
+
// This means every path we put into env vars or substitute into the command
|
|
798
|
+
// string MUST be a POSIX path (/c/Users/foo), not a Windows path
|
|
799
|
+
// (C:\Users\foo or C:/Users/foo). Git Bash cannot resolve Windows paths.
|
|
800
|
+
//
|
|
801
|
+
// windowsPathToPosixPath() is pure-JS regex conversion (no cygpath shell-out):
|
|
802
|
+
// C:\Users\foo -> /c/Users/foo, UNC preserved, slashes flipped. Memoized
|
|
803
|
+
// (LRU-500) so repeated calls are cheap.
|
|
804
|
+
//
|
|
805
|
+
// PowerShell path: use native paths — skip the conversion entirely.
|
|
806
|
+
// PowerShell expects Windows paths on Windows (and native paths on
|
|
807
|
+
// Unix where pwsh is also available).
|
|
808
|
+
const toHookPath =
|
|
809
|
+
isWindows && !isPowerShell
|
|
810
|
+
? (p: string) => windowsPathToPosixPath(p)
|
|
811
|
+
: (p: string) => p
|
|
812
|
+
|
|
813
|
+
// Set CLAUDE_PROJECT_DIR to the stable project root (not the worktree path).
|
|
814
|
+
// getProjectRoot() is never updated when entering a worktree, so hooks that
|
|
815
|
+
// reference $CLAUDE_PROJECT_DIR always resolve relative to the real repo root.
|
|
816
|
+
const projectDir = getProjectRoot()
|
|
817
|
+
|
|
818
|
+
// Substitute ${CLAUDE_PLUGIN_ROOT} and ${user_config.X} in the command string.
|
|
819
|
+
// Order matches MCP/LSP (plugin vars FIRST, then user config) so a user-
|
|
820
|
+
// entered value containing the literal text ${CLAUDE_PLUGIN_ROOT} is treated
|
|
821
|
+
// as opaque — not re-interpreted as a template.
|
|
822
|
+
let command = hook.command
|
|
823
|
+
let pluginOpts: ReturnType<typeof loadPluginOptions> | undefined
|
|
824
|
+
if (pluginRoot) {
|
|
825
|
+
// Plugin directory gone (orphan GC race, concurrent session deleted it):
|
|
826
|
+
// throw so callers yield a non-blocking error. Running would fail — and
|
|
827
|
+
// `python3 <missing>.py` exits 2, the hook protocol's "block" code, which
|
|
828
|
+
// bricks UserPromptSubmit/Stop until restart. The pre-check is necessary
|
|
829
|
+
// because exit-2-from-missing-script is indistinguishable from an
|
|
830
|
+
// intentional block after spawn.
|
|
831
|
+
if (!(await pathExists(pluginRoot))) {
|
|
832
|
+
throw new Error(
|
|
833
|
+
`Plugin directory does not exist: ${pluginRoot}` +
|
|
834
|
+
(pluginId ? ` (${pluginId} — run /plugin to reinstall)` : ''),
|
|
835
|
+
)
|
|
836
|
+
}
|
|
837
|
+
// Inline both ROOT and DATA substitution instead of calling
|
|
838
|
+
// substitutePluginVariables(). That helper normalizes \ → / on Windows
|
|
839
|
+
// unconditionally — correct for bash (toHookPath already produced /c/...
|
|
840
|
+
// so it's a no-op) but wrong for PS where toHookPath is identity and we
|
|
841
|
+
// want native C:\... backslashes. Inlining also lets us use the function-
|
|
842
|
+
// form .replace() so paths containing $ aren't mangled by $-pattern
|
|
843
|
+
// interpretation (rare but possible: \\server\c$\plugin).
|
|
844
|
+
const rootPath = toHookPath(pluginRoot)
|
|
845
|
+
command = command.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => rootPath)
|
|
846
|
+
if (pluginId) {
|
|
847
|
+
const dataPath = toHookPath(getPluginDataDir(pluginId))
|
|
848
|
+
command = command.replace(/\$\{CLAUDE_PLUGIN_DATA\}/g, () => dataPath)
|
|
849
|
+
}
|
|
850
|
+
if (pluginId) {
|
|
851
|
+
pluginOpts = loadPluginOptions(pluginId)
|
|
852
|
+
// Throws if a referenced key is missing — that means the hook uses a key
|
|
853
|
+
// that's either not declared in manifest.userConfig or not yet configured.
|
|
854
|
+
// Caught upstream like any other hook exec failure.
|
|
855
|
+
command = substituteUserConfigVariables(command, pluginOpts)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// On Windows (bash only), auto-prepend `bash` for .sh scripts so they
|
|
860
|
+
// execute instead of opening in the default file handler. PowerShell
|
|
861
|
+
// runs .ps1 files natively — no prepend needed.
|
|
862
|
+
if (isWindows && !isPowerShell && command.trim().match(/\.sh(\s|$|")/)) {
|
|
863
|
+
if (!command.trim().startsWith('bash ')) {
|
|
864
|
+
command = `bash ${command}`
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// CLAUDE_CODE_SHELL_PREFIX wraps the command via POSIX quoting
|
|
869
|
+
// (formatShellPrefixCommand uses shell-quote). This makes no sense for
|
|
870
|
+
// PowerShell — see design §8.1. For now PS hooks ignore the prefix;
|
|
871
|
+
// a CLAUDE_CODE_PS_SHELL_PREFIX (or shell-aware prefix) is a follow-up.
|
|
872
|
+
const finalCommand =
|
|
873
|
+
!isPowerShell && process.env.CLAUDE_CODE_SHELL_PREFIX
|
|
874
|
+
? formatShellPrefixCommand(process.env.CLAUDE_CODE_SHELL_PREFIX, command)
|
|
875
|
+
: command
|
|
876
|
+
|
|
877
|
+
const hookTimeoutMs = hook.timeout
|
|
878
|
+
? hook.timeout * 1000
|
|
879
|
+
: TOOL_HOOK_EXECUTION_TIMEOUT_MS
|
|
880
|
+
|
|
881
|
+
// Build env vars — all paths go through toHookPath for Windows POSIX conversion
|
|
882
|
+
const envVars: NodeJS.ProcessEnv = {
|
|
883
|
+
...subprocessEnv(),
|
|
884
|
+
CLAUDE_PROJECT_DIR: toHookPath(projectDir),
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Plugin and skill hooks both set CLAUDE_PLUGIN_ROOT (skills use the same
|
|
888
|
+
// name for consistency — skills can migrate to plugins without code changes)
|
|
889
|
+
if (pluginRoot) {
|
|
890
|
+
envVars.CLAUDE_PLUGIN_ROOT = toHookPath(pluginRoot)
|
|
891
|
+
if (pluginId) {
|
|
892
|
+
envVars.CLAUDE_PLUGIN_DATA = toHookPath(getPluginDataDir(pluginId))
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// Expose plugin options as env vars too, so hooks can read them without
|
|
896
|
+
// ${user_config.X} in the command string. Sensitive values included — hooks
|
|
897
|
+
// run the user's own code, same trust boundary as reading keychain directly.
|
|
898
|
+
if (pluginOpts) {
|
|
899
|
+
for (const [key, value] of Object.entries(pluginOpts)) {
|
|
900
|
+
// Sanitize non-identifier chars (bash can't ref $FOO-BAR). The schema
|
|
901
|
+
// at schemas.ts:611 now constrains keys to /^[A-Za-z_]\w*$/ so this is
|
|
902
|
+
// belt-and-suspenders, but cheap insurance if someone bypasses the schema.
|
|
903
|
+
const envKey = key.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()
|
|
904
|
+
envVars[`CLAUDE_PLUGIN_OPTION_${envKey}`] = String(value)
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (skillRoot) {
|
|
908
|
+
envVars.CLAUDE_PLUGIN_ROOT = toHookPath(skillRoot)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// CLAUDE_ENV_FILE points to a .sh file that the hook writes env var
|
|
912
|
+
// definitions into; getSessionEnvironmentScript() concatenates them and
|
|
913
|
+
// bashProvider injects the content into bash commands. A PS hook would
|
|
914
|
+
// naturally write PS syntax ($env:FOO = 'bar'), which bash can't parse.
|
|
915
|
+
// Skip for PS — consistent with how .sh prepend and SHELL_PREFIX are
|
|
916
|
+
// already bash-only above.
|
|
917
|
+
if (
|
|
918
|
+
!isPowerShell &&
|
|
919
|
+
(hookEvent === 'SessionStart' ||
|
|
920
|
+
hookEvent === 'Setup' ||
|
|
921
|
+
hookEvent === 'CwdChanged' ||
|
|
922
|
+
hookEvent === 'FileChanged') &&
|
|
923
|
+
hookIndex !== undefined
|
|
924
|
+
) {
|
|
925
|
+
envVars.CLAUDE_ENV_FILE = await getHookEnvFilePath(hookEvent, hookIndex)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// When agent worktrees are removed, getCwd() may return a deleted path via
|
|
929
|
+
// AsyncLocalStorage. Validate before spawning since spawn() emits async
|
|
930
|
+
// 'error' events for missing cwd rather than throwing synchronously.
|
|
931
|
+
const hookCwd = getCwd()
|
|
932
|
+
const safeCwd = (await pathExists(hookCwd)) ? hookCwd : getOriginalCwd()
|
|
933
|
+
if (safeCwd !== hookCwd) {
|
|
934
|
+
logForDebugging(
|
|
935
|
+
`Hooks: cwd ${hookCwd} not found, falling back to original cwd`,
|
|
936
|
+
{ level: 'warn' },
|
|
937
|
+
)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// --
|
|
941
|
+
// Spawn. Two completely separate paths:
|
|
942
|
+
//
|
|
943
|
+
// Bash: spawn(cmd, [], { shell: <gitBashPath | true> }) — the shell
|
|
944
|
+
// option makes Node pass the whole string to the shell for parsing.
|
|
945
|
+
//
|
|
946
|
+
// PowerShell: spawn(pwshPath, ['-NoProfile', '-NonInteractive',
|
|
947
|
+
// '-Command', cmd]) — explicit argv, no shell option. -NoProfile
|
|
948
|
+
// skips user profile scripts (faster, deterministic).
|
|
949
|
+
// -NonInteractive fails fast instead of prompting.
|
|
950
|
+
//
|
|
951
|
+
// The Git Bash hard-exit in findGitBashPath() is still in place for
|
|
952
|
+
// bash hooks. PowerShell hooks never call it, so a Windows user with
|
|
953
|
+
// only pwsh and shell: 'powershell' on every hook could in theory run
|
|
954
|
+
// without Git Bash — but init.ts still calls setShellIfWindows() on
|
|
955
|
+
// startup, which will exit first. Relaxing that is phase 1 of the
|
|
956
|
+
// design's implementation order (separate PR).
|
|
957
|
+
let child: ChildProcessWithoutNullStreams
|
|
958
|
+
if (shellType === 'powershell') {
|
|
959
|
+
const pwshPath = await getCachedPowerShellPath()
|
|
960
|
+
if (!pwshPath) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
`Hook "${hook.command}" has shell: 'powershell' but no PowerShell ` +
|
|
963
|
+
`executable (pwsh or powershell) was found on PATH. Install ` +
|
|
964
|
+
`PowerShell, or remove "shell": "powershell" to use bash.`,
|
|
965
|
+
)
|
|
966
|
+
}
|
|
967
|
+
child = spawn(pwshPath, buildPowerShellArgs(finalCommand), {
|
|
968
|
+
env: envVars,
|
|
969
|
+
cwd: safeCwd,
|
|
970
|
+
// Prevent visible console window on Windows (no-op on other platforms)
|
|
971
|
+
windowsHide: true,
|
|
972
|
+
}) as ChildProcessWithoutNullStreams
|
|
973
|
+
} else {
|
|
974
|
+
// On Windows, use Git Bash explicitly (cmd.exe can't run bash syntax).
|
|
975
|
+
// On other platforms, shell: true uses /bin/sh.
|
|
976
|
+
const shell = isWindows ? findGitBashPath() : true
|
|
977
|
+
child = spawn(finalCommand, [], {
|
|
978
|
+
env: envVars,
|
|
979
|
+
cwd: safeCwd,
|
|
980
|
+
shell,
|
|
981
|
+
// Prevent visible console window on Windows (no-op on other platforms)
|
|
982
|
+
windowsHide: true,
|
|
983
|
+
}) as ChildProcessWithoutNullStreams
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Hooks use pipe mode — stdout must be streamed into JS so we can parse
|
|
987
|
+
// the first response line to detect async hooks ({"async": true}).
|
|
988
|
+
const hookTaskOutput = new TaskOutput(`hook_${child.pid}`, null)
|
|
989
|
+
const shellCommand = wrapSpawn(child, signal, hookTimeoutMs, hookTaskOutput)
|
|
990
|
+
// Track whether shellCommand ownership was transferred (e.g., to async hook registry)
|
|
991
|
+
let shellCommandTransferred = false
|
|
992
|
+
// Track whether stdin has already been written (to avoid "write after end" errors)
|
|
993
|
+
let stdinWritten = false
|
|
994
|
+
|
|
995
|
+
if ((hook.async || hook.asyncRewake) && !forceSyncExecution) {
|
|
996
|
+
const processId = `async_hook_${child.pid}`
|
|
997
|
+
logForDebugging(
|
|
998
|
+
`Hooks: Config-based async hook, backgrounding process ${processId}`,
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
// Write stdin before backgrounding so the hook receives its input.
|
|
1002
|
+
// The trailing newline matches the sync path (L1000). Without it,
|
|
1003
|
+
// bash `read -r line` returns exit 1 (EOF before delimiter) — the
|
|
1004
|
+
// variable IS populated but `if read -r line; then ...` skips the
|
|
1005
|
+
// branch. See gh-30509 / CC-161.
|
|
1006
|
+
child.stdin.write(jsonInput + '\n', 'utf8')
|
|
1007
|
+
child.stdin.end()
|
|
1008
|
+
stdinWritten = true
|
|
1009
|
+
|
|
1010
|
+
const backgrounded = executeInBackground({
|
|
1011
|
+
processId,
|
|
1012
|
+
hookId,
|
|
1013
|
+
shellCommand,
|
|
1014
|
+
asyncResponse: { async: true, asyncTimeout: hookTimeoutMs },
|
|
1015
|
+
hookEvent,
|
|
1016
|
+
hookName,
|
|
1017
|
+
command: hook.command,
|
|
1018
|
+
asyncRewake: hook.asyncRewake,
|
|
1019
|
+
pluginId,
|
|
1020
|
+
})
|
|
1021
|
+
if (backgrounded) {
|
|
1022
|
+
return {
|
|
1023
|
+
stdout: '',
|
|
1024
|
+
stderr: '',
|
|
1025
|
+
output: '',
|
|
1026
|
+
status: 0,
|
|
1027
|
+
backgrounded: true,
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
let stdout = ''
|
|
1033
|
+
let stderr = ''
|
|
1034
|
+
let output = ''
|
|
1035
|
+
|
|
1036
|
+
// Set up output data collection with explicit UTF-8 encoding
|
|
1037
|
+
child.stdout.setEncoding('utf8')
|
|
1038
|
+
child.stderr.setEncoding('utf8')
|
|
1039
|
+
|
|
1040
|
+
let initialResponseChecked = false
|
|
1041
|
+
|
|
1042
|
+
let asyncResolve:
|
|
1043
|
+
| ((result: {
|
|
1044
|
+
stdout: string
|
|
1045
|
+
stderr: string
|
|
1046
|
+
output: string
|
|
1047
|
+
status: number
|
|
1048
|
+
}) => void)
|
|
1049
|
+
| null = null
|
|
1050
|
+
const childIsAsyncPromise = new Promise<{
|
|
1051
|
+
stdout: string
|
|
1052
|
+
stderr: string
|
|
1053
|
+
output: string
|
|
1054
|
+
status: number
|
|
1055
|
+
aborted?: boolean
|
|
1056
|
+
}>(resolve => {
|
|
1057
|
+
asyncResolve = resolve
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
// Track trimmed prompt-request lines we processed so we can strip them
|
|
1061
|
+
// from final stdout by content match (no index tracking → no index drift)
|
|
1062
|
+
const processedPromptLines = new Set<string>()
|
|
1063
|
+
// Serialize async prompt handling so responses are sent in order
|
|
1064
|
+
let promptChain = Promise.resolve()
|
|
1065
|
+
// Line buffer for detecting prompt requests in streaming output
|
|
1066
|
+
let lineBuffer = ''
|
|
1067
|
+
|
|
1068
|
+
child.stdout.on('data', data => {
|
|
1069
|
+
stdout += data
|
|
1070
|
+
output += data
|
|
1071
|
+
|
|
1072
|
+
// When requestPrompt is provided, parse stdout line-by-line for prompt requests
|
|
1073
|
+
if (requestPrompt) {
|
|
1074
|
+
lineBuffer += data
|
|
1075
|
+
const lines = lineBuffer.split('\n')
|
|
1076
|
+
lineBuffer = lines.pop() ?? '' // last element is an incomplete line
|
|
1077
|
+
|
|
1078
|
+
for (const line of lines) {
|
|
1079
|
+
const trimmed = line.trim()
|
|
1080
|
+
if (!trimmed) continue
|
|
1081
|
+
|
|
1082
|
+
try {
|
|
1083
|
+
const parsed = jsonParse(trimmed)
|
|
1084
|
+
const validation = promptRequestSchema().safeParse(parsed)
|
|
1085
|
+
if (validation.success) {
|
|
1086
|
+
processedPromptLines.add(trimmed)
|
|
1087
|
+
logForDebugging(
|
|
1088
|
+
`Hooks: Detected prompt request from hook: ${trimmed}`,
|
|
1089
|
+
)
|
|
1090
|
+
// Chain the async handling to serialize prompt responses
|
|
1091
|
+
const promptReq = validation.data
|
|
1092
|
+
const reqPrompt = requestPrompt
|
|
1093
|
+
promptChain = promptChain.then(async () => {
|
|
1094
|
+
try {
|
|
1095
|
+
const response = await reqPrompt(promptReq)
|
|
1096
|
+
child.stdin.write(jsonStringify(response) + '\n', 'utf8')
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
logForDebugging(`Hooks: Prompt request handling failed: ${err}`)
|
|
1099
|
+
// User cancelled or prompt failed — close stdin so the hook
|
|
1100
|
+
// process doesn't hang waiting for input
|
|
1101
|
+
child.stdin.destroy()
|
|
1102
|
+
}
|
|
1103
|
+
})
|
|
1104
|
+
continue
|
|
1105
|
+
}
|
|
1106
|
+
} catch {
|
|
1107
|
+
// Not JSON, just a normal line
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Check for async response on first line of output. The async protocol is:
|
|
1113
|
+
// hook emits {"async":true,...} as its FIRST line, then its normal output.
|
|
1114
|
+
// We must parse ONLY the first line — if the process is fast and writes more
|
|
1115
|
+
// before this 'data' event fires, parsing the full accumulated stdout fails
|
|
1116
|
+
// and an async hook blocks for its full duration instead of backgrounding.
|
|
1117
|
+
if (!initialResponseChecked) {
|
|
1118
|
+
const firstLine = firstLineOf(stdout).trim()
|
|
1119
|
+
if (!firstLine.includes('}')) return
|
|
1120
|
+
initialResponseChecked = true
|
|
1121
|
+
logForDebugging(`Hooks: Checking first line for async: ${firstLine}`)
|
|
1122
|
+
try {
|
|
1123
|
+
const parsed = jsonParse(firstLine)
|
|
1124
|
+
logForDebugging(
|
|
1125
|
+
`Hooks: Parsed initial response: ${jsonStringify(parsed)}`,
|
|
1126
|
+
)
|
|
1127
|
+
if (isAsyncHookJSONOutput(parsed) && !forceSyncExecution) {
|
|
1128
|
+
const processId = `async_hook_${child.pid}`
|
|
1129
|
+
logForDebugging(
|
|
1130
|
+
`Hooks: Detected async hook, backgrounding process ${processId}`,
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
const backgrounded = executeInBackground({
|
|
1134
|
+
processId,
|
|
1135
|
+
hookId,
|
|
1136
|
+
shellCommand,
|
|
1137
|
+
asyncResponse: parsed,
|
|
1138
|
+
hookEvent,
|
|
1139
|
+
hookName,
|
|
1140
|
+
command: hook.command,
|
|
1141
|
+
pluginId,
|
|
1142
|
+
})
|
|
1143
|
+
if (backgrounded) {
|
|
1144
|
+
shellCommandTransferred = true
|
|
1145
|
+
asyncResolve?.({
|
|
1146
|
+
stdout,
|
|
1147
|
+
stderr,
|
|
1148
|
+
output,
|
|
1149
|
+
status: 0,
|
|
1150
|
+
})
|
|
1151
|
+
}
|
|
1152
|
+
} else if (isAsyncHookJSONOutput(parsed) && forceSyncExecution) {
|
|
1153
|
+
logForDebugging(
|
|
1154
|
+
`Hooks: Detected async hook but forceSyncExecution is true, waiting for completion`,
|
|
1155
|
+
)
|
|
1156
|
+
} else {
|
|
1157
|
+
logForDebugging(
|
|
1158
|
+
`Hooks: Initial response is not async, continuing normal processing`,
|
|
1159
|
+
)
|
|
1160
|
+
}
|
|
1161
|
+
} catch (e) {
|
|
1162
|
+
logForDebugging(`Hooks: Failed to parse initial response as JSON: ${e}`)
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
child.stderr.on('data', data => {
|
|
1168
|
+
stderr += data
|
|
1169
|
+
output += data
|
|
1170
|
+
})
|
|
1171
|
+
|
|
1172
|
+
const stopProgressInterval = startHookProgressInterval({
|
|
1173
|
+
hookId,
|
|
1174
|
+
hookName,
|
|
1175
|
+
hookEvent,
|
|
1176
|
+
getOutput: async () => ({ stdout, stderr, output }),
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
// Wait for stdout and stderr streams to finish before considering output complete
|
|
1180
|
+
// This prevents a race condition where 'close' fires before all 'data' events are processed
|
|
1181
|
+
const stdoutEndPromise = new Promise<void>(resolve => {
|
|
1182
|
+
child.stdout.on('end', () => resolve())
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
const stderrEndPromise = new Promise<void>(resolve => {
|
|
1186
|
+
child.stderr.on('end', () => resolve())
|
|
1187
|
+
})
|
|
1188
|
+
|
|
1189
|
+
// Write to stdin, making sure to handle EPIPE errors that can happen when
|
|
1190
|
+
// the hook command exits before reading all input.
|
|
1191
|
+
// Note: EPIPE handling is difficult to set up in testing since Bun and Node
|
|
1192
|
+
// have different behaviors.
|
|
1193
|
+
// TODO: Add tests for EPIPE handling.
|
|
1194
|
+
// Skip if stdin was already written (e.g., by config-based async hook path)
|
|
1195
|
+
const stdinWritePromise = stdinWritten
|
|
1196
|
+
? Promise.resolve()
|
|
1197
|
+
: new Promise<void>((resolve, reject) => {
|
|
1198
|
+
child.stdin.on('error', err => {
|
|
1199
|
+
// When requestPrompt is provided, stdin stays open for prompt responses.
|
|
1200
|
+
// EPIPE errors from later writes (after process exits) are expected -- suppress them.
|
|
1201
|
+
if (!requestPrompt) {
|
|
1202
|
+
reject(err)
|
|
1203
|
+
} else {
|
|
1204
|
+
logForDebugging(
|
|
1205
|
+
`Hooks: stdin error during prompt flow (likely process exited): ${err}`,
|
|
1206
|
+
)
|
|
1207
|
+
}
|
|
1208
|
+
})
|
|
1209
|
+
// Explicitly specify UTF-8 encoding to ensure proper handling of Unicode characters
|
|
1210
|
+
child.stdin.write(jsonInput + '\n', 'utf8')
|
|
1211
|
+
// When requestPrompt is provided, keep stdin open for prompt responses
|
|
1212
|
+
if (!requestPrompt) {
|
|
1213
|
+
child.stdin.end()
|
|
1214
|
+
}
|
|
1215
|
+
resolve()
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
// Create promise for child process error
|
|
1219
|
+
const childErrorPromise = new Promise<never>((_, reject) => {
|
|
1220
|
+
child.on('error', reject)
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1223
|
+
// Create promise for child process close - but only resolve after streams end
|
|
1224
|
+
// to ensure all output has been collected
|
|
1225
|
+
const childClosePromise = new Promise<{
|
|
1226
|
+
stdout: string
|
|
1227
|
+
stderr: string
|
|
1228
|
+
output: string
|
|
1229
|
+
status: number
|
|
1230
|
+
aborted?: boolean
|
|
1231
|
+
}>(resolve => {
|
|
1232
|
+
let exitCode: number | null = null
|
|
1233
|
+
|
|
1234
|
+
child.on('close', code => {
|
|
1235
|
+
exitCode = code ?? 1
|
|
1236
|
+
|
|
1237
|
+
// Wait for both streams to end before resolving with the final output
|
|
1238
|
+
void Promise.all([stdoutEndPromise, stderrEndPromise]).then(() => {
|
|
1239
|
+
// Strip lines we processed as prompt requests so parseHookOutput
|
|
1240
|
+
// only sees the final hook result. Content-matching against the set
|
|
1241
|
+
// of actually-processed lines means prompt JSON can never leak
|
|
1242
|
+
// through (fail-closed), regardless of line positioning.
|
|
1243
|
+
const finalStdout =
|
|
1244
|
+
processedPromptLines.size === 0
|
|
1245
|
+
? stdout
|
|
1246
|
+
: stdout
|
|
1247
|
+
.split('\n')
|
|
1248
|
+
.filter(line => !processedPromptLines.has(line.trim()))
|
|
1249
|
+
.join('\n')
|
|
1250
|
+
|
|
1251
|
+
resolve({
|
|
1252
|
+
stdout: finalStdout,
|
|
1253
|
+
stderr,
|
|
1254
|
+
output,
|
|
1255
|
+
status: exitCode!,
|
|
1256
|
+
aborted: signal.aborted,
|
|
1257
|
+
})
|
|
1258
|
+
})
|
|
1259
|
+
})
|
|
1260
|
+
})
|
|
1261
|
+
|
|
1262
|
+
// Race between stdin write, async detection, and process completion
|
|
1263
|
+
try {
|
|
1264
|
+
if (shouldEmitDiag) {
|
|
1265
|
+
logForDiagnosticsNoPII('info', 'hook_spawn_started', {
|
|
1266
|
+
hook_event_name: hookEvent,
|
|
1267
|
+
index: hookIndex,
|
|
1268
|
+
})
|
|
1269
|
+
}
|
|
1270
|
+
await Promise.race([stdinWritePromise, childErrorPromise])
|
|
1271
|
+
|
|
1272
|
+
// Wait for any pending prompt responses before resolving
|
|
1273
|
+
const result = await Promise.race([
|
|
1274
|
+
childIsAsyncPromise,
|
|
1275
|
+
childClosePromise,
|
|
1276
|
+
childErrorPromise,
|
|
1277
|
+
])
|
|
1278
|
+
// Ensure all queued prompt responses have been sent
|
|
1279
|
+
await promptChain
|
|
1280
|
+
diagExitCode = result.status
|
|
1281
|
+
diagAborted = result.aborted ?? false
|
|
1282
|
+
return result
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
// Handle errors from stdin write or child process
|
|
1285
|
+
const code = getErrnoCode(error)
|
|
1286
|
+
diagExitCode = 1
|
|
1287
|
+
|
|
1288
|
+
if (code === 'EPIPE') {
|
|
1289
|
+
logForDebugging(
|
|
1290
|
+
'EPIPE error while writing to hook stdin (hook command likely closed early)',
|
|
1291
|
+
)
|
|
1292
|
+
const errMsg =
|
|
1293
|
+
'Hook command closed stdin before hook input was fully written (EPIPE)'
|
|
1294
|
+
return {
|
|
1295
|
+
stdout: '',
|
|
1296
|
+
stderr: errMsg,
|
|
1297
|
+
output: errMsg,
|
|
1298
|
+
status: 1,
|
|
1299
|
+
}
|
|
1300
|
+
} else if (code === 'ABORT_ERR') {
|
|
1301
|
+
diagAborted = true
|
|
1302
|
+
return {
|
|
1303
|
+
stdout: '',
|
|
1304
|
+
stderr: 'Hook cancelled',
|
|
1305
|
+
output: 'Hook cancelled',
|
|
1306
|
+
status: 1,
|
|
1307
|
+
aborted: true,
|
|
1308
|
+
}
|
|
1309
|
+
} else {
|
|
1310
|
+
const errorMsg = errorMessage(error)
|
|
1311
|
+
const errOutput = `Error occurred while executing hook command: ${errorMsg}`
|
|
1312
|
+
return {
|
|
1313
|
+
stdout: '',
|
|
1314
|
+
stderr: errOutput,
|
|
1315
|
+
output: errOutput,
|
|
1316
|
+
status: 1,
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
} finally {
|
|
1320
|
+
if (shouldEmitDiag) {
|
|
1321
|
+
logForDiagnosticsNoPII('info', 'hook_spawn_completed', {
|
|
1322
|
+
hook_event_name: hookEvent,
|
|
1323
|
+
index: hookIndex,
|
|
1324
|
+
duration_ms: Date.now() - diagStartMs,
|
|
1325
|
+
exit_code: diagExitCode,
|
|
1326
|
+
aborted: diagAborted,
|
|
1327
|
+
})
|
|
1328
|
+
}
|
|
1329
|
+
stopProgressInterval()
|
|
1330
|
+
// Clean up stream resources unless ownership was transferred (e.g., to async hook registry)
|
|
1331
|
+
if (!shellCommandTransferred) {
|
|
1332
|
+
shellCommand.cleanup()
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Check if a match query matches a hook matcher pattern
|
|
1339
|
+
* @param matchQuery The query to match (e.g., 'Write', 'Edit', 'Bash')
|
|
1340
|
+
* @param matcher The matcher pattern - can be:
|
|
1341
|
+
* - Simple string for exact match (e.g., 'Write')
|
|
1342
|
+
* - Pipe-separated list for multiple exact matches (e.g., 'Write|Edit')
|
|
1343
|
+
* - Regex pattern (e.g., '^Write.*', '.*', '^(Write|Edit)$')
|
|
1344
|
+
* @returns true if the query matches the pattern
|
|
1345
|
+
*/
|
|
1346
|
+
function matchesPattern(matchQuery: string, matcher: string): boolean {
|
|
1347
|
+
if (!matcher || matcher === '*') {
|
|
1348
|
+
return true
|
|
1349
|
+
}
|
|
1350
|
+
// Check if it's a simple string or pipe-separated list (no regex special chars except |)
|
|
1351
|
+
if (/^[a-zA-Z0-9_|]+$/.test(matcher)) {
|
|
1352
|
+
// Handle pipe-separated exact matches
|
|
1353
|
+
if (matcher.includes('|')) {
|
|
1354
|
+
const patterns = matcher
|
|
1355
|
+
.split('|')
|
|
1356
|
+
.map(p => normalizeLegacyToolName(p.trim()))
|
|
1357
|
+
return patterns.includes(matchQuery)
|
|
1358
|
+
}
|
|
1359
|
+
// Simple exact match
|
|
1360
|
+
return matchQuery === normalizeLegacyToolName(matcher)
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Otherwise treat as regex
|
|
1364
|
+
try {
|
|
1365
|
+
const regex = new RegExp(matcher)
|
|
1366
|
+
if (regex.test(matchQuery)) {
|
|
1367
|
+
return true
|
|
1368
|
+
}
|
|
1369
|
+
// Also test against legacy names so patterns like "^Task$" still match
|
|
1370
|
+
for (const legacyName of getLegacyToolNames(matchQuery)) {
|
|
1371
|
+
if (regex.test(legacyName)) {
|
|
1372
|
+
return true
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return false
|
|
1376
|
+
} catch {
|
|
1377
|
+
// If the regex is invalid, log error and return false
|
|
1378
|
+
logForDebugging(`Invalid regex pattern in hook matcher: ${matcher}`)
|
|
1379
|
+
return false
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
type IfConditionMatcher = (ifCondition: string) => boolean
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Prepare a matcher for hook `if` conditions. Expensive work (tool lookup,
|
|
1387
|
+
* Zod validation, tree-sitter parsing for Bash) happens once here; the
|
|
1388
|
+
* returned closure is called per hook. Returns undefined for non-tool events.
|
|
1389
|
+
*/
|
|
1390
|
+
async function prepareIfConditionMatcher(
|
|
1391
|
+
hookInput: HookInput,
|
|
1392
|
+
tools: Tools | undefined,
|
|
1393
|
+
): Promise<IfConditionMatcher | undefined> {
|
|
1394
|
+
if (
|
|
1395
|
+
hookInput.hook_event_name !== 'PreToolUse' &&
|
|
1396
|
+
hookInput.hook_event_name !== 'PostToolUse' &&
|
|
1397
|
+
hookInput.hook_event_name !== 'PostToolUseFailure' &&
|
|
1398
|
+
hookInput.hook_event_name !== 'PermissionRequest'
|
|
1399
|
+
) {
|
|
1400
|
+
return undefined
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const toolName = normalizeLegacyToolName(hookInput.tool_name)
|
|
1404
|
+
const tool = tools && findToolByName(tools, hookInput.tool_name)
|
|
1405
|
+
const input = tool?.inputSchema.safeParse(hookInput.tool_input)
|
|
1406
|
+
const patternMatcher =
|
|
1407
|
+
input?.success && tool?.preparePermissionMatcher
|
|
1408
|
+
? await tool.preparePermissionMatcher(input.data)
|
|
1409
|
+
: undefined
|
|
1410
|
+
|
|
1411
|
+
return ifCondition => {
|
|
1412
|
+
const parsed = permissionRuleValueFromString(ifCondition)
|
|
1413
|
+
if (normalizeLegacyToolName(parsed.toolName) !== toolName) {
|
|
1414
|
+
return false
|
|
1415
|
+
}
|
|
1416
|
+
if (!parsed.ruleContent) {
|
|
1417
|
+
return true
|
|
1418
|
+
}
|
|
1419
|
+
return patternMatcher ? patternMatcher(parsed.ruleContent) : false
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
type FunctionHookMatcher = {
|
|
1424
|
+
matcher: string
|
|
1425
|
+
hooks: FunctionHook[]
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* A hook paired with optional plugin context.
|
|
1430
|
+
* Used when returning matched hooks so we can apply plugin env vars at execution time.
|
|
1431
|
+
*/
|
|
1432
|
+
type MatchedHook = {
|
|
1433
|
+
hook: HookCommand | HookCallback | FunctionHook
|
|
1434
|
+
pluginRoot?: string
|
|
1435
|
+
pluginId?: string
|
|
1436
|
+
skillRoot?: string
|
|
1437
|
+
hookSource?: string
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function isInternalHook(matched: MatchedHook): boolean {
|
|
1441
|
+
return matched.hook.type === 'callback' && matched.hook.internal === true
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Build a dedup key for a matched hook, namespaced by source context.
|
|
1446
|
+
*
|
|
1447
|
+
* Settings-file hooks (no pluginRoot/skillRoot) share the '' prefix so the
|
|
1448
|
+
* same command defined in user/project/local still collapses to one — the
|
|
1449
|
+
* original intent of the dedup. Plugin/skill hooks get their root as the
|
|
1450
|
+
* prefix, so two plugins sharing an unexpanded `${CLAUDE_PLUGIN_ROOT}/hook.sh`
|
|
1451
|
+
* template don't collapse: after expansion they point to different files.
|
|
1452
|
+
*/
|
|
1453
|
+
function hookDedupKey(m: MatchedHook, payload: string): string {
|
|
1454
|
+
return `${m.pluginRoot ?? m.skillRoot ?? ''}\0${payload}`
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Build a map of {sanitizedPluginName: hookCount} from matched hooks.
|
|
1459
|
+
* Only logs actual names for official marketplace plugins; others become 'third-party'.
|
|
1460
|
+
*/
|
|
1461
|
+
function getPluginHookCounts(
|
|
1462
|
+
hooks: MatchedHook[],
|
|
1463
|
+
): Record<string, number> | undefined {
|
|
1464
|
+
const pluginHooks = hooks.filter(h => h.pluginId)
|
|
1465
|
+
if (pluginHooks.length === 0) {
|
|
1466
|
+
return undefined
|
|
1467
|
+
}
|
|
1468
|
+
const counts: Record<string, number> = {}
|
|
1469
|
+
for (const h of pluginHooks) {
|
|
1470
|
+
const atIndex = h.pluginId!.lastIndexOf('@')
|
|
1471
|
+
const isOfficial =
|
|
1472
|
+
atIndex > 0 &&
|
|
1473
|
+
ALLOWED_OFFICIAL_MARKETPLACE_NAMES.has(h.pluginId!.slice(atIndex + 1))
|
|
1474
|
+
const key = isOfficial ? h.pluginId! : 'third-party'
|
|
1475
|
+
counts[key] = (counts[key] || 0) + 1
|
|
1476
|
+
}
|
|
1477
|
+
return counts
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Build a map of {hookType: count} from matched hooks.
|
|
1483
|
+
*/
|
|
1484
|
+
function getHookTypeCounts(hooks: MatchedHook[]): Record<string, number> {
|
|
1485
|
+
const counts: Record<string, number> = {}
|
|
1486
|
+
for (const h of hooks) {
|
|
1487
|
+
counts[h.hook.type] = (counts[h.hook.type] || 0) + 1
|
|
1488
|
+
}
|
|
1489
|
+
return counts
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function getHooksConfig(
|
|
1493
|
+
appState: AppState | undefined,
|
|
1494
|
+
sessionId: string,
|
|
1495
|
+
hookEvent: HookEvent,
|
|
1496
|
+
): Array<
|
|
1497
|
+
| HookMatcher
|
|
1498
|
+
| HookCallbackMatcher
|
|
1499
|
+
| FunctionHookMatcher
|
|
1500
|
+
| PluginHookMatcher
|
|
1501
|
+
| SkillHookMatcher
|
|
1502
|
+
| SessionDerivedHookMatcher
|
|
1503
|
+
> {
|
|
1504
|
+
// HookMatcher is a zod-stripped {matcher, hooks} so snapshot matchers can be
|
|
1505
|
+
// pushed directly without re-wrapping.
|
|
1506
|
+
const hooks: Array<
|
|
1507
|
+
| HookMatcher
|
|
1508
|
+
| HookCallbackMatcher
|
|
1509
|
+
| FunctionHookMatcher
|
|
1510
|
+
| PluginHookMatcher
|
|
1511
|
+
| SkillHookMatcher
|
|
1512
|
+
| SessionDerivedHookMatcher
|
|
1513
|
+
> = [...(getHooksConfigFromSnapshot()?.[hookEvent] ?? [])]
|
|
1514
|
+
|
|
1515
|
+
// Check if only managed hooks should run (used for both registered and session hooks)
|
|
1516
|
+
const managedOnly = shouldAllowManagedHooksOnly()
|
|
1517
|
+
|
|
1518
|
+
// Process registered hooks (SDK callbacks and plugin native hooks)
|
|
1519
|
+
const registeredHooks = getRegisteredHooks()?.[hookEvent]
|
|
1520
|
+
if (registeredHooks) {
|
|
1521
|
+
for (const matcher of registeredHooks) {
|
|
1522
|
+
// Skip plugin hooks when restricted to managed hooks only
|
|
1523
|
+
// Plugin hooks have pluginRoot set, SDK callbacks do not
|
|
1524
|
+
if (managedOnly && 'pluginRoot' in matcher) {
|
|
1525
|
+
continue
|
|
1526
|
+
}
|
|
1527
|
+
hooks.push(matcher)
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// Merge session hooks for the current session only
|
|
1532
|
+
// Function hooks (like structured output enforcement) must be scoped to their session
|
|
1533
|
+
// to prevent hooks from one agent leaking to another (e.g., verification agent to main agent)
|
|
1534
|
+
// Skip session hooks entirely when allowManagedHooksOnly is set —
|
|
1535
|
+
// this prevents frontmatter hooks from agents/skills from bypassing the policy.
|
|
1536
|
+
// strictPluginOnlyCustomization does NOT block here — it gates at the
|
|
1537
|
+
// REGISTRATION sites (runAgent.ts:526 for agent frontmatter hooks) where
|
|
1538
|
+
// agentDefinition.source is known. A blanket block here would also kill
|
|
1539
|
+
// plugin-provided agents' frontmatter hooks, which is too broad.
|
|
1540
|
+
// Also skip if appState not provided (for backwards compatibility)
|
|
1541
|
+
if (!managedOnly && appState !== undefined) {
|
|
1542
|
+
const sessionHooks = getSessionHooks(appState, sessionId, hookEvent).get(
|
|
1543
|
+
hookEvent,
|
|
1544
|
+
)
|
|
1545
|
+
if (sessionHooks) {
|
|
1546
|
+
// SessionDerivedHookMatcher already includes optional skillRoot
|
|
1547
|
+
for (const matcher of sessionHooks) {
|
|
1548
|
+
hooks.push(matcher)
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Merge session function hooks separately (can't be persisted to HookMatcher format)
|
|
1553
|
+
const sessionFunctionHooks = getSessionFunctionHooks(
|
|
1554
|
+
appState,
|
|
1555
|
+
sessionId,
|
|
1556
|
+
hookEvent,
|
|
1557
|
+
).get(hookEvent)
|
|
1558
|
+
if (sessionFunctionHooks) {
|
|
1559
|
+
for (const matcher of sessionFunctionHooks) {
|
|
1560
|
+
hooks.push(matcher)
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
return hooks
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* Lightweight existence check for hooks on a given event. Mirrors the sources
|
|
1570
|
+
* assembled by getHooksConfig() but stops at the first hit without building
|
|
1571
|
+
* the full merged config.
|
|
1572
|
+
*
|
|
1573
|
+
* Intentionally over-approximates: returns true if any matcher exists for the
|
|
1574
|
+
* event, even if managed-only filtering or pattern matching would later
|
|
1575
|
+
* discard it. A false positive just means we proceed to the full matching
|
|
1576
|
+
* path; a false negative would skip a hook, so we err on the side of true.
|
|
1577
|
+
*
|
|
1578
|
+
* Used to skip createBaseHookInput (getTranscriptPathForSession path joins)
|
|
1579
|
+
* and getMatchingHooks on hot paths where hooks are typically unconfigured.
|
|
1580
|
+
* See hasInstructionsLoadedHook / hasWorktreeCreateHook for the same pattern.
|
|
1581
|
+
*/
|
|
1582
|
+
function hasHookForEvent(
|
|
1583
|
+
hookEvent: HookEvent,
|
|
1584
|
+
appState: AppState | undefined,
|
|
1585
|
+
sessionId: string,
|
|
1586
|
+
): boolean {
|
|
1587
|
+
const snap = getHooksConfigFromSnapshot()?.[hookEvent]
|
|
1588
|
+
if (snap && snap.length > 0) return true
|
|
1589
|
+
const reg = getRegisteredHooks()?.[hookEvent]
|
|
1590
|
+
if (reg && reg.length > 0) return true
|
|
1591
|
+
if (appState?.sessionHooks.get(sessionId)?.hooks[hookEvent]) return true
|
|
1592
|
+
return false
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* Get hook commands that match the given query
|
|
1597
|
+
* @param appState The current app state (optional for backwards compatibility)
|
|
1598
|
+
* @param sessionId The current session ID (main session or agent ID)
|
|
1599
|
+
* @param hookEvent The hook event
|
|
1600
|
+
* @param hookInput The hook input for matching
|
|
1601
|
+
* @returns Array of matched hooks with optional plugin context
|
|
1602
|
+
*/
|
|
1603
|
+
export async function getMatchingHooks(
|
|
1604
|
+
appState: AppState | undefined,
|
|
1605
|
+
sessionId: string,
|
|
1606
|
+
hookEvent: HookEvent,
|
|
1607
|
+
hookInput: HookInput,
|
|
1608
|
+
tools?: Tools,
|
|
1609
|
+
): Promise<MatchedHook[]> {
|
|
1610
|
+
try {
|
|
1611
|
+
const hookMatchers = getHooksConfig(appState, sessionId, hookEvent)
|
|
1612
|
+
|
|
1613
|
+
// If you change the criteria below, then you must change
|
|
1614
|
+
// src/utils/hooks/hooksConfigManager.ts as well.
|
|
1615
|
+
let matchQuery: string | undefined = undefined
|
|
1616
|
+
switch (hookInput.hook_event_name) {
|
|
1617
|
+
case 'PreToolUse':
|
|
1618
|
+
case 'PostToolUse':
|
|
1619
|
+
case 'PostToolUseFailure':
|
|
1620
|
+
case 'PermissionRequest':
|
|
1621
|
+
case 'PermissionDenied':
|
|
1622
|
+
matchQuery = hookInput.tool_name
|
|
1623
|
+
break
|
|
1624
|
+
case 'SessionStart':
|
|
1625
|
+
matchQuery = hookInput.source
|
|
1626
|
+
break
|
|
1627
|
+
case 'Setup':
|
|
1628
|
+
matchQuery = hookInput.trigger
|
|
1629
|
+
break
|
|
1630
|
+
case 'PreCompact':
|
|
1631
|
+
case 'PostCompact':
|
|
1632
|
+
matchQuery = hookInput.trigger
|
|
1633
|
+
break
|
|
1634
|
+
case 'Notification':
|
|
1635
|
+
matchQuery = hookInput.notification_type
|
|
1636
|
+
break
|
|
1637
|
+
case 'SessionEnd':
|
|
1638
|
+
matchQuery = hookInput.reason
|
|
1639
|
+
break
|
|
1640
|
+
case 'StopFailure':
|
|
1641
|
+
matchQuery = hookInput.error
|
|
1642
|
+
break
|
|
1643
|
+
case 'SubagentStart':
|
|
1644
|
+
matchQuery = hookInput.agent_type
|
|
1645
|
+
break
|
|
1646
|
+
case 'SubagentStop':
|
|
1647
|
+
matchQuery = hookInput.agent_type
|
|
1648
|
+
break
|
|
1649
|
+
case 'TeammateIdle':
|
|
1650
|
+
case 'TaskCreated':
|
|
1651
|
+
case 'TaskCompleted':
|
|
1652
|
+
break
|
|
1653
|
+
case 'Elicitation':
|
|
1654
|
+
matchQuery = hookInput.mcp_server_name
|
|
1655
|
+
break
|
|
1656
|
+
case 'ElicitationResult':
|
|
1657
|
+
matchQuery = hookInput.mcp_server_name
|
|
1658
|
+
break
|
|
1659
|
+
case 'ConfigChange':
|
|
1660
|
+
matchQuery = hookInput.source
|
|
1661
|
+
break
|
|
1662
|
+
case 'InstructionsLoaded':
|
|
1663
|
+
matchQuery = hookInput.load_reason
|
|
1664
|
+
break
|
|
1665
|
+
case 'FileChanged':
|
|
1666
|
+
matchQuery = basename(hookInput.file_path)
|
|
1667
|
+
break
|
|
1668
|
+
default:
|
|
1669
|
+
break
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
logForDebugging(
|
|
1673
|
+
`Getting matching hook commands for ${hookEvent} with query: ${matchQuery}`,
|
|
1674
|
+
{ level: 'verbose' },
|
|
1675
|
+
)
|
|
1676
|
+
logForDebugging(`Found ${hookMatchers.length} hook matchers in settings`, {
|
|
1677
|
+
level: 'verbose',
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
// Extract hooks with their plugin context (if any)
|
|
1681
|
+
const filteredMatchers = matchQuery
|
|
1682
|
+
? hookMatchers.filter(
|
|
1683
|
+
matcher =>
|
|
1684
|
+
!matcher.matcher || matchesPattern(matchQuery, matcher.matcher),
|
|
1685
|
+
)
|
|
1686
|
+
: hookMatchers
|
|
1687
|
+
|
|
1688
|
+
const matchedHooks: MatchedHook[] = filteredMatchers.flatMap(matcher => {
|
|
1689
|
+
// Check if this is a PluginHookMatcher (has pluginRoot) or SkillHookMatcher (has skillRoot)
|
|
1690
|
+
const pluginRoot =
|
|
1691
|
+
'pluginRoot' in matcher ? matcher.pluginRoot : undefined
|
|
1692
|
+
const pluginId = 'pluginId' in matcher ? matcher.pluginId : undefined
|
|
1693
|
+
const skillRoot = 'skillRoot' in matcher ? matcher.skillRoot : undefined
|
|
1694
|
+
const hookSource = pluginRoot
|
|
1695
|
+
? 'pluginName' in matcher
|
|
1696
|
+
? `plugin:${matcher.pluginName}`
|
|
1697
|
+
: 'plugin'
|
|
1698
|
+
: skillRoot
|
|
1699
|
+
? 'skillName' in matcher
|
|
1700
|
+
? `skill:${matcher.skillName}`
|
|
1701
|
+
: 'skill'
|
|
1702
|
+
: 'settings'
|
|
1703
|
+
return matcher.hooks.map(hook => ({
|
|
1704
|
+
hook,
|
|
1705
|
+
pluginRoot,
|
|
1706
|
+
pluginId,
|
|
1707
|
+
skillRoot,
|
|
1708
|
+
hookSource,
|
|
1709
|
+
}))
|
|
1710
|
+
})
|
|
1711
|
+
|
|
1712
|
+
// Deduplicate hooks by command/prompt/url within the same source context.
|
|
1713
|
+
// Key is namespaced by pluginRoot/skillRoot (see hookDedupKey above) so
|
|
1714
|
+
// cross-plugin template collisions don't drop hooks (gh-29724).
|
|
1715
|
+
//
|
|
1716
|
+
// Note: new Map(entries) keeps the LAST entry on key collision, not first.
|
|
1717
|
+
// For settings hooks this means the last-merged scope wins; for
|
|
1718
|
+
// same-plugin duplicates the pluginRoot is identical so it doesn't matter.
|
|
1719
|
+
// Fast-path: callback/function hooks don't need dedup (each is unique).
|
|
1720
|
+
// Skip the 6-pass filter + 4×Map + 4×Array.from below when all hooks are
|
|
1721
|
+
// callback/function — the common case for internal hooks like
|
|
1722
|
+
// sessionFileAccessHooks/attributionHooks (44x faster in microbench).
|
|
1723
|
+
if (
|
|
1724
|
+
matchedHooks.every(
|
|
1725
|
+
m => m.hook.type === 'callback' || m.hook.type === 'function',
|
|
1726
|
+
)
|
|
1727
|
+
) {
|
|
1728
|
+
return matchedHooks
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Helper to extract the `if` condition from a hook for dedup keys.
|
|
1732
|
+
// Hooks with different `if` conditions are distinct even if otherwise identical.
|
|
1733
|
+
const getIfCondition = (hook: { if?: string }): string => hook.if ?? ''
|
|
1734
|
+
|
|
1735
|
+
const uniqueCommandHooks = Array.from(
|
|
1736
|
+
new Map(
|
|
1737
|
+
matchedHooks
|
|
1738
|
+
.filter(
|
|
1739
|
+
(
|
|
1740
|
+
m,
|
|
1741
|
+
): m is MatchedHook & { hook: HookCommand & { type: 'command' } } =>
|
|
1742
|
+
m.hook.type === 'command',
|
|
1743
|
+
)
|
|
1744
|
+
// shell is part of identity: {command:'echo x', shell:'bash'}
|
|
1745
|
+
// and {command:'echo x', shell:'powershell'} are distinct hooks,
|
|
1746
|
+
// not duplicates. Default to 'bash' so legacy configs (no shell
|
|
1747
|
+
// field) still dedup against explicit shell:'bash'.
|
|
1748
|
+
.map(m => [
|
|
1749
|
+
hookDedupKey(
|
|
1750
|
+
m,
|
|
1751
|
+
`${m.hook.shell ?? DEFAULT_HOOK_SHELL}\0${m.hook.command}\0${getIfCondition(m.hook)}`,
|
|
1752
|
+
),
|
|
1753
|
+
m,
|
|
1754
|
+
]),
|
|
1755
|
+
).values(),
|
|
1756
|
+
)
|
|
1757
|
+
const uniquePromptHooks = Array.from(
|
|
1758
|
+
new Map(
|
|
1759
|
+
matchedHooks
|
|
1760
|
+
.filter(m => m.hook.type === 'prompt')
|
|
1761
|
+
.map(m => [
|
|
1762
|
+
hookDedupKey(
|
|
1763
|
+
m,
|
|
1764
|
+
`${(m.hook as { prompt: string }).prompt}\0${getIfCondition(m.hook as { if?: string })}`,
|
|
1765
|
+
),
|
|
1766
|
+
m,
|
|
1767
|
+
]),
|
|
1768
|
+
).values(),
|
|
1769
|
+
)
|
|
1770
|
+
const uniqueAgentHooks = Array.from(
|
|
1771
|
+
new Map(
|
|
1772
|
+
matchedHooks
|
|
1773
|
+
.filter(m => m.hook.type === 'agent')
|
|
1774
|
+
.map(m => [
|
|
1775
|
+
hookDedupKey(
|
|
1776
|
+
m,
|
|
1777
|
+
`${(m.hook as { prompt: string }).prompt}\0${getIfCondition(m.hook as { if?: string })}`,
|
|
1778
|
+
),
|
|
1779
|
+
m,
|
|
1780
|
+
]),
|
|
1781
|
+
).values(),
|
|
1782
|
+
)
|
|
1783
|
+
const uniqueHttpHooks = Array.from(
|
|
1784
|
+
new Map(
|
|
1785
|
+
matchedHooks
|
|
1786
|
+
.filter(m => m.hook.type === 'http')
|
|
1787
|
+
.map(m => [
|
|
1788
|
+
hookDedupKey(
|
|
1789
|
+
m,
|
|
1790
|
+
`${(m.hook as { url: string }).url}\0${getIfCondition(m.hook as { if?: string })}`,
|
|
1791
|
+
),
|
|
1792
|
+
m,
|
|
1793
|
+
]),
|
|
1794
|
+
).values(),
|
|
1795
|
+
)
|
|
1796
|
+
const callbackHooks = matchedHooks.filter(m => m.hook.type === 'callback')
|
|
1797
|
+
// Function hooks don't need deduplication - each callback is unique
|
|
1798
|
+
const functionHooks = matchedHooks.filter(m => m.hook.type === 'function')
|
|
1799
|
+
const uniqueHooks = [
|
|
1800
|
+
...uniqueCommandHooks,
|
|
1801
|
+
...uniquePromptHooks,
|
|
1802
|
+
...uniqueAgentHooks,
|
|
1803
|
+
...uniqueHttpHooks,
|
|
1804
|
+
...callbackHooks,
|
|
1805
|
+
...functionHooks,
|
|
1806
|
+
]
|
|
1807
|
+
|
|
1808
|
+
// Filter hooks based on their `if` condition. This allows hooks to specify
|
|
1809
|
+
// conditions like "Bash(git *)" to only run for git commands, avoiding
|
|
1810
|
+
// process spawning overhead for non-matching commands.
|
|
1811
|
+
const hasIfCondition = uniqueHooks.some(
|
|
1812
|
+
h =>
|
|
1813
|
+
(h.hook.type === 'command' ||
|
|
1814
|
+
h.hook.type === 'prompt' ||
|
|
1815
|
+
h.hook.type === 'agent' ||
|
|
1816
|
+
h.hook.type === 'http') &&
|
|
1817
|
+
(h.hook as { if?: string }).if,
|
|
1818
|
+
)
|
|
1819
|
+
const ifMatcher = hasIfCondition
|
|
1820
|
+
? await prepareIfConditionMatcher(hookInput, tools)
|
|
1821
|
+
: undefined
|
|
1822
|
+
const ifFilteredHooks = uniqueHooks.filter(h => {
|
|
1823
|
+
if (
|
|
1824
|
+
h.hook.type !== 'command' &&
|
|
1825
|
+
h.hook.type !== 'prompt' &&
|
|
1826
|
+
h.hook.type !== 'agent' &&
|
|
1827
|
+
h.hook.type !== 'http'
|
|
1828
|
+
) {
|
|
1829
|
+
return true
|
|
1830
|
+
}
|
|
1831
|
+
const ifCondition = (h.hook as { if?: string }).if
|
|
1832
|
+
if (!ifCondition) {
|
|
1833
|
+
return true
|
|
1834
|
+
}
|
|
1835
|
+
if (!ifMatcher) {
|
|
1836
|
+
logForDebugging(
|
|
1837
|
+
`Hook if condition "${ifCondition}" cannot be evaluated for non-tool event ${hookInput.hook_event_name}`,
|
|
1838
|
+
)
|
|
1839
|
+
return false
|
|
1840
|
+
}
|
|
1841
|
+
if (ifMatcher(ifCondition)) {
|
|
1842
|
+
return true
|
|
1843
|
+
}
|
|
1844
|
+
logForDebugging(
|
|
1845
|
+
`Skipping hook due to if condition "${ifCondition}" not matching`,
|
|
1846
|
+
)
|
|
1847
|
+
return false
|
|
1848
|
+
})
|
|
1849
|
+
|
|
1850
|
+
// HTTP hooks are not supported for SessionStart/Setup events. In headless
|
|
1851
|
+
// mode the sandbox ask callback deadlocks because the structuredInput
|
|
1852
|
+
// consumer hasn't started yet when these hooks fire.
|
|
1853
|
+
const filteredHooks =
|
|
1854
|
+
hookEvent === 'SessionStart' || hookEvent === 'Setup'
|
|
1855
|
+
? ifFilteredHooks.filter(h => {
|
|
1856
|
+
if (h.hook.type === 'http') {
|
|
1857
|
+
logForDebugging(
|
|
1858
|
+
`Skipping HTTP hook ${(h.hook as { url: string }).url} — HTTP hooks are not supported for ${hookEvent}`,
|
|
1859
|
+
)
|
|
1860
|
+
return false
|
|
1861
|
+
}
|
|
1862
|
+
return true
|
|
1863
|
+
})
|
|
1864
|
+
: ifFilteredHooks
|
|
1865
|
+
|
|
1866
|
+
logForDebugging(
|
|
1867
|
+
`Matched ${filteredHooks.length} unique hooks for query "${matchQuery || 'no match query'}" (${matchedHooks.length} before deduplication)`,
|
|
1868
|
+
{ level: 'verbose' },
|
|
1869
|
+
)
|
|
1870
|
+
return filteredHooks
|
|
1871
|
+
} catch {
|
|
1872
|
+
return []
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Format a list of blocking errors from a PreTool hook's configured commands.
|
|
1878
|
+
* @param hookName The name of the hook (e.g., 'PreToolUse:Write', 'PreToolUse:Edit', 'PreToolUse:Bash')
|
|
1879
|
+
* @param blockingErrors Array of blocking errors from hooks
|
|
1880
|
+
* @returns Formatted blocking message
|
|
1881
|
+
*/
|
|
1882
|
+
export function getPreToolHookBlockingMessage(
|
|
1883
|
+
hookName: string,
|
|
1884
|
+
blockingError: HookBlockingError,
|
|
1885
|
+
): string {
|
|
1886
|
+
return `${hookName} hook error: ${blockingError.blockingError}`
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/**
|
|
1890
|
+
* Format a list of blocking errors from a Stop hook's configured commands.
|
|
1891
|
+
* @param blockingErrors Array of blocking errors from hooks
|
|
1892
|
+
* @returns Formatted message to give feedback to the model
|
|
1893
|
+
*/
|
|
1894
|
+
export function getStopHookMessage(blockingError: HookBlockingError): string {
|
|
1895
|
+
return `Stop hook feedback:\n${blockingError.blockingError}`
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Format a blocking error from a TeammateIdle hook.
|
|
1900
|
+
* @param blockingError The blocking error from the hook
|
|
1901
|
+
* @returns Formatted message to give feedback to the model
|
|
1902
|
+
*/
|
|
1903
|
+
export function getTeammateIdleHookMessage(
|
|
1904
|
+
blockingError: HookBlockingError,
|
|
1905
|
+
): string {
|
|
1906
|
+
return `TeammateIdle hook feedback:\n${blockingError.blockingError}`
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
/**
|
|
1910
|
+
* Format a blocking error from a TaskCreated hook.
|
|
1911
|
+
* @param blockingError The blocking error from the hook
|
|
1912
|
+
* @returns Formatted message to give feedback to the model
|
|
1913
|
+
*/
|
|
1914
|
+
export function getTaskCreatedHookMessage(
|
|
1915
|
+
blockingError: HookBlockingError,
|
|
1916
|
+
): string {
|
|
1917
|
+
return `TaskCreated hook feedback:\n${blockingError.blockingError}`
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* Format a blocking error from a TaskCompleted hook.
|
|
1922
|
+
* @param blockingError The blocking error from the hook
|
|
1923
|
+
* @returns Formatted message to give feedback to the model
|
|
1924
|
+
*/
|
|
1925
|
+
export function getTaskCompletedHookMessage(
|
|
1926
|
+
blockingError: HookBlockingError,
|
|
1927
|
+
): string {
|
|
1928
|
+
return `TaskCompleted hook feedback:\n${blockingError.blockingError}`
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* Format a list of blocking errors from a UserPromptSubmit hook's configured commands.
|
|
1933
|
+
* @param blockingErrors Array of blocking errors from hooks
|
|
1934
|
+
* @returns Formatted blocking message
|
|
1935
|
+
*/
|
|
1936
|
+
export function getUserPromptSubmitHookBlockingMessage(
|
|
1937
|
+
blockingError: HookBlockingError,
|
|
1938
|
+
): string {
|
|
1939
|
+
return `UserPromptSubmit operation blocked by hook:\n${blockingError.blockingError}`
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Common logic for executing hooks
|
|
1943
|
+
* @param hookInput The structured hook input that will be validated and converted to JSON
|
|
1944
|
+
* @param toolUseID The ID for tracking this hook execution
|
|
1945
|
+
* @param matchQuery The query to match against hook matchers
|
|
1946
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
1947
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
1948
|
+
* @param toolUseContext Optional ToolUseContext for prompt-based hooks (required if using prompt hooks)
|
|
1949
|
+
* @param messages Optional conversation history for prompt/function hooks
|
|
1950
|
+
* @returns Async generator that yields progress messages and hook results
|
|
1951
|
+
*/
|
|
1952
|
+
async function* executeHooks({
|
|
1953
|
+
hookInput,
|
|
1954
|
+
toolUseID,
|
|
1955
|
+
matchQuery,
|
|
1956
|
+
signal,
|
|
1957
|
+
timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
1958
|
+
toolUseContext,
|
|
1959
|
+
messages,
|
|
1960
|
+
forceSyncExecution,
|
|
1961
|
+
requestPrompt,
|
|
1962
|
+
toolInputSummary,
|
|
1963
|
+
}: {
|
|
1964
|
+
hookInput: HookInput
|
|
1965
|
+
toolUseID: string
|
|
1966
|
+
matchQuery?: string
|
|
1967
|
+
signal?: AbortSignal
|
|
1968
|
+
timeoutMs?: number
|
|
1969
|
+
toolUseContext?: ToolUseContext
|
|
1970
|
+
messages?: Message[]
|
|
1971
|
+
forceSyncExecution?: boolean
|
|
1972
|
+
requestPrompt?: (
|
|
1973
|
+
sourceName: string,
|
|
1974
|
+
toolInputSummary?: string | null,
|
|
1975
|
+
) => (request: PromptRequest) => Promise<PromptResponse>
|
|
1976
|
+
toolInputSummary?: string | null
|
|
1977
|
+
}): AsyncGenerator<AggregatedHookResult> {
|
|
1978
|
+
if (shouldDisableAllHooksIncludingManaged()) {
|
|
1979
|
+
return
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
|
|
1983
|
+
return
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
const hookEvent = hookInput.hook_event_name
|
|
1987
|
+
const hookName = matchQuery ? `${hookEvent}:${matchQuery}` : hookEvent
|
|
1988
|
+
|
|
1989
|
+
// Bind the prompt callback to this hook's name and tool input summary so the UI can display context
|
|
1990
|
+
const boundRequestPrompt = requestPrompt?.(hookName, toolInputSummary)
|
|
1991
|
+
|
|
1992
|
+
// SECURITY: ALL hooks require workspace trust in interactive mode
|
|
1993
|
+
// This centralized check prevents RCE vulnerabilities for all current and future hooks
|
|
1994
|
+
if (shouldSkipHookDueToTrust()) {
|
|
1995
|
+
logForDebugging(
|
|
1996
|
+
`Skipping ${hookName} hook execution - workspace trust not accepted`,
|
|
1997
|
+
)
|
|
1998
|
+
return
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
const appState = toolUseContext ? toolUseContext.getAppState() : undefined
|
|
2002
|
+
// Use the agent's session ID if available, otherwise fall back to main session
|
|
2003
|
+
const sessionId = toolUseContext?.agentId ?? getSessionId()
|
|
2004
|
+
const matchingHooks = await getMatchingHooks(
|
|
2005
|
+
appState,
|
|
2006
|
+
sessionId,
|
|
2007
|
+
hookEvent,
|
|
2008
|
+
hookInput,
|
|
2009
|
+
toolUseContext?.options?.tools,
|
|
2010
|
+
)
|
|
2011
|
+
if (matchingHooks.length === 0) {
|
|
2012
|
+
return
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
if (signal?.aborted) {
|
|
2016
|
+
return
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
const userHooks = matchingHooks.filter(h => !isInternalHook(h))
|
|
2020
|
+
if (userHooks.length > 0) {
|
|
2021
|
+
const pluginHookCounts = getPluginHookCounts(userHooks)
|
|
2022
|
+
const hookTypeCounts = getHookTypeCounts(userHooks)
|
|
2023
|
+
logEvent(`tengu_run_hook`, {
|
|
2024
|
+
hookName:
|
|
2025
|
+
hookName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
2026
|
+
numCommands: userHooks.length,
|
|
2027
|
+
hookTypeCounts: jsonStringify(
|
|
2028
|
+
hookTypeCounts,
|
|
2029
|
+
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
2030
|
+
...(pluginHookCounts && {
|
|
2031
|
+
pluginHookCounts: jsonStringify(
|
|
2032
|
+
pluginHookCounts,
|
|
2033
|
+
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
2034
|
+
}),
|
|
2035
|
+
})
|
|
2036
|
+
} else {
|
|
2037
|
+
// Fast-path: all hooks are internal callbacks (sessionFileAccessHooks,
|
|
2038
|
+
// attributionHooks). These return {} and don't use the abort signal, so we
|
|
2039
|
+
// can skip span/progress/abortSignal/processHookJSONOutput/resultLoop.
|
|
2040
|
+
// Measured: 6.01µs → ~1.8µs per PostToolUse hit (-70%).
|
|
2041
|
+
const batchStartTime = Date.now()
|
|
2042
|
+
const context = toolUseContext
|
|
2043
|
+
? {
|
|
2044
|
+
getAppState: toolUseContext.getAppState,
|
|
2045
|
+
updateAttributionState: toolUseContext.updateAttributionState,
|
|
2046
|
+
}
|
|
2047
|
+
: undefined
|
|
2048
|
+
for (const [i, { hook }] of matchingHooks.entries()) {
|
|
2049
|
+
if (hook.type === 'callback') {
|
|
2050
|
+
await hook.callback(hookInput, toolUseID, signal, i, context)
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
const totalDurationMs = Date.now() - batchStartTime
|
|
2054
|
+
getStatsStore()?.observe('hook_duration_ms', totalDurationMs)
|
|
2055
|
+
addToTurnHookDuration(totalDurationMs)
|
|
2056
|
+
logEvent(`tengu_repl_hook_finished`, {
|
|
2057
|
+
hookName:
|
|
2058
|
+
hookName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
2059
|
+
numCommands: matchingHooks.length,
|
|
2060
|
+
numSuccess: matchingHooks.length,
|
|
2061
|
+
numBlocking: 0,
|
|
2062
|
+
numNonBlockingError: 0,
|
|
2063
|
+
numCancelled: 0,
|
|
2064
|
+
totalDurationMs,
|
|
2065
|
+
})
|
|
2066
|
+
return
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// Collect hook definitions for beta tracing telemetry
|
|
2070
|
+
const hookDefinitionsJson = isBetaTracingEnabled()
|
|
2071
|
+
? jsonStringify(getHookDefinitionsForTelemetry(matchingHooks))
|
|
2072
|
+
: '[]'
|
|
2073
|
+
|
|
2074
|
+
// Log hook execution start to OTEL (only for beta tracing)
|
|
2075
|
+
if (isBetaTracingEnabled()) {
|
|
2076
|
+
void logOTelEvent('hook_execution_start', {
|
|
2077
|
+
hook_event: hookEvent,
|
|
2078
|
+
hook_name: hookName,
|
|
2079
|
+
num_hooks: String(matchingHooks.length),
|
|
2080
|
+
managed_only: String(shouldAllowManagedHooksOnly()),
|
|
2081
|
+
hook_definitions: hookDefinitionsJson,
|
|
2082
|
+
hook_source: shouldAllowManagedHooksOnly() ? 'policySettings' : 'merged',
|
|
2083
|
+
})
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// Start hook span for beta tracing
|
|
2087
|
+
const hookSpan = startHookSpan(
|
|
2088
|
+
hookEvent,
|
|
2089
|
+
hookName,
|
|
2090
|
+
matchingHooks.length,
|
|
2091
|
+
hookDefinitionsJson,
|
|
2092
|
+
)
|
|
2093
|
+
|
|
2094
|
+
// Yield progress messages for each hook before execution
|
|
2095
|
+
for (const { hook } of matchingHooks) {
|
|
2096
|
+
yield {
|
|
2097
|
+
message: {
|
|
2098
|
+
type: 'progress',
|
|
2099
|
+
data: {
|
|
2100
|
+
type: 'hook_progress',
|
|
2101
|
+
hookEvent,
|
|
2102
|
+
hookName,
|
|
2103
|
+
command: getHookDisplayText(hook),
|
|
2104
|
+
...(hook.type === 'prompt' && { promptText: hook.prompt }),
|
|
2105
|
+
...('statusMessage' in hook &&
|
|
2106
|
+
hook.statusMessage != null && {
|
|
2107
|
+
statusMessage: hook.statusMessage,
|
|
2108
|
+
}),
|
|
2109
|
+
},
|
|
2110
|
+
parentToolUseID: toolUseID,
|
|
2111
|
+
toolUseID,
|
|
2112
|
+
timestamp: new Date().toISOString(),
|
|
2113
|
+
uuid: randomUUID(),
|
|
2114
|
+
},
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// Track wall-clock time for the entire hook batch
|
|
2119
|
+
const batchStartTime = Date.now()
|
|
2120
|
+
|
|
2121
|
+
// Lazy-once stringify of hookInput. Shared across all command/prompt/agent/http
|
|
2122
|
+
// hooks in this batch (hookInput is never mutated). Callback/function hooks
|
|
2123
|
+
// return before reaching this, so batches with only those pay no stringify cost.
|
|
2124
|
+
let jsonInputResult:
|
|
2125
|
+
| { ok: true; value: string }
|
|
2126
|
+
| { ok: false; error: unknown }
|
|
2127
|
+
| undefined
|
|
2128
|
+
function getJsonInput() {
|
|
2129
|
+
if (jsonInputResult !== undefined) {
|
|
2130
|
+
return jsonInputResult
|
|
2131
|
+
}
|
|
2132
|
+
try {
|
|
2133
|
+
return (jsonInputResult = { ok: true, value: jsonStringify(hookInput) })
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
logError(
|
|
2136
|
+
Error(`Failed to stringify hook ${hookName} input`, { cause: error }),
|
|
2137
|
+
)
|
|
2138
|
+
return (jsonInputResult = { ok: false, error })
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// Run all hooks in parallel with individual timeouts
|
|
2143
|
+
const hookPromises = matchingHooks.map(async function* (
|
|
2144
|
+
{ hook, pluginRoot, pluginId, skillRoot },
|
|
2145
|
+
hookIndex,
|
|
2146
|
+
): AsyncGenerator<HookResult> {
|
|
2147
|
+
if (hook.type === 'callback') {
|
|
2148
|
+
const callbackTimeoutMs = hook.timeout ? hook.timeout * 1000 : timeoutMs
|
|
2149
|
+
const { signal: abortSignal, cleanup } = createCombinedAbortSignal(
|
|
2150
|
+
signal,
|
|
2151
|
+
{ timeoutMs: callbackTimeoutMs },
|
|
2152
|
+
)
|
|
2153
|
+
yield executeHookCallback({
|
|
2154
|
+
toolUseID,
|
|
2155
|
+
hook,
|
|
2156
|
+
hookEvent,
|
|
2157
|
+
hookInput,
|
|
2158
|
+
signal: abortSignal,
|
|
2159
|
+
hookIndex,
|
|
2160
|
+
toolUseContext,
|
|
2161
|
+
}).finally(cleanup)
|
|
2162
|
+
return
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
if (hook.type === 'function') {
|
|
2166
|
+
if (!messages) {
|
|
2167
|
+
yield {
|
|
2168
|
+
message: createAttachmentMessage({
|
|
2169
|
+
type: 'hook_error_during_execution',
|
|
2170
|
+
hookName,
|
|
2171
|
+
toolUseID,
|
|
2172
|
+
hookEvent,
|
|
2173
|
+
content: 'Messages not provided for function hook',
|
|
2174
|
+
}),
|
|
2175
|
+
outcome: 'non_blocking_error',
|
|
2176
|
+
hook,
|
|
2177
|
+
}
|
|
2178
|
+
return
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// Function hooks only come from session storage with callback embedded
|
|
2182
|
+
yield executeFunctionHook({
|
|
2183
|
+
hook,
|
|
2184
|
+
messages,
|
|
2185
|
+
hookName,
|
|
2186
|
+
toolUseID,
|
|
2187
|
+
hookEvent,
|
|
2188
|
+
timeoutMs,
|
|
2189
|
+
signal,
|
|
2190
|
+
})
|
|
2191
|
+
return
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// Command and prompt hooks need jsonInput
|
|
2195
|
+
const commandTimeoutMs = hook.timeout ? hook.timeout * 1000 : timeoutMs
|
|
2196
|
+
const { signal: abortSignal, cleanup } = createCombinedAbortSignal(signal, {
|
|
2197
|
+
timeoutMs: commandTimeoutMs,
|
|
2198
|
+
})
|
|
2199
|
+
const hookId = randomUUID()
|
|
2200
|
+
const hookStartMs = Date.now()
|
|
2201
|
+
const hookCommand = getHookDisplayText(hook)
|
|
2202
|
+
|
|
2203
|
+
try {
|
|
2204
|
+
const jsonInputRes = getJsonInput()
|
|
2205
|
+
if (!jsonInputRes.ok) {
|
|
2206
|
+
yield {
|
|
2207
|
+
message: createAttachmentMessage({
|
|
2208
|
+
type: 'hook_error_during_execution',
|
|
2209
|
+
hookName,
|
|
2210
|
+
toolUseID,
|
|
2211
|
+
hookEvent,
|
|
2212
|
+
content: `Failed to prepare hook input: ${errorMessage(jsonInputRes.error)}`,
|
|
2213
|
+
command: hookCommand,
|
|
2214
|
+
durationMs: Date.now() - hookStartMs,
|
|
2215
|
+
}),
|
|
2216
|
+
outcome: 'non_blocking_error',
|
|
2217
|
+
hook,
|
|
2218
|
+
}
|
|
2219
|
+
cleanup()
|
|
2220
|
+
return
|
|
2221
|
+
}
|
|
2222
|
+
const jsonInput = jsonInputRes.value
|
|
2223
|
+
|
|
2224
|
+
if (hook.type === 'prompt') {
|
|
2225
|
+
if (!toolUseContext) {
|
|
2226
|
+
throw new Error(
|
|
2227
|
+
'ToolUseContext is required for prompt hooks. This is a bug.',
|
|
2228
|
+
)
|
|
2229
|
+
}
|
|
2230
|
+
const promptResult = await execPromptHook(
|
|
2231
|
+
hook,
|
|
2232
|
+
hookName,
|
|
2233
|
+
hookEvent,
|
|
2234
|
+
jsonInput,
|
|
2235
|
+
abortSignal,
|
|
2236
|
+
toolUseContext,
|
|
2237
|
+
messages,
|
|
2238
|
+
toolUseID,
|
|
2239
|
+
)
|
|
2240
|
+
// Inject timing fields for hook visibility
|
|
2241
|
+
if (promptResult.message?.type === 'attachment') {
|
|
2242
|
+
const att = promptResult.message.attachment
|
|
2243
|
+
if (
|
|
2244
|
+
att.type === 'hook_success' ||
|
|
2245
|
+
att.type === 'hook_non_blocking_error'
|
|
2246
|
+
) {
|
|
2247
|
+
att.command = hookCommand
|
|
2248
|
+
att.durationMs = Date.now() - hookStartMs
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
yield promptResult
|
|
2252
|
+
cleanup?.()
|
|
2253
|
+
return
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
if (hook.type === 'agent') {
|
|
2257
|
+
if (!toolUseContext) {
|
|
2258
|
+
throw new Error(
|
|
2259
|
+
'ToolUseContext is required for agent hooks. This is a bug.',
|
|
2260
|
+
)
|
|
2261
|
+
}
|
|
2262
|
+
if (!messages) {
|
|
2263
|
+
throw new Error(
|
|
2264
|
+
'Messages are required for agent hooks. This is a bug.',
|
|
2265
|
+
)
|
|
2266
|
+
}
|
|
2267
|
+
const agentResult = await execAgentHook(
|
|
2268
|
+
hook,
|
|
2269
|
+
hookName,
|
|
2270
|
+
hookEvent,
|
|
2271
|
+
jsonInput,
|
|
2272
|
+
abortSignal,
|
|
2273
|
+
toolUseContext,
|
|
2274
|
+
toolUseID,
|
|
2275
|
+
messages,
|
|
2276
|
+
'agent_type' in hookInput
|
|
2277
|
+
? (hookInput.agent_type as string)
|
|
2278
|
+
: undefined,
|
|
2279
|
+
)
|
|
2280
|
+
// Inject timing fields for hook visibility
|
|
2281
|
+
if (agentResult.message?.type === 'attachment') {
|
|
2282
|
+
const att = agentResult.message.attachment
|
|
2283
|
+
if (
|
|
2284
|
+
att.type === 'hook_success' ||
|
|
2285
|
+
att.type === 'hook_non_blocking_error'
|
|
2286
|
+
) {
|
|
2287
|
+
att.command = hookCommand
|
|
2288
|
+
att.durationMs = Date.now() - hookStartMs
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
yield agentResult
|
|
2292
|
+
cleanup?.()
|
|
2293
|
+
return
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
if (hook.type === 'http') {
|
|
2297
|
+
emitHookStarted(hookId, hookName, hookEvent)
|
|
2298
|
+
|
|
2299
|
+
// execHttpHook manages its own timeout internally via hook.timeout or
|
|
2300
|
+
// DEFAULT_HTTP_HOOK_TIMEOUT_MS, so pass the parent signal directly
|
|
2301
|
+
// to avoid double-stacking timeouts with abortSignal.
|
|
2302
|
+
const httpResult = await execHttpHook(
|
|
2303
|
+
hook,
|
|
2304
|
+
hookEvent,
|
|
2305
|
+
jsonInput,
|
|
2306
|
+
signal,
|
|
2307
|
+
)
|
|
2308
|
+
cleanup?.()
|
|
2309
|
+
|
|
2310
|
+
if (httpResult.aborted) {
|
|
2311
|
+
emitHookResponse({
|
|
2312
|
+
hookId,
|
|
2313
|
+
hookName,
|
|
2314
|
+
hookEvent,
|
|
2315
|
+
output: 'Hook cancelled',
|
|
2316
|
+
stdout: '',
|
|
2317
|
+
stderr: '',
|
|
2318
|
+
exitCode: undefined,
|
|
2319
|
+
outcome: 'cancelled',
|
|
2320
|
+
})
|
|
2321
|
+
yield {
|
|
2322
|
+
message: createAttachmentMessage({
|
|
2323
|
+
type: 'hook_cancelled',
|
|
2324
|
+
hookName,
|
|
2325
|
+
toolUseID,
|
|
2326
|
+
hookEvent,
|
|
2327
|
+
}),
|
|
2328
|
+
outcome: 'cancelled' as const,
|
|
2329
|
+
hook,
|
|
2330
|
+
}
|
|
2331
|
+
return
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
if (httpResult.error || !httpResult.ok) {
|
|
2335
|
+
const stderr =
|
|
2336
|
+
httpResult.error || `HTTP ${httpResult.statusCode} from ${hook.url}`
|
|
2337
|
+
emitHookResponse({
|
|
2338
|
+
hookId,
|
|
2339
|
+
hookName,
|
|
2340
|
+
hookEvent,
|
|
2341
|
+
output: stderr,
|
|
2342
|
+
stdout: '',
|
|
2343
|
+
stderr,
|
|
2344
|
+
exitCode: httpResult.statusCode,
|
|
2345
|
+
outcome: 'error',
|
|
2346
|
+
})
|
|
2347
|
+
yield {
|
|
2348
|
+
message: createAttachmentMessage({
|
|
2349
|
+
type: 'hook_non_blocking_error',
|
|
2350
|
+
hookName,
|
|
2351
|
+
toolUseID,
|
|
2352
|
+
hookEvent,
|
|
2353
|
+
stderr,
|
|
2354
|
+
stdout: '',
|
|
2355
|
+
exitCode: httpResult.statusCode ?? 0,
|
|
2356
|
+
}),
|
|
2357
|
+
outcome: 'non_blocking_error' as const,
|
|
2358
|
+
hook,
|
|
2359
|
+
}
|
|
2360
|
+
return
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// HTTP hooks must return JSON — parse and validate through Zod
|
|
2364
|
+
const { json: httpJson, validationError: httpValidationError } =
|
|
2365
|
+
parseHttpHookOutput(httpResult.body)
|
|
2366
|
+
|
|
2367
|
+
if (httpValidationError) {
|
|
2368
|
+
emitHookResponse({
|
|
2369
|
+
hookId,
|
|
2370
|
+
hookName,
|
|
2371
|
+
hookEvent,
|
|
2372
|
+
output: httpResult.body,
|
|
2373
|
+
stdout: httpResult.body,
|
|
2374
|
+
stderr: `JSON validation failed: ${httpValidationError}`,
|
|
2375
|
+
exitCode: httpResult.statusCode,
|
|
2376
|
+
outcome: 'error',
|
|
2377
|
+
})
|
|
2378
|
+
yield {
|
|
2379
|
+
message: createAttachmentMessage({
|
|
2380
|
+
type: 'hook_non_blocking_error',
|
|
2381
|
+
hookName,
|
|
2382
|
+
toolUseID,
|
|
2383
|
+
hookEvent,
|
|
2384
|
+
stderr: `JSON validation failed: ${httpValidationError}`,
|
|
2385
|
+
stdout: httpResult.body,
|
|
2386
|
+
exitCode: httpResult.statusCode ?? 0,
|
|
2387
|
+
}),
|
|
2388
|
+
outcome: 'non_blocking_error' as const,
|
|
2389
|
+
hook,
|
|
2390
|
+
}
|
|
2391
|
+
return
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
if (httpJson && isAsyncHookJSONOutput(httpJson)) {
|
|
2395
|
+
// Async response: treat as success (no further processing)
|
|
2396
|
+
emitHookResponse({
|
|
2397
|
+
hookId,
|
|
2398
|
+
hookName,
|
|
2399
|
+
hookEvent,
|
|
2400
|
+
output: httpResult.body,
|
|
2401
|
+
stdout: httpResult.body,
|
|
2402
|
+
stderr: '',
|
|
2403
|
+
exitCode: httpResult.statusCode,
|
|
2404
|
+
outcome: 'success',
|
|
2405
|
+
})
|
|
2406
|
+
yield {
|
|
2407
|
+
outcome: 'success' as const,
|
|
2408
|
+
hook,
|
|
2409
|
+
}
|
|
2410
|
+
return
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (httpJson) {
|
|
2414
|
+
const processed = processHookJSONOutput({
|
|
2415
|
+
json: httpJson,
|
|
2416
|
+
command: hook.url,
|
|
2417
|
+
hookName,
|
|
2418
|
+
toolUseID,
|
|
2419
|
+
hookEvent,
|
|
2420
|
+
expectedHookEvent: hookEvent,
|
|
2421
|
+
stdout: httpResult.body,
|
|
2422
|
+
stderr: '',
|
|
2423
|
+
exitCode: httpResult.statusCode,
|
|
2424
|
+
})
|
|
2425
|
+
emitHookResponse({
|
|
2426
|
+
hookId,
|
|
2427
|
+
hookName,
|
|
2428
|
+
hookEvent,
|
|
2429
|
+
output: httpResult.body,
|
|
2430
|
+
stdout: httpResult.body,
|
|
2431
|
+
stderr: '',
|
|
2432
|
+
exitCode: httpResult.statusCode,
|
|
2433
|
+
outcome: 'success',
|
|
2434
|
+
})
|
|
2435
|
+
yield {
|
|
2436
|
+
...processed,
|
|
2437
|
+
outcome: 'success' as const,
|
|
2438
|
+
hook,
|
|
2439
|
+
}
|
|
2440
|
+
return
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
return
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
emitHookStarted(hookId, hookName, hookEvent)
|
|
2447
|
+
|
|
2448
|
+
const result = await execCommandHook(
|
|
2449
|
+
hook,
|
|
2450
|
+
hookEvent,
|
|
2451
|
+
hookName,
|
|
2452
|
+
jsonInput,
|
|
2453
|
+
abortSignal,
|
|
2454
|
+
hookId,
|
|
2455
|
+
hookIndex,
|
|
2456
|
+
pluginRoot,
|
|
2457
|
+
pluginId,
|
|
2458
|
+
skillRoot,
|
|
2459
|
+
forceSyncExecution,
|
|
2460
|
+
boundRequestPrompt,
|
|
2461
|
+
)
|
|
2462
|
+
cleanup?.()
|
|
2463
|
+
const durationMs = Date.now() - hookStartMs
|
|
2464
|
+
|
|
2465
|
+
if (result.backgrounded) {
|
|
2466
|
+
yield {
|
|
2467
|
+
outcome: 'success' as const,
|
|
2468
|
+
hook,
|
|
2469
|
+
}
|
|
2470
|
+
return
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
if (result.aborted) {
|
|
2474
|
+
emitHookResponse({
|
|
2475
|
+
hookId,
|
|
2476
|
+
hookName,
|
|
2477
|
+
hookEvent,
|
|
2478
|
+
output: result.output,
|
|
2479
|
+
stdout: result.stdout,
|
|
2480
|
+
stderr: result.stderr,
|
|
2481
|
+
exitCode: result.status,
|
|
2482
|
+
outcome: 'cancelled',
|
|
2483
|
+
})
|
|
2484
|
+
yield {
|
|
2485
|
+
message: createAttachmentMessage({
|
|
2486
|
+
type: 'hook_cancelled',
|
|
2487
|
+
hookName,
|
|
2488
|
+
toolUseID,
|
|
2489
|
+
hookEvent,
|
|
2490
|
+
command: hookCommand,
|
|
2491
|
+
durationMs,
|
|
2492
|
+
}),
|
|
2493
|
+
outcome: 'cancelled' as const,
|
|
2494
|
+
hook,
|
|
2495
|
+
}
|
|
2496
|
+
return
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// Try JSON parsing first
|
|
2500
|
+
const { json, plainText, validationError } = parseHookOutput(
|
|
2501
|
+
result.stdout,
|
|
2502
|
+
)
|
|
2503
|
+
|
|
2504
|
+
if (validationError) {
|
|
2505
|
+
emitHookResponse({
|
|
2506
|
+
hookId,
|
|
2507
|
+
hookName,
|
|
2508
|
+
hookEvent,
|
|
2509
|
+
output: result.output,
|
|
2510
|
+
stdout: result.stdout,
|
|
2511
|
+
stderr: `JSON validation failed: ${validationError}`,
|
|
2512
|
+
exitCode: 1,
|
|
2513
|
+
outcome: 'error',
|
|
2514
|
+
})
|
|
2515
|
+
yield {
|
|
2516
|
+
message: createAttachmentMessage({
|
|
2517
|
+
type: 'hook_non_blocking_error',
|
|
2518
|
+
hookName,
|
|
2519
|
+
toolUseID,
|
|
2520
|
+
hookEvent,
|
|
2521
|
+
stderr: `JSON validation failed: ${validationError}`,
|
|
2522
|
+
stdout: result.stdout,
|
|
2523
|
+
exitCode: 1,
|
|
2524
|
+
command: hookCommand,
|
|
2525
|
+
durationMs,
|
|
2526
|
+
}),
|
|
2527
|
+
outcome: 'non_blocking_error' as const,
|
|
2528
|
+
hook,
|
|
2529
|
+
}
|
|
2530
|
+
return
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
if (json) {
|
|
2534
|
+
// Async responses were already backgrounded during execution
|
|
2535
|
+
if (isAsyncHookJSONOutput(json)) {
|
|
2536
|
+
yield {
|
|
2537
|
+
outcome: 'success' as const,
|
|
2538
|
+
hook,
|
|
2539
|
+
}
|
|
2540
|
+
return
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// Process JSON output
|
|
2544
|
+
const processed = processHookJSONOutput({
|
|
2545
|
+
json,
|
|
2546
|
+
command: hookCommand,
|
|
2547
|
+
hookName,
|
|
2548
|
+
toolUseID,
|
|
2549
|
+
hookEvent,
|
|
2550
|
+
expectedHookEvent: hookEvent,
|
|
2551
|
+
stdout: result.stdout,
|
|
2552
|
+
stderr: result.stderr,
|
|
2553
|
+
exitCode: result.status,
|
|
2554
|
+
durationMs,
|
|
2555
|
+
})
|
|
2556
|
+
|
|
2557
|
+
// Handle suppressOutput (skip for async responses)
|
|
2558
|
+
if (
|
|
2559
|
+
isSyncHookJSONOutput(json) &&
|
|
2560
|
+
!json.suppressOutput &&
|
|
2561
|
+
plainText &&
|
|
2562
|
+
result.status === 0
|
|
2563
|
+
) {
|
|
2564
|
+
// Still show non-JSON output if not suppressed
|
|
2565
|
+
const content = `${chalk.bold(hookName)} completed`
|
|
2566
|
+
emitHookResponse({
|
|
2567
|
+
hookId,
|
|
2568
|
+
hookName,
|
|
2569
|
+
hookEvent,
|
|
2570
|
+
output: result.output,
|
|
2571
|
+
stdout: result.stdout,
|
|
2572
|
+
stderr: result.stderr,
|
|
2573
|
+
exitCode: result.status,
|
|
2574
|
+
outcome: 'success',
|
|
2575
|
+
})
|
|
2576
|
+
yield {
|
|
2577
|
+
...processed,
|
|
2578
|
+
message:
|
|
2579
|
+
processed.message ||
|
|
2580
|
+
createAttachmentMessage({
|
|
2581
|
+
type: 'hook_success',
|
|
2582
|
+
hookName,
|
|
2583
|
+
toolUseID,
|
|
2584
|
+
hookEvent,
|
|
2585
|
+
content,
|
|
2586
|
+
stdout: result.stdout,
|
|
2587
|
+
stderr: result.stderr,
|
|
2588
|
+
exitCode: result.status,
|
|
2589
|
+
command: hookCommand,
|
|
2590
|
+
durationMs,
|
|
2591
|
+
}),
|
|
2592
|
+
outcome: 'success' as const,
|
|
2593
|
+
hook,
|
|
2594
|
+
}
|
|
2595
|
+
return
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
emitHookResponse({
|
|
2599
|
+
hookId,
|
|
2600
|
+
hookName,
|
|
2601
|
+
hookEvent,
|
|
2602
|
+
output: result.output,
|
|
2603
|
+
stdout: result.stdout,
|
|
2604
|
+
stderr: result.stderr,
|
|
2605
|
+
exitCode: result.status,
|
|
2606
|
+
outcome: result.status === 0 ? 'success' : 'error',
|
|
2607
|
+
})
|
|
2608
|
+
yield {
|
|
2609
|
+
...processed,
|
|
2610
|
+
outcome: 'success' as const,
|
|
2611
|
+
hook,
|
|
2612
|
+
}
|
|
2613
|
+
return
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// Fall back to existing logic for non-JSON output
|
|
2617
|
+
if (result.status === 0) {
|
|
2618
|
+
emitHookResponse({
|
|
2619
|
+
hookId,
|
|
2620
|
+
hookName,
|
|
2621
|
+
hookEvent,
|
|
2622
|
+
output: result.output,
|
|
2623
|
+
stdout: result.stdout,
|
|
2624
|
+
stderr: result.stderr,
|
|
2625
|
+
exitCode: result.status,
|
|
2626
|
+
outcome: 'success',
|
|
2627
|
+
})
|
|
2628
|
+
yield {
|
|
2629
|
+
message: createAttachmentMessage({
|
|
2630
|
+
type: 'hook_success',
|
|
2631
|
+
hookName,
|
|
2632
|
+
toolUseID,
|
|
2633
|
+
hookEvent,
|
|
2634
|
+
content: result.stdout.trim(),
|
|
2635
|
+
stdout: result.stdout,
|
|
2636
|
+
stderr: result.stderr,
|
|
2637
|
+
exitCode: result.status,
|
|
2638
|
+
command: hookCommand,
|
|
2639
|
+
durationMs,
|
|
2640
|
+
}),
|
|
2641
|
+
outcome: 'success' as const,
|
|
2642
|
+
hook,
|
|
2643
|
+
}
|
|
2644
|
+
return
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Hooks with exit code 2 provide blocking feedback
|
|
2648
|
+
if (result.status === 2) {
|
|
2649
|
+
emitHookResponse({
|
|
2650
|
+
hookId,
|
|
2651
|
+
hookName,
|
|
2652
|
+
hookEvent,
|
|
2653
|
+
output: result.output,
|
|
2654
|
+
stdout: result.stdout,
|
|
2655
|
+
stderr: result.stderr,
|
|
2656
|
+
exitCode: result.status,
|
|
2657
|
+
outcome: 'error',
|
|
2658
|
+
})
|
|
2659
|
+
yield {
|
|
2660
|
+
blockingError: {
|
|
2661
|
+
blockingError: `[${hook.command}]: ${result.stderr || 'No stderr output'}`,
|
|
2662
|
+
command: hook.command,
|
|
2663
|
+
},
|
|
2664
|
+
outcome: 'blocking' as const,
|
|
2665
|
+
hook,
|
|
2666
|
+
}
|
|
2667
|
+
return
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// Any other non-zero exit code is a non-critical error that should just
|
|
2671
|
+
// be shown to the user.
|
|
2672
|
+
emitHookResponse({
|
|
2673
|
+
hookId,
|
|
2674
|
+
hookName,
|
|
2675
|
+
hookEvent,
|
|
2676
|
+
output: result.output,
|
|
2677
|
+
stdout: result.stdout,
|
|
2678
|
+
stderr: result.stderr,
|
|
2679
|
+
exitCode: result.status,
|
|
2680
|
+
outcome: 'error',
|
|
2681
|
+
})
|
|
2682
|
+
yield {
|
|
2683
|
+
message: createAttachmentMessage({
|
|
2684
|
+
type: 'hook_non_blocking_error',
|
|
2685
|
+
hookName,
|
|
2686
|
+
toolUseID,
|
|
2687
|
+
hookEvent,
|
|
2688
|
+
stderr: `Failed with non-blocking status code: ${result.stderr.trim() || 'No stderr output'}`,
|
|
2689
|
+
stdout: result.stdout,
|
|
2690
|
+
exitCode: result.status,
|
|
2691
|
+
command: hookCommand,
|
|
2692
|
+
durationMs,
|
|
2693
|
+
}),
|
|
2694
|
+
outcome: 'non_blocking_error' as const,
|
|
2695
|
+
hook,
|
|
2696
|
+
}
|
|
2697
|
+
return
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
// Clean up on error
|
|
2700
|
+
cleanup?.()
|
|
2701
|
+
|
|
2702
|
+
const errorMessage =
|
|
2703
|
+
error instanceof Error ? error.message : String(error)
|
|
2704
|
+
emitHookResponse({
|
|
2705
|
+
hookId,
|
|
2706
|
+
hookName,
|
|
2707
|
+
hookEvent,
|
|
2708
|
+
output: `Failed to run: ${errorMessage}`,
|
|
2709
|
+
stdout: '',
|
|
2710
|
+
stderr: `Failed to run: ${errorMessage}`,
|
|
2711
|
+
exitCode: 1,
|
|
2712
|
+
outcome: 'error',
|
|
2713
|
+
})
|
|
2714
|
+
yield {
|
|
2715
|
+
message: createAttachmentMessage({
|
|
2716
|
+
type: 'hook_non_blocking_error',
|
|
2717
|
+
hookName,
|
|
2718
|
+
toolUseID,
|
|
2719
|
+
hookEvent,
|
|
2720
|
+
stderr: `Failed to run: ${errorMessage}`,
|
|
2721
|
+
stdout: '',
|
|
2722
|
+
exitCode: 1,
|
|
2723
|
+
command: hookCommand,
|
|
2724
|
+
durationMs: Date.now() - hookStartMs,
|
|
2725
|
+
}),
|
|
2726
|
+
outcome: 'non_blocking_error' as const,
|
|
2727
|
+
hook,
|
|
2728
|
+
}
|
|
2729
|
+
return
|
|
2730
|
+
}
|
|
2731
|
+
})
|
|
2732
|
+
|
|
2733
|
+
// Track outcomes for logging
|
|
2734
|
+
const outcomes = {
|
|
2735
|
+
success: 0,
|
|
2736
|
+
blocking: 0,
|
|
2737
|
+
non_blocking_error: 0,
|
|
2738
|
+
cancelled: 0,
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
let permissionBehavior: PermissionResult['behavior'] | undefined
|
|
2742
|
+
|
|
2743
|
+
// Run all hooks in parallel and wait for all to complete
|
|
2744
|
+
for await (const result of all(hookPromises)) {
|
|
2745
|
+
outcomes[result.outcome]++
|
|
2746
|
+
|
|
2747
|
+
// Check for preventContinuation early
|
|
2748
|
+
if (result.preventContinuation) {
|
|
2749
|
+
logForDebugging(
|
|
2750
|
+
`Hook ${hookEvent} (${getHookDisplayText(result.hook)}) requested preventContinuation`,
|
|
2751
|
+
)
|
|
2752
|
+
yield {
|
|
2753
|
+
preventContinuation: true,
|
|
2754
|
+
stopReason: result.stopReason,
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// Handle different result types
|
|
2759
|
+
if (result.blockingError) {
|
|
2760
|
+
yield {
|
|
2761
|
+
blockingError: result.blockingError,
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
if (result.message) {
|
|
2766
|
+
yield { message: result.message }
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// Yield system message separately if present
|
|
2770
|
+
if (result.systemMessage) {
|
|
2771
|
+
yield {
|
|
2772
|
+
message: createAttachmentMessage({
|
|
2773
|
+
type: 'hook_system_message',
|
|
2774
|
+
content: result.systemMessage,
|
|
2775
|
+
hookName,
|
|
2776
|
+
toolUseID,
|
|
2777
|
+
hookEvent,
|
|
2778
|
+
}),
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// Collect additional context from hooks
|
|
2783
|
+
if (result.additionalContext) {
|
|
2784
|
+
logForDebugging(
|
|
2785
|
+
`Hook ${hookEvent} (${getHookDisplayText(result.hook)}) provided additionalContext (${result.additionalContext.length} chars)`,
|
|
2786
|
+
)
|
|
2787
|
+
yield {
|
|
2788
|
+
additionalContexts: [result.additionalContext],
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
if (result.initialUserMessage) {
|
|
2793
|
+
logForDebugging(
|
|
2794
|
+
`Hook ${hookEvent} (${getHookDisplayText(result.hook)}) provided initialUserMessage (${result.initialUserMessage.length} chars)`,
|
|
2795
|
+
)
|
|
2796
|
+
yield {
|
|
2797
|
+
initialUserMessage: result.initialUserMessage,
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
if (result.watchPaths && result.watchPaths.length > 0) {
|
|
2802
|
+
logForDebugging(
|
|
2803
|
+
`Hook ${hookEvent} (${getHookDisplayText(result.hook)}) provided ${result.watchPaths.length} watchPaths`,
|
|
2804
|
+
)
|
|
2805
|
+
yield {
|
|
2806
|
+
watchPaths: result.watchPaths,
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// Yield updatedMCPToolOutput if provided (from PostToolUse hooks)
|
|
2811
|
+
if (result.updatedMCPToolOutput) {
|
|
2812
|
+
logForDebugging(
|
|
2813
|
+
`Hook ${hookEvent} (${getHookDisplayText(result.hook)}) replaced MCP tool output`,
|
|
2814
|
+
)
|
|
2815
|
+
yield {
|
|
2816
|
+
updatedMCPToolOutput: result.updatedMCPToolOutput,
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// Check for permission behavior with precedence: deny > ask > allow
|
|
2821
|
+
if (result.permissionBehavior) {
|
|
2822
|
+
logForDebugging(
|
|
2823
|
+
`Hook ${hookEvent} (${getHookDisplayText(result.hook)}) returned permissionDecision: ${result.permissionBehavior}${result.hookPermissionDecisionReason ? ` (reason: ${result.hookPermissionDecisionReason})` : ''}`,
|
|
2824
|
+
)
|
|
2825
|
+
// Apply precedence rules
|
|
2826
|
+
switch (result.permissionBehavior) {
|
|
2827
|
+
case 'deny':
|
|
2828
|
+
// deny always takes precedence
|
|
2829
|
+
permissionBehavior = 'deny'
|
|
2830
|
+
break
|
|
2831
|
+
case 'ask':
|
|
2832
|
+
// ask takes precedence over allow but not deny
|
|
2833
|
+
if (permissionBehavior !== 'deny') {
|
|
2834
|
+
permissionBehavior = 'ask'
|
|
2835
|
+
}
|
|
2836
|
+
break
|
|
2837
|
+
case 'allow':
|
|
2838
|
+
// allow only if no other behavior set
|
|
2839
|
+
if (!permissionBehavior) {
|
|
2840
|
+
permissionBehavior = 'allow'
|
|
2841
|
+
}
|
|
2842
|
+
break
|
|
2843
|
+
case 'passthrough':
|
|
2844
|
+
// passthrough doesn't set permission behavior
|
|
2845
|
+
break
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// Yield permission behavior and updatedInput if provided (from allow or ask behavior)
|
|
2850
|
+
if (permissionBehavior !== undefined) {
|
|
2851
|
+
const updatedInput =
|
|
2852
|
+
result.updatedInput &&
|
|
2853
|
+
(result.permissionBehavior === 'allow' ||
|
|
2854
|
+
result.permissionBehavior === 'ask')
|
|
2855
|
+
? result.updatedInput
|
|
2856
|
+
: undefined
|
|
2857
|
+
if (updatedInput) {
|
|
2858
|
+
logForDebugging(
|
|
2859
|
+
`Hook ${hookEvent} (${getHookDisplayText(result.hook)}) modified tool input keys: [${Object.keys(updatedInput).join(', ')}]`,
|
|
2860
|
+
)
|
|
2861
|
+
}
|
|
2862
|
+
yield {
|
|
2863
|
+
permissionBehavior,
|
|
2864
|
+
hookPermissionDecisionReason: result.hookPermissionDecisionReason,
|
|
2865
|
+
hookSource: matchingHooks.find(m => m.hook === result.hook)?.hookSource,
|
|
2866
|
+
updatedInput,
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
// Yield updatedInput separately for passthrough case (no permission decision)
|
|
2871
|
+
// This allows hooks to modify input without making a permission decision
|
|
2872
|
+
// Note: Check result.permissionBehavior (this hook's behavior), not the aggregated permissionBehavior
|
|
2873
|
+
if (result.updatedInput && result.permissionBehavior === undefined) {
|
|
2874
|
+
logForDebugging(
|
|
2875
|
+
`Hook ${hookEvent} (${getHookDisplayText(result.hook)}) modified tool input keys: [${Object.keys(result.updatedInput).join(', ')}]`,
|
|
2876
|
+
)
|
|
2877
|
+
yield {
|
|
2878
|
+
updatedInput: result.updatedInput,
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
// Yield permission request result if provided (from PermissionRequest hooks)
|
|
2882
|
+
if (result.permissionRequestResult) {
|
|
2883
|
+
yield {
|
|
2884
|
+
permissionRequestResult: result.permissionRequestResult,
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
// Yield retry flag if provided (from PermissionDenied hooks)
|
|
2888
|
+
if (result.retry) {
|
|
2889
|
+
yield {
|
|
2890
|
+
retry: result.retry,
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
// Yield elicitation response if provided (from Elicitation hooks)
|
|
2894
|
+
if (result.elicitationResponse) {
|
|
2895
|
+
yield {
|
|
2896
|
+
elicitationResponse: result.elicitationResponse,
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
// Yield elicitation result response if provided (from ElicitationResult hooks)
|
|
2900
|
+
if (result.elicitationResultResponse) {
|
|
2901
|
+
yield {
|
|
2902
|
+
elicitationResultResponse: result.elicitationResultResponse,
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// Invoke session hook callback if this is a command/prompt/function hook (not a callback hook)
|
|
2907
|
+
if (appState && result.hook.type !== 'callback') {
|
|
2908
|
+
const sessionId = getSessionId()
|
|
2909
|
+
// Use empty string as matcher when matchQuery is undefined (e.g., for Stop hooks)
|
|
2910
|
+
const matcher = matchQuery ?? ''
|
|
2911
|
+
const hookEntry = getSessionHookCallback(
|
|
2912
|
+
appState,
|
|
2913
|
+
sessionId,
|
|
2914
|
+
hookEvent,
|
|
2915
|
+
matcher,
|
|
2916
|
+
result.hook,
|
|
2917
|
+
)
|
|
2918
|
+
// Invoke onHookSuccess only on success outcome
|
|
2919
|
+
if (hookEntry?.onHookSuccess && result.outcome === 'success') {
|
|
2920
|
+
try {
|
|
2921
|
+
hookEntry.onHookSuccess(result.hook, result as AggregatedHookResult)
|
|
2922
|
+
} catch (error) {
|
|
2923
|
+
logError(
|
|
2924
|
+
Error('Session hook success callback failed', { cause: error }),
|
|
2925
|
+
)
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
const totalDurationMs = Date.now() - batchStartTime
|
|
2932
|
+
getStatsStore()?.observe('hook_duration_ms', totalDurationMs)
|
|
2933
|
+
addToTurnHookDuration(totalDurationMs)
|
|
2934
|
+
|
|
2935
|
+
logEvent(`tengu_repl_hook_finished`, {
|
|
2936
|
+
hookName:
|
|
2937
|
+
hookName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
2938
|
+
numCommands: matchingHooks.length,
|
|
2939
|
+
numSuccess: outcomes.success,
|
|
2940
|
+
numBlocking: outcomes.blocking,
|
|
2941
|
+
numNonBlockingError: outcomes.non_blocking_error,
|
|
2942
|
+
numCancelled: outcomes.cancelled,
|
|
2943
|
+
totalDurationMs,
|
|
2944
|
+
})
|
|
2945
|
+
|
|
2946
|
+
// Log hook execution completion to OTEL (only for beta tracing)
|
|
2947
|
+
if (isBetaTracingEnabled()) {
|
|
2948
|
+
const hookDefinitionsComplete =
|
|
2949
|
+
getHookDefinitionsForTelemetry(matchingHooks)
|
|
2950
|
+
|
|
2951
|
+
void logOTelEvent('hook_execution_complete', {
|
|
2952
|
+
hook_event: hookEvent,
|
|
2953
|
+
hook_name: hookName,
|
|
2954
|
+
num_hooks: String(matchingHooks.length),
|
|
2955
|
+
num_success: String(outcomes.success),
|
|
2956
|
+
num_blocking: String(outcomes.blocking),
|
|
2957
|
+
num_non_blocking_error: String(outcomes.non_blocking_error),
|
|
2958
|
+
num_cancelled: String(outcomes.cancelled),
|
|
2959
|
+
managed_only: String(shouldAllowManagedHooksOnly()),
|
|
2960
|
+
hook_definitions: jsonStringify(hookDefinitionsComplete),
|
|
2961
|
+
hook_source: shouldAllowManagedHooksOnly() ? 'policySettings' : 'merged',
|
|
2962
|
+
})
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// End hook span for beta tracing
|
|
2966
|
+
endHookSpan(hookSpan, {
|
|
2967
|
+
numSuccess: outcomes.success,
|
|
2968
|
+
numBlocking: outcomes.blocking,
|
|
2969
|
+
numNonBlockingError: outcomes.non_blocking_error,
|
|
2970
|
+
numCancelled: outcomes.cancelled,
|
|
2971
|
+
})
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
export type HookOutsideReplResult = {
|
|
2975
|
+
command: string
|
|
2976
|
+
succeeded: boolean
|
|
2977
|
+
output: string
|
|
2978
|
+
blocked: boolean
|
|
2979
|
+
watchPaths?: string[]
|
|
2980
|
+
systemMessage?: string
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
export function hasBlockingResult(results: HookOutsideReplResult[]): boolean {
|
|
2984
|
+
return results.some(r => r.blocked)
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
/**
|
|
2988
|
+
* Execute hooks outside of the REPL (e.g. notifications, session end)
|
|
2989
|
+
*
|
|
2990
|
+
* Unlike executeHooks() which yields messages that are exposed to the model as
|
|
2991
|
+
* system messages, this function only logs errors via logForDebugging (visible
|
|
2992
|
+
* with --debug). Callers that need to surface errors to users should handle
|
|
2993
|
+
* the returned results appropriately (e.g. executeSessionEndHooks writes to
|
|
2994
|
+
* stderr during shutdown).
|
|
2995
|
+
*
|
|
2996
|
+
* @param getAppState Optional function to get the current app state (for session hooks)
|
|
2997
|
+
* @param hookInput The structured hook input that will be validated and converted to JSON
|
|
2998
|
+
* @param matchQuery The query to match against hook matchers
|
|
2999
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3000
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3001
|
+
* @returns Array of HookOutsideReplResult objects containing command, succeeded, and output
|
|
3002
|
+
*/
|
|
3003
|
+
async function executeHooksOutsideREPL({
|
|
3004
|
+
getAppState,
|
|
3005
|
+
hookInput,
|
|
3006
|
+
matchQuery,
|
|
3007
|
+
signal,
|
|
3008
|
+
timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3009
|
+
}: {
|
|
3010
|
+
getAppState?: () => AppState
|
|
3011
|
+
hookInput: HookInput
|
|
3012
|
+
matchQuery?: string
|
|
3013
|
+
signal?: AbortSignal
|
|
3014
|
+
timeoutMs: number
|
|
3015
|
+
}): Promise<HookOutsideReplResult[]> {
|
|
3016
|
+
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
|
|
3017
|
+
return []
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
const hookEvent = hookInput.hook_event_name
|
|
3021
|
+
const hookName = matchQuery ? `${hookEvent}:${matchQuery}` : hookEvent
|
|
3022
|
+
if (shouldDisableAllHooksIncludingManaged()) {
|
|
3023
|
+
logForDebugging(
|
|
3024
|
+
`Skipping hooks for ${hookName} due to 'disableAllHooks' managed setting`,
|
|
3025
|
+
)
|
|
3026
|
+
return []
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// SECURITY: ALL hooks require workspace trust in interactive mode
|
|
3030
|
+
// This centralized check prevents RCE vulnerabilities for all current and future hooks
|
|
3031
|
+
if (shouldSkipHookDueToTrust()) {
|
|
3032
|
+
logForDebugging(
|
|
3033
|
+
`Skipping ${hookName} hook execution - workspace trust not accepted`,
|
|
3034
|
+
)
|
|
3035
|
+
return []
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
const appState = getAppState ? getAppState() : undefined
|
|
3039
|
+
// Use main session ID for outside-REPL hooks
|
|
3040
|
+
const sessionId = getSessionId()
|
|
3041
|
+
const matchingHooks = await getMatchingHooks(
|
|
3042
|
+
appState,
|
|
3043
|
+
sessionId,
|
|
3044
|
+
hookEvent,
|
|
3045
|
+
hookInput,
|
|
3046
|
+
)
|
|
3047
|
+
if (matchingHooks.length === 0) {
|
|
3048
|
+
return []
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
if (signal?.aborted) {
|
|
3052
|
+
return []
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
const userHooks = matchingHooks.filter(h => !isInternalHook(h))
|
|
3056
|
+
if (userHooks.length > 0) {
|
|
3057
|
+
const pluginHookCounts = getPluginHookCounts(userHooks)
|
|
3058
|
+
const hookTypeCounts = getHookTypeCounts(userHooks)
|
|
3059
|
+
logEvent(`tengu_run_hook`, {
|
|
3060
|
+
hookName:
|
|
3061
|
+
hookName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
3062
|
+
numCommands: userHooks.length,
|
|
3063
|
+
hookTypeCounts: jsonStringify(
|
|
3064
|
+
hookTypeCounts,
|
|
3065
|
+
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
3066
|
+
...(pluginHookCounts && {
|
|
3067
|
+
pluginHookCounts: jsonStringify(
|
|
3068
|
+
pluginHookCounts,
|
|
3069
|
+
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
3070
|
+
}),
|
|
3071
|
+
})
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// Validate and stringify the hook input
|
|
3075
|
+
let jsonInput: string
|
|
3076
|
+
try {
|
|
3077
|
+
jsonInput = jsonStringify(hookInput)
|
|
3078
|
+
} catch (error) {
|
|
3079
|
+
logError(error)
|
|
3080
|
+
return []
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
// Run all hooks in parallel with individual timeouts
|
|
3084
|
+
const hookPromises = matchingHooks.map(
|
|
3085
|
+
async ({ hook, pluginRoot, pluginId }, hookIndex) => {
|
|
3086
|
+
// Handle callback hooks
|
|
3087
|
+
if (hook.type === 'callback') {
|
|
3088
|
+
const callbackTimeoutMs = hook.timeout ? hook.timeout * 1000 : timeoutMs
|
|
3089
|
+
const { signal: abortSignal, cleanup } = createCombinedAbortSignal(
|
|
3090
|
+
signal,
|
|
3091
|
+
{ timeoutMs: callbackTimeoutMs },
|
|
3092
|
+
)
|
|
3093
|
+
|
|
3094
|
+
try {
|
|
3095
|
+
const toolUseID = randomUUID()
|
|
3096
|
+
const json = await hook.callback(
|
|
3097
|
+
hookInput,
|
|
3098
|
+
toolUseID,
|
|
3099
|
+
abortSignal,
|
|
3100
|
+
hookIndex,
|
|
3101
|
+
)
|
|
3102
|
+
|
|
3103
|
+
cleanup?.()
|
|
3104
|
+
|
|
3105
|
+
if (isAsyncHookJSONOutput(json)) {
|
|
3106
|
+
logForDebugging(
|
|
3107
|
+
`${hookName} [callback] returned async response, returning empty output`,
|
|
3108
|
+
)
|
|
3109
|
+
return {
|
|
3110
|
+
command: 'callback',
|
|
3111
|
+
succeeded: true,
|
|
3112
|
+
output: '',
|
|
3113
|
+
blocked: false,
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
const output =
|
|
3118
|
+
hookEvent === 'WorktreeCreate' &&
|
|
3119
|
+
isSyncHookJSONOutput(json) &&
|
|
3120
|
+
json.hookSpecificOutput?.hookEventName === 'WorktreeCreate'
|
|
3121
|
+
? json.hookSpecificOutput.worktreePath
|
|
3122
|
+
: json.systemMessage || ''
|
|
3123
|
+
const blocked =
|
|
3124
|
+
isSyncHookJSONOutput(json) && json.decision === 'block'
|
|
3125
|
+
|
|
3126
|
+
logForDebugging(`${hookName} [callback] completed successfully`)
|
|
3127
|
+
|
|
3128
|
+
return {
|
|
3129
|
+
command: 'callback',
|
|
3130
|
+
succeeded: true,
|
|
3131
|
+
output,
|
|
3132
|
+
blocked,
|
|
3133
|
+
}
|
|
3134
|
+
} catch (error) {
|
|
3135
|
+
cleanup?.()
|
|
3136
|
+
|
|
3137
|
+
const errorMessage =
|
|
3138
|
+
error instanceof Error ? error.message : String(error)
|
|
3139
|
+
logForDebugging(
|
|
3140
|
+
`${hookName} [callback] failed to run: ${errorMessage}`,
|
|
3141
|
+
{ level: 'error' },
|
|
3142
|
+
)
|
|
3143
|
+
return {
|
|
3144
|
+
command: 'callback',
|
|
3145
|
+
succeeded: false,
|
|
3146
|
+
output: errorMessage,
|
|
3147
|
+
blocked: false,
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// TODO: Implement prompt stop hooks outside REPL
|
|
3153
|
+
if (hook.type === 'prompt') {
|
|
3154
|
+
return {
|
|
3155
|
+
command: hook.prompt,
|
|
3156
|
+
succeeded: false,
|
|
3157
|
+
output: 'Prompt stop hooks are not yet supported outside REPL',
|
|
3158
|
+
blocked: false,
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
// TODO: Implement agent stop hooks outside REPL
|
|
3163
|
+
if (hook.type === 'agent') {
|
|
3164
|
+
return {
|
|
3165
|
+
command: hook.prompt,
|
|
3166
|
+
succeeded: false,
|
|
3167
|
+
output: 'Agent stop hooks are not yet supported outside REPL',
|
|
3168
|
+
blocked: false,
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// Function hooks require messages array (only available in REPL context)
|
|
3173
|
+
// For -p mode Stop hooks, use executeStopHooks which supports function hooks
|
|
3174
|
+
if (hook.type === 'function') {
|
|
3175
|
+
logError(
|
|
3176
|
+
new Error(
|
|
3177
|
+
`Function hook reached executeHooksOutsideREPL for ${hookEvent}. Function hooks should only be used in REPL context (Stop hooks).`,
|
|
3178
|
+
),
|
|
3179
|
+
)
|
|
3180
|
+
return {
|
|
3181
|
+
command: 'function',
|
|
3182
|
+
succeeded: false,
|
|
3183
|
+
output: 'Internal error: function hook executed outside REPL context',
|
|
3184
|
+
blocked: false,
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// Handle HTTP hooks (no toolUseContext needed - just HTTP POST).
|
|
3189
|
+
// execHttpHook handles its own timeout internally via hook.timeout or
|
|
3190
|
+
// DEFAULT_HTTP_HOOK_TIMEOUT_MS, so we pass signal directly.
|
|
3191
|
+
if (hook.type === 'http') {
|
|
3192
|
+
try {
|
|
3193
|
+
const httpResult = await execHttpHook(
|
|
3194
|
+
hook,
|
|
3195
|
+
hookEvent,
|
|
3196
|
+
jsonInput,
|
|
3197
|
+
signal,
|
|
3198
|
+
)
|
|
3199
|
+
|
|
3200
|
+
if (httpResult.aborted) {
|
|
3201
|
+
logForDebugging(`${hookName} [${hook.url}] cancelled`)
|
|
3202
|
+
return {
|
|
3203
|
+
command: hook.url,
|
|
3204
|
+
succeeded: false,
|
|
3205
|
+
output: 'Hook cancelled',
|
|
3206
|
+
blocked: false,
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
if (httpResult.error || !httpResult.ok) {
|
|
3211
|
+
const errMsg =
|
|
3212
|
+
httpResult.error ||
|
|
3213
|
+
`HTTP ${httpResult.statusCode} from ${hook.url}`
|
|
3214
|
+
logForDebugging(`${hookName} [${hook.url}] failed: ${errMsg}`, {
|
|
3215
|
+
level: 'error',
|
|
3216
|
+
})
|
|
3217
|
+
return {
|
|
3218
|
+
command: hook.url,
|
|
3219
|
+
succeeded: false,
|
|
3220
|
+
output: errMsg,
|
|
3221
|
+
blocked: false,
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// HTTP hooks must return JSON — parse and validate through Zod
|
|
3226
|
+
const { json: httpJson, validationError: httpValidationError } =
|
|
3227
|
+
parseHttpHookOutput(httpResult.body)
|
|
3228
|
+
if (httpValidationError) {
|
|
3229
|
+
throw new Error(httpValidationError)
|
|
3230
|
+
}
|
|
3231
|
+
if (httpJson && !isAsyncHookJSONOutput(httpJson)) {
|
|
3232
|
+
logForDebugging(
|
|
3233
|
+
`Parsed JSON output from HTTP hook: ${jsonStringify(httpJson)}`,
|
|
3234
|
+
{ level: 'verbose' },
|
|
3235
|
+
)
|
|
3236
|
+
}
|
|
3237
|
+
const jsonBlocked =
|
|
3238
|
+
httpJson &&
|
|
3239
|
+
!isAsyncHookJSONOutput(httpJson) &&
|
|
3240
|
+
isSyncHookJSONOutput(httpJson) &&
|
|
3241
|
+
httpJson.decision === 'block'
|
|
3242
|
+
|
|
3243
|
+
// WorktreeCreate's consumer reads `output` as the bare filesystem
|
|
3244
|
+
// path. Command hooks provide it via stdout; http hooks provide it
|
|
3245
|
+
// via hookSpecificOutput.worktreePath. Without worktreePath, emit ''
|
|
3246
|
+
// so the consumer's length filter skips it instead of treating the
|
|
3247
|
+
// raw '{}' body as a path.
|
|
3248
|
+
const output =
|
|
3249
|
+
hookEvent === 'WorktreeCreate'
|
|
3250
|
+
? httpJson &&
|
|
3251
|
+
isSyncHookJSONOutput(httpJson) &&
|
|
3252
|
+
httpJson.hookSpecificOutput?.hookEventName === 'WorktreeCreate'
|
|
3253
|
+
? httpJson.hookSpecificOutput.worktreePath
|
|
3254
|
+
: ''
|
|
3255
|
+
: httpResult.body
|
|
3256
|
+
|
|
3257
|
+
return {
|
|
3258
|
+
command: hook.url,
|
|
3259
|
+
succeeded: true,
|
|
3260
|
+
output,
|
|
3261
|
+
blocked: !!jsonBlocked,
|
|
3262
|
+
}
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
const errorMessage =
|
|
3265
|
+
error instanceof Error ? error.message : String(error)
|
|
3266
|
+
logForDebugging(
|
|
3267
|
+
`${hookName} [${hook.url}] failed to run: ${errorMessage}`,
|
|
3268
|
+
{ level: 'error' },
|
|
3269
|
+
)
|
|
3270
|
+
return {
|
|
3271
|
+
command: hook.url,
|
|
3272
|
+
succeeded: false,
|
|
3273
|
+
output: errorMessage,
|
|
3274
|
+
blocked: false,
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// Handle command hooks
|
|
3280
|
+
const commandTimeoutMs = hook.timeout ? hook.timeout * 1000 : timeoutMs
|
|
3281
|
+
const { signal: abortSignal, cleanup } = createCombinedAbortSignal(
|
|
3282
|
+
signal,
|
|
3283
|
+
{ timeoutMs: commandTimeoutMs },
|
|
3284
|
+
)
|
|
3285
|
+
try {
|
|
3286
|
+
const result = await execCommandHook(
|
|
3287
|
+
hook,
|
|
3288
|
+
hookEvent,
|
|
3289
|
+
hookName,
|
|
3290
|
+
jsonInput,
|
|
3291
|
+
abortSignal,
|
|
3292
|
+
randomUUID(),
|
|
3293
|
+
hookIndex,
|
|
3294
|
+
pluginRoot,
|
|
3295
|
+
pluginId,
|
|
3296
|
+
)
|
|
3297
|
+
|
|
3298
|
+
// Clear timeout if hook completes
|
|
3299
|
+
cleanup?.()
|
|
3300
|
+
|
|
3301
|
+
if (result.aborted) {
|
|
3302
|
+
logForDebugging(`${hookName} [${hook.command}] cancelled`)
|
|
3303
|
+
return {
|
|
3304
|
+
command: hook.command,
|
|
3305
|
+
succeeded: false,
|
|
3306
|
+
output: 'Hook cancelled',
|
|
3307
|
+
blocked: false,
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
logForDebugging(
|
|
3312
|
+
`${hookName} [${hook.command}] completed with status ${result.status}`,
|
|
3313
|
+
)
|
|
3314
|
+
|
|
3315
|
+
// Parse JSON for any messages to print out.
|
|
3316
|
+
const { json, validationError } = parseHookOutput(result.stdout)
|
|
3317
|
+
if (validationError) {
|
|
3318
|
+
// Validation error is logged via logForDebugging and returned in output
|
|
3319
|
+
throw new Error(validationError)
|
|
3320
|
+
}
|
|
3321
|
+
if (json && !isAsyncHookJSONOutput(json)) {
|
|
3322
|
+
logForDebugging(
|
|
3323
|
+
`Parsed JSON output from hook: ${jsonStringify(json)}`,
|
|
3324
|
+
{ level: 'verbose' },
|
|
3325
|
+
)
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
// Blocked if exit code 2 or JSON decision: 'block'
|
|
3329
|
+
const jsonBlocked =
|
|
3330
|
+
json &&
|
|
3331
|
+
!isAsyncHookJSONOutput(json) &&
|
|
3332
|
+
isSyncHookJSONOutput(json) &&
|
|
3333
|
+
json.decision === 'block'
|
|
3334
|
+
const blocked = result.status === 2 || !!jsonBlocked
|
|
3335
|
+
|
|
3336
|
+
// For successful hooks (exit code 0), use stdout; for failed hooks, use stderr
|
|
3337
|
+
const output =
|
|
3338
|
+
result.status === 0 ? result.stdout || '' : result.stderr || ''
|
|
3339
|
+
|
|
3340
|
+
const watchPaths =
|
|
3341
|
+
json &&
|
|
3342
|
+
isSyncHookJSONOutput(json) &&
|
|
3343
|
+
json.hookSpecificOutput &&
|
|
3344
|
+
'watchPaths' in json.hookSpecificOutput
|
|
3345
|
+
? json.hookSpecificOutput.watchPaths
|
|
3346
|
+
: undefined
|
|
3347
|
+
|
|
3348
|
+
const systemMessage =
|
|
3349
|
+
json && isSyncHookJSONOutput(json) ? json.systemMessage : undefined
|
|
3350
|
+
|
|
3351
|
+
return {
|
|
3352
|
+
command: hook.command,
|
|
3353
|
+
succeeded: result.status === 0,
|
|
3354
|
+
output,
|
|
3355
|
+
blocked,
|
|
3356
|
+
watchPaths,
|
|
3357
|
+
systemMessage,
|
|
3358
|
+
}
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
// Clean up on error
|
|
3361
|
+
cleanup?.()
|
|
3362
|
+
|
|
3363
|
+
const errorMessage =
|
|
3364
|
+
error instanceof Error ? error.message : String(error)
|
|
3365
|
+
logForDebugging(
|
|
3366
|
+
`${hookName} [${hook.command}] failed to run: ${errorMessage}`,
|
|
3367
|
+
{ level: 'error' },
|
|
3368
|
+
)
|
|
3369
|
+
return {
|
|
3370
|
+
command: hook.command,
|
|
3371
|
+
succeeded: false,
|
|
3372
|
+
output: errorMessage,
|
|
3373
|
+
blocked: false,
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
},
|
|
3377
|
+
)
|
|
3378
|
+
|
|
3379
|
+
// Wait for all hooks to complete and collect results
|
|
3380
|
+
return await Promise.all(hookPromises)
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
/**
|
|
3384
|
+
* Execute pre-tool hooks if configured
|
|
3385
|
+
* @param toolName The name of the tool (e.g., 'Write', 'Edit', 'Bash')
|
|
3386
|
+
* @param toolUseID The ID of the tool use
|
|
3387
|
+
* @param toolInput The input that will be passed to the tool
|
|
3388
|
+
* @param permissionMode Optional permission mode from toolPermissionContext
|
|
3389
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3390
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3391
|
+
* @param toolUseContext Optional ToolUseContext for prompt-based hooks
|
|
3392
|
+
* @returns Async generator that yields progress messages and returns blocking errors
|
|
3393
|
+
*/
|
|
3394
|
+
export async function* executePreToolHooks<ToolInput>(
|
|
3395
|
+
toolName: string,
|
|
3396
|
+
toolUseID: string,
|
|
3397
|
+
toolInput: ToolInput,
|
|
3398
|
+
toolUseContext: ToolUseContext,
|
|
3399
|
+
permissionMode?: string,
|
|
3400
|
+
signal?: AbortSignal,
|
|
3401
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3402
|
+
requestPrompt?: (
|
|
3403
|
+
sourceName: string,
|
|
3404
|
+
toolInputSummary?: string | null,
|
|
3405
|
+
) => (request: PromptRequest) => Promise<PromptResponse>,
|
|
3406
|
+
toolInputSummary?: string | null,
|
|
3407
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3408
|
+
const appState = toolUseContext.getAppState()
|
|
3409
|
+
const sessionId = toolUseContext.agentId ?? getSessionId()
|
|
3410
|
+
if (!hasHookForEvent('PreToolUse', appState, sessionId)) {
|
|
3411
|
+
return
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
logForDebugging(`executePreToolHooks called for tool: ${toolName}`, {
|
|
3415
|
+
level: 'verbose',
|
|
3416
|
+
})
|
|
3417
|
+
|
|
3418
|
+
const hookInput: PreToolUseHookInput = {
|
|
3419
|
+
...createBaseHookInput(permissionMode, undefined, toolUseContext),
|
|
3420
|
+
hook_event_name: 'PreToolUse',
|
|
3421
|
+
tool_name: toolName,
|
|
3422
|
+
tool_input: toolInput,
|
|
3423
|
+
tool_use_id: toolUseID,
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
yield* executeHooks({
|
|
3427
|
+
hookInput,
|
|
3428
|
+
toolUseID,
|
|
3429
|
+
matchQuery: toolName,
|
|
3430
|
+
signal,
|
|
3431
|
+
timeoutMs,
|
|
3432
|
+
toolUseContext,
|
|
3433
|
+
requestPrompt,
|
|
3434
|
+
toolInputSummary,
|
|
3435
|
+
})
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
/**
|
|
3439
|
+
* Execute post-tool hooks if configured
|
|
3440
|
+
* @param toolName The name of the tool (e.g., 'Write', 'Edit', 'Bash')
|
|
3441
|
+
* @param toolUseID The ID of the tool use
|
|
3442
|
+
* @param toolInput The input that was passed to the tool
|
|
3443
|
+
* @param toolResponse The response from the tool
|
|
3444
|
+
* @param toolUseContext ToolUseContext for prompt-based hooks
|
|
3445
|
+
* @param permissionMode Optional permission mode from toolPermissionContext
|
|
3446
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3447
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3448
|
+
* @returns Async generator that yields progress messages and blocking errors for automated feedback
|
|
3449
|
+
*/
|
|
3450
|
+
export async function* executePostToolHooks<ToolInput, ToolResponse>(
|
|
3451
|
+
toolName: string,
|
|
3452
|
+
toolUseID: string,
|
|
3453
|
+
toolInput: ToolInput,
|
|
3454
|
+
toolResponse: ToolResponse,
|
|
3455
|
+
toolUseContext: ToolUseContext,
|
|
3456
|
+
permissionMode?: string,
|
|
3457
|
+
signal?: AbortSignal,
|
|
3458
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3459
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3460
|
+
const hookInput: PostToolUseHookInput = {
|
|
3461
|
+
...createBaseHookInput(permissionMode, undefined, toolUseContext),
|
|
3462
|
+
hook_event_name: 'PostToolUse',
|
|
3463
|
+
tool_name: toolName,
|
|
3464
|
+
tool_input: toolInput,
|
|
3465
|
+
tool_response: toolResponse,
|
|
3466
|
+
tool_use_id: toolUseID,
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
yield* executeHooks({
|
|
3470
|
+
hookInput,
|
|
3471
|
+
toolUseID,
|
|
3472
|
+
matchQuery: toolName,
|
|
3473
|
+
signal,
|
|
3474
|
+
timeoutMs,
|
|
3475
|
+
toolUseContext,
|
|
3476
|
+
})
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
/**
|
|
3480
|
+
* Execute post-tool-use-failure hooks if configured
|
|
3481
|
+
* @param toolName The name of the tool (e.g., 'Write', 'Edit', 'Bash')
|
|
3482
|
+
* @param toolUseID The ID of the tool use
|
|
3483
|
+
* @param toolInput The input that was passed to the tool
|
|
3484
|
+
* @param error The error message from the failed tool call
|
|
3485
|
+
* @param toolUseContext ToolUseContext for prompt-based hooks
|
|
3486
|
+
* @param isInterrupt Whether the tool was interrupted by user
|
|
3487
|
+
* @param permissionMode Optional permission mode from toolPermissionContext
|
|
3488
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3489
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3490
|
+
* @returns Async generator that yields progress messages and blocking errors
|
|
3491
|
+
*/
|
|
3492
|
+
export async function* executePostToolUseFailureHooks<ToolInput>(
|
|
3493
|
+
toolName: string,
|
|
3494
|
+
toolUseID: string,
|
|
3495
|
+
toolInput: ToolInput,
|
|
3496
|
+
error: string,
|
|
3497
|
+
toolUseContext: ToolUseContext,
|
|
3498
|
+
isInterrupt?: boolean,
|
|
3499
|
+
permissionMode?: string,
|
|
3500
|
+
signal?: AbortSignal,
|
|
3501
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3502
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3503
|
+
const appState = toolUseContext.getAppState()
|
|
3504
|
+
const sessionId = toolUseContext.agentId ?? getSessionId()
|
|
3505
|
+
if (!hasHookForEvent('PostToolUseFailure', appState, sessionId)) {
|
|
3506
|
+
return
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
const hookInput: PostToolUseFailureHookInput = {
|
|
3510
|
+
...createBaseHookInput(permissionMode, undefined, toolUseContext),
|
|
3511
|
+
hook_event_name: 'PostToolUseFailure',
|
|
3512
|
+
tool_name: toolName,
|
|
3513
|
+
tool_input: toolInput,
|
|
3514
|
+
tool_use_id: toolUseID,
|
|
3515
|
+
error,
|
|
3516
|
+
is_interrupt: isInterrupt,
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
yield* executeHooks({
|
|
3520
|
+
hookInput,
|
|
3521
|
+
toolUseID,
|
|
3522
|
+
matchQuery: toolName,
|
|
3523
|
+
signal,
|
|
3524
|
+
timeoutMs,
|
|
3525
|
+
toolUseContext,
|
|
3526
|
+
})
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
export async function* executePermissionDeniedHooks<ToolInput>(
|
|
3530
|
+
toolName: string,
|
|
3531
|
+
toolUseID: string,
|
|
3532
|
+
toolInput: ToolInput,
|
|
3533
|
+
reason: string,
|
|
3534
|
+
toolUseContext: ToolUseContext,
|
|
3535
|
+
permissionMode?: string,
|
|
3536
|
+
signal?: AbortSignal,
|
|
3537
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3538
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3539
|
+
const appState = toolUseContext.getAppState()
|
|
3540
|
+
const sessionId = toolUseContext.agentId ?? getSessionId()
|
|
3541
|
+
if (!hasHookForEvent('PermissionDenied', appState, sessionId)) {
|
|
3542
|
+
return
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
const hookInput: PermissionDeniedHookInput = {
|
|
3546
|
+
...createBaseHookInput(permissionMode, undefined, toolUseContext),
|
|
3547
|
+
hook_event_name: 'PermissionDenied',
|
|
3548
|
+
tool_name: toolName,
|
|
3549
|
+
tool_input: toolInput,
|
|
3550
|
+
tool_use_id: toolUseID,
|
|
3551
|
+
reason,
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
yield* executeHooks({
|
|
3555
|
+
hookInput,
|
|
3556
|
+
toolUseID,
|
|
3557
|
+
matchQuery: toolName,
|
|
3558
|
+
signal,
|
|
3559
|
+
timeoutMs,
|
|
3560
|
+
toolUseContext,
|
|
3561
|
+
})
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
/**
|
|
3565
|
+
* Execute notification hooks if configured
|
|
3566
|
+
* @param notificationData The notification data to pass to hooks
|
|
3567
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3568
|
+
* @returns Promise that resolves when all hooks complete
|
|
3569
|
+
*/
|
|
3570
|
+
export async function executeNotificationHooks(
|
|
3571
|
+
notificationData: {
|
|
3572
|
+
message: string
|
|
3573
|
+
title?: string
|
|
3574
|
+
notificationType: string
|
|
3575
|
+
},
|
|
3576
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3577
|
+
): Promise<void> {
|
|
3578
|
+
const { message, title, notificationType } = notificationData
|
|
3579
|
+
const hookInput: NotificationHookInput = {
|
|
3580
|
+
...createBaseHookInput(undefined),
|
|
3581
|
+
hook_event_name: 'Notification',
|
|
3582
|
+
message,
|
|
3583
|
+
title,
|
|
3584
|
+
notification_type: notificationType,
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
await executeHooksOutsideREPL({
|
|
3588
|
+
hookInput,
|
|
3589
|
+
timeoutMs,
|
|
3590
|
+
matchQuery: notificationType,
|
|
3591
|
+
})
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
export async function executeStopFailureHooks(
|
|
3595
|
+
lastMessage: AssistantMessage,
|
|
3596
|
+
toolUseContext?: ToolUseContext,
|
|
3597
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3598
|
+
): Promise<void> {
|
|
3599
|
+
const appState = toolUseContext?.getAppState()
|
|
3600
|
+
// executeHooksOutsideREPL hardcodes main sessionId (:2738). Agent frontmatter
|
|
3601
|
+
// hooks (registerFrontmatterHooks) key by agentId; gating with agentId here
|
|
3602
|
+
// would pass the gate but fail execution. Align gate with execution.
|
|
3603
|
+
const sessionId = getSessionId()
|
|
3604
|
+
if (!hasHookForEvent('StopFailure', appState, sessionId)) return
|
|
3605
|
+
|
|
3606
|
+
const lastAssistantText =
|
|
3607
|
+
extractTextContent(lastMessage.message.content, '\n').trim() || undefined
|
|
3608
|
+
|
|
3609
|
+
// Some createAssistantAPIErrorMessage call sites omit `error` (e.g.
|
|
3610
|
+
// image-size at errors.ts:431). Default to 'unknown' so matcher filtering
|
|
3611
|
+
// at getMatchingHooks:1525 always applies.
|
|
3612
|
+
const error = lastMessage.error ?? 'unknown'
|
|
3613
|
+
const hookInput: StopFailureHookInput = {
|
|
3614
|
+
...createBaseHookInput(undefined, undefined, toolUseContext),
|
|
3615
|
+
hook_event_name: 'StopFailure',
|
|
3616
|
+
error,
|
|
3617
|
+
error_details: lastMessage.errorDetails,
|
|
3618
|
+
last_assistant_message: lastAssistantText,
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
await executeHooksOutsideREPL({
|
|
3622
|
+
getAppState: toolUseContext?.getAppState,
|
|
3623
|
+
hookInput,
|
|
3624
|
+
timeoutMs,
|
|
3625
|
+
matchQuery: error,
|
|
3626
|
+
})
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
/**
|
|
3630
|
+
* Execute stop hooks if configured
|
|
3631
|
+
* @param toolUseContext ToolUseContext for prompt-based hooks
|
|
3632
|
+
* @param permissionMode permission mode from toolPermissionContext
|
|
3633
|
+
* @param signal AbortSignal to cancel hook execution
|
|
3634
|
+
* @param stopHookActive Whether this call is happening within another stop hook
|
|
3635
|
+
* @param isSubagent Whether the current execution context is a subagent
|
|
3636
|
+
* @param messages Optional conversation history for prompt/function hooks
|
|
3637
|
+
* @returns Async generator that yields progress messages and blocking errors
|
|
3638
|
+
*/
|
|
3639
|
+
export async function* executeStopHooks(
|
|
3640
|
+
permissionMode?: string,
|
|
3641
|
+
signal?: AbortSignal,
|
|
3642
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3643
|
+
stopHookActive: boolean = false,
|
|
3644
|
+
subagentId?: AgentId,
|
|
3645
|
+
toolUseContext?: ToolUseContext,
|
|
3646
|
+
messages?: Message[],
|
|
3647
|
+
agentType?: string,
|
|
3648
|
+
requestPrompt?: (
|
|
3649
|
+
sourceName: string,
|
|
3650
|
+
toolInputSummary?: string | null,
|
|
3651
|
+
) => (request: PromptRequest) => Promise<PromptResponse>,
|
|
3652
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3653
|
+
const hookEvent = subagentId ? 'SubagentStop' : 'Stop'
|
|
3654
|
+
const appState = toolUseContext?.getAppState()
|
|
3655
|
+
const sessionId = toolUseContext?.agentId ?? getSessionId()
|
|
3656
|
+
if (!hasHookForEvent(hookEvent, appState, sessionId)) {
|
|
3657
|
+
return
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
// Extract text content from the last assistant message so hooks can
|
|
3661
|
+
// inspect the final response without reading the transcript file.
|
|
3662
|
+
const lastAssistantMessage = messages
|
|
3663
|
+
? getLastAssistantMessage(messages)
|
|
3664
|
+
: undefined
|
|
3665
|
+
const lastAssistantText = lastAssistantMessage
|
|
3666
|
+
? extractTextContent(lastAssistantMessage.message.content, '\n').trim() ||
|
|
3667
|
+
undefined
|
|
3668
|
+
: undefined
|
|
3669
|
+
|
|
3670
|
+
const hookInput: StopHookInput | SubagentStopHookInput = subagentId
|
|
3671
|
+
? {
|
|
3672
|
+
...createBaseHookInput(permissionMode),
|
|
3673
|
+
hook_event_name: 'SubagentStop',
|
|
3674
|
+
stop_hook_active: stopHookActive,
|
|
3675
|
+
agent_id: subagentId,
|
|
3676
|
+
agent_transcript_path: getAgentTranscriptPath(subagentId),
|
|
3677
|
+
agent_type: agentType ?? '',
|
|
3678
|
+
last_assistant_message: lastAssistantText,
|
|
3679
|
+
}
|
|
3680
|
+
: {
|
|
3681
|
+
...createBaseHookInput(permissionMode),
|
|
3682
|
+
hook_event_name: 'Stop',
|
|
3683
|
+
stop_hook_active: stopHookActive,
|
|
3684
|
+
last_assistant_message: lastAssistantText,
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// Trust check is now centralized in executeHooks()
|
|
3688
|
+
yield* executeHooks({
|
|
3689
|
+
hookInput,
|
|
3690
|
+
toolUseID: randomUUID(),
|
|
3691
|
+
signal,
|
|
3692
|
+
timeoutMs,
|
|
3693
|
+
toolUseContext,
|
|
3694
|
+
messages,
|
|
3695
|
+
requestPrompt,
|
|
3696
|
+
})
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
/**
|
|
3700
|
+
* Execute TeammateIdle hooks when a teammate is about to go idle.
|
|
3701
|
+
* If a hook blocks (exit code 2), the teammate should continue working instead of going idle.
|
|
3702
|
+
* @param teammateName The name of the teammate going idle
|
|
3703
|
+
* @param teamName The team this teammate belongs to
|
|
3704
|
+
* @param permissionMode Optional permission mode
|
|
3705
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3706
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3707
|
+
* @returns Async generator that yields progress messages and blocking errors
|
|
3708
|
+
*/
|
|
3709
|
+
export async function* executeTeammateIdleHooks(
|
|
3710
|
+
teammateName: string,
|
|
3711
|
+
teamName: string,
|
|
3712
|
+
permissionMode?: string,
|
|
3713
|
+
signal?: AbortSignal,
|
|
3714
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3715
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3716
|
+
const hookInput: TeammateIdleHookInput = {
|
|
3717
|
+
...createBaseHookInput(permissionMode),
|
|
3718
|
+
hook_event_name: 'TeammateIdle',
|
|
3719
|
+
teammate_name: teammateName,
|
|
3720
|
+
team_name: teamName,
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
yield* executeHooks({
|
|
3724
|
+
hookInput,
|
|
3725
|
+
toolUseID: randomUUID(),
|
|
3726
|
+
signal,
|
|
3727
|
+
timeoutMs,
|
|
3728
|
+
})
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
/**
|
|
3732
|
+
* Execute TaskCreated hooks when a task is being created.
|
|
3733
|
+
* If a hook blocks (exit code 2), the task creation should be prevented and feedback returned.
|
|
3734
|
+
* @param taskId The ID of the task being created
|
|
3735
|
+
* @param taskSubject The subject/title of the task
|
|
3736
|
+
* @param taskDescription Optional description of the task
|
|
3737
|
+
* @param teammateName Optional name of the teammate creating the task
|
|
3738
|
+
* @param teamName Optional team name
|
|
3739
|
+
* @param permissionMode Optional permission mode
|
|
3740
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3741
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3742
|
+
* @param toolUseContext Optional ToolUseContext for resolving appState and sessionId
|
|
3743
|
+
* @returns Async generator that yields progress messages and blocking errors
|
|
3744
|
+
*/
|
|
3745
|
+
export async function* executeTaskCreatedHooks(
|
|
3746
|
+
taskId: string,
|
|
3747
|
+
taskSubject: string,
|
|
3748
|
+
taskDescription?: string,
|
|
3749
|
+
teammateName?: string,
|
|
3750
|
+
teamName?: string,
|
|
3751
|
+
permissionMode?: string,
|
|
3752
|
+
signal?: AbortSignal,
|
|
3753
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3754
|
+
toolUseContext?: ToolUseContext,
|
|
3755
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3756
|
+
const hookInput: TaskCreatedHookInput = {
|
|
3757
|
+
...createBaseHookInput(permissionMode),
|
|
3758
|
+
hook_event_name: 'TaskCreated',
|
|
3759
|
+
task_id: taskId,
|
|
3760
|
+
task_subject: taskSubject,
|
|
3761
|
+
task_description: taskDescription,
|
|
3762
|
+
teammate_name: teammateName,
|
|
3763
|
+
team_name: teamName,
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
yield* executeHooks({
|
|
3767
|
+
hookInput,
|
|
3768
|
+
toolUseID: randomUUID(),
|
|
3769
|
+
signal,
|
|
3770
|
+
timeoutMs,
|
|
3771
|
+
toolUseContext,
|
|
3772
|
+
})
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
/**
|
|
3776
|
+
* Execute TaskCompleted hooks when a task is being marked as completed.
|
|
3777
|
+
* If a hook blocks (exit code 2), the task completion should be prevented and feedback returned.
|
|
3778
|
+
* @param taskId The ID of the task being completed
|
|
3779
|
+
* @param taskSubject The subject/title of the task
|
|
3780
|
+
* @param taskDescription Optional description of the task
|
|
3781
|
+
* @param teammateName Optional name of the teammate completing the task
|
|
3782
|
+
* @param teamName Optional team name
|
|
3783
|
+
* @param permissionMode Optional permission mode
|
|
3784
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3785
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3786
|
+
* @param toolUseContext Optional ToolUseContext for resolving appState and sessionId
|
|
3787
|
+
* @returns Async generator that yields progress messages and blocking errors
|
|
3788
|
+
*/
|
|
3789
|
+
export async function* executeTaskCompletedHooks(
|
|
3790
|
+
taskId: string,
|
|
3791
|
+
taskSubject: string,
|
|
3792
|
+
taskDescription?: string,
|
|
3793
|
+
teammateName?: string,
|
|
3794
|
+
teamName?: string,
|
|
3795
|
+
permissionMode?: string,
|
|
3796
|
+
signal?: AbortSignal,
|
|
3797
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3798
|
+
toolUseContext?: ToolUseContext,
|
|
3799
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3800
|
+
const hookInput: TaskCompletedHookInput = {
|
|
3801
|
+
...createBaseHookInput(permissionMode),
|
|
3802
|
+
hook_event_name: 'TaskCompleted',
|
|
3803
|
+
task_id: taskId,
|
|
3804
|
+
task_subject: taskSubject,
|
|
3805
|
+
task_description: taskDescription,
|
|
3806
|
+
teammate_name: teammateName,
|
|
3807
|
+
team_name: teamName,
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
yield* executeHooks({
|
|
3811
|
+
hookInput,
|
|
3812
|
+
toolUseID: randomUUID(),
|
|
3813
|
+
signal,
|
|
3814
|
+
timeoutMs,
|
|
3815
|
+
toolUseContext,
|
|
3816
|
+
})
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
/**
|
|
3820
|
+
* Execute start hooks if configured
|
|
3821
|
+
* @param prompt The user prompt that will be passed to the tool
|
|
3822
|
+
* @param permissionMode Permission mode from toolPermissionContext
|
|
3823
|
+
* @param toolUseContext ToolUseContext for prompt-based hooks
|
|
3824
|
+
* @returns Async generator that yields progress messages and hook results
|
|
3825
|
+
*/
|
|
3826
|
+
export async function* executeUserPromptSubmitHooks(
|
|
3827
|
+
prompt: string,
|
|
3828
|
+
permissionMode: string,
|
|
3829
|
+
toolUseContext: ToolUseContext,
|
|
3830
|
+
requestPrompt?: (
|
|
3831
|
+
sourceName: string,
|
|
3832
|
+
toolInputSummary?: string | null,
|
|
3833
|
+
) => (request: PromptRequest) => Promise<PromptResponse>,
|
|
3834
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3835
|
+
const appState = toolUseContext.getAppState()
|
|
3836
|
+
const sessionId = toolUseContext.agentId ?? getSessionId()
|
|
3837
|
+
if (!hasHookForEvent('UserPromptSubmit', appState, sessionId)) {
|
|
3838
|
+
return
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
const hookInput: UserPromptSubmitHookInput = {
|
|
3842
|
+
...createBaseHookInput(permissionMode),
|
|
3843
|
+
hook_event_name: 'UserPromptSubmit',
|
|
3844
|
+
prompt,
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
yield* executeHooks({
|
|
3848
|
+
hookInput,
|
|
3849
|
+
toolUseID: randomUUID(),
|
|
3850
|
+
signal: toolUseContext.abortController.signal,
|
|
3851
|
+
timeoutMs: TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3852
|
+
toolUseContext,
|
|
3853
|
+
requestPrompt,
|
|
3854
|
+
})
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
/**
|
|
3858
|
+
* Execute session start hooks if configured
|
|
3859
|
+
* @param source The source of the session start (startup, resume, clear)
|
|
3860
|
+
* @param sessionId Optional The session id to use as hook input
|
|
3861
|
+
* @param agentType Optional The agent type (from --agent flag) running this session
|
|
3862
|
+
* @param model Optional The model being used for this session
|
|
3863
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3864
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3865
|
+
* @returns Async generator that yields progress messages and hook results
|
|
3866
|
+
*/
|
|
3867
|
+
export async function* executeSessionStartHooks(
|
|
3868
|
+
source: 'startup' | 'resume' | 'clear' | 'compact',
|
|
3869
|
+
sessionId?: string,
|
|
3870
|
+
agentType?: string,
|
|
3871
|
+
model?: string,
|
|
3872
|
+
signal?: AbortSignal,
|
|
3873
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3874
|
+
forceSyncExecution?: boolean,
|
|
3875
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3876
|
+
const hookInput: SessionStartHookInput = {
|
|
3877
|
+
...createBaseHookInput(undefined, sessionId),
|
|
3878
|
+
hook_event_name: 'SessionStart',
|
|
3879
|
+
source,
|
|
3880
|
+
agent_type: agentType,
|
|
3881
|
+
model,
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
yield* executeHooks({
|
|
3885
|
+
hookInput,
|
|
3886
|
+
toolUseID: randomUUID(),
|
|
3887
|
+
matchQuery: source,
|
|
3888
|
+
signal,
|
|
3889
|
+
timeoutMs,
|
|
3890
|
+
forceSyncExecution,
|
|
3891
|
+
})
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
/**
|
|
3895
|
+
* Execute setup hooks if configured
|
|
3896
|
+
* @param trigger The trigger type ('init' or 'maintenance')
|
|
3897
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3898
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3899
|
+
* @param forceSyncExecution If true, async hooks will not be backgrounded
|
|
3900
|
+
* @returns Async generator that yields progress messages and hook results
|
|
3901
|
+
*/
|
|
3902
|
+
export async function* executeSetupHooks(
|
|
3903
|
+
trigger: 'init' | 'maintenance',
|
|
3904
|
+
signal?: AbortSignal,
|
|
3905
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3906
|
+
forceSyncExecution?: boolean,
|
|
3907
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3908
|
+
const hookInput: SetupHookInput = {
|
|
3909
|
+
...createBaseHookInput(undefined),
|
|
3910
|
+
hook_event_name: 'Setup',
|
|
3911
|
+
trigger,
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
yield* executeHooks({
|
|
3915
|
+
hookInput,
|
|
3916
|
+
toolUseID: randomUUID(),
|
|
3917
|
+
matchQuery: trigger,
|
|
3918
|
+
signal,
|
|
3919
|
+
timeoutMs,
|
|
3920
|
+
forceSyncExecution,
|
|
3921
|
+
})
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3924
|
+
/**
|
|
3925
|
+
* Execute subagent start hooks if configured
|
|
3926
|
+
* @param agentId The unique identifier for the subagent
|
|
3927
|
+
* @param agentType The type/name of the subagent being started
|
|
3928
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3929
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3930
|
+
* @returns Async generator that yields progress messages and hook results
|
|
3931
|
+
*/
|
|
3932
|
+
export async function* executeSubagentStartHooks(
|
|
3933
|
+
agentId: string,
|
|
3934
|
+
agentType: string,
|
|
3935
|
+
signal?: AbortSignal,
|
|
3936
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3937
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
3938
|
+
const hookInput: SubagentStartHookInput = {
|
|
3939
|
+
...createBaseHookInput(undefined),
|
|
3940
|
+
hook_event_name: 'SubagentStart',
|
|
3941
|
+
agent_id: agentId,
|
|
3942
|
+
agent_type: agentType,
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
yield* executeHooks({
|
|
3946
|
+
hookInput,
|
|
3947
|
+
toolUseID: randomUUID(),
|
|
3948
|
+
matchQuery: agentType,
|
|
3949
|
+
signal,
|
|
3950
|
+
timeoutMs,
|
|
3951
|
+
})
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
/**
|
|
3955
|
+
* Execute pre-compact hooks if configured
|
|
3956
|
+
* @param compactData The compact data to pass to hooks
|
|
3957
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
3958
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
3959
|
+
* @returns Object with optional newCustomInstructions and userDisplayMessage
|
|
3960
|
+
*/
|
|
3961
|
+
export async function executePreCompactHooks(
|
|
3962
|
+
compactData: {
|
|
3963
|
+
trigger: 'manual' | 'auto'
|
|
3964
|
+
customInstructions: string | null
|
|
3965
|
+
},
|
|
3966
|
+
signal?: AbortSignal,
|
|
3967
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
3968
|
+
): Promise<{
|
|
3969
|
+
newCustomInstructions?: string
|
|
3970
|
+
userDisplayMessage?: string
|
|
3971
|
+
}> {
|
|
3972
|
+
const hookInput: PreCompactHookInput = {
|
|
3973
|
+
...createBaseHookInput(undefined),
|
|
3974
|
+
hook_event_name: 'PreCompact',
|
|
3975
|
+
trigger: compactData.trigger,
|
|
3976
|
+
custom_instructions: compactData.customInstructions,
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
const results = await executeHooksOutsideREPL({
|
|
3980
|
+
hookInput,
|
|
3981
|
+
matchQuery: compactData.trigger,
|
|
3982
|
+
signal,
|
|
3983
|
+
timeoutMs,
|
|
3984
|
+
})
|
|
3985
|
+
|
|
3986
|
+
if (results.length === 0) {
|
|
3987
|
+
return {}
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
// Extract custom instructions from successful hooks with non-empty output
|
|
3991
|
+
const successfulOutputs = results
|
|
3992
|
+
.filter(result => result.succeeded && result.output.trim().length > 0)
|
|
3993
|
+
.map(result => result.output.trim())
|
|
3994
|
+
|
|
3995
|
+
// Build user display messages with command info
|
|
3996
|
+
const displayMessages: string[] = []
|
|
3997
|
+
for (const result of results) {
|
|
3998
|
+
if (result.succeeded) {
|
|
3999
|
+
if (result.output.trim()) {
|
|
4000
|
+
displayMessages.push(
|
|
4001
|
+
`PreCompact [${result.command}] completed successfully: ${result.output.trim()}`,
|
|
4002
|
+
)
|
|
4003
|
+
} else {
|
|
4004
|
+
displayMessages.push(
|
|
4005
|
+
`PreCompact [${result.command}] completed successfully`,
|
|
4006
|
+
)
|
|
4007
|
+
}
|
|
4008
|
+
} else {
|
|
4009
|
+
if (result.output.trim()) {
|
|
4010
|
+
displayMessages.push(
|
|
4011
|
+
`PreCompact [${result.command}] failed: ${result.output.trim()}`,
|
|
4012
|
+
)
|
|
4013
|
+
} else {
|
|
4014
|
+
displayMessages.push(`PreCompact [${result.command}] failed`)
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
return {
|
|
4020
|
+
newCustomInstructions:
|
|
4021
|
+
successfulOutputs.length > 0 ? successfulOutputs.join('\n\n') : undefined,
|
|
4022
|
+
userDisplayMessage:
|
|
4023
|
+
displayMessages.length > 0 ? displayMessages.join('\n') : undefined,
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
/**
|
|
4028
|
+
* Execute post-compact hooks if configured
|
|
4029
|
+
* @param compactData The compact data to pass to hooks, including the summary
|
|
4030
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
4031
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
4032
|
+
* @returns Object with optional userDisplayMessage
|
|
4033
|
+
*/
|
|
4034
|
+
export async function executePostCompactHooks(
|
|
4035
|
+
compactData: {
|
|
4036
|
+
trigger: 'manual' | 'auto'
|
|
4037
|
+
compactSummary: string
|
|
4038
|
+
},
|
|
4039
|
+
signal?: AbortSignal,
|
|
4040
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4041
|
+
): Promise<{
|
|
4042
|
+
userDisplayMessage?: string
|
|
4043
|
+
}> {
|
|
4044
|
+
const hookInput: PostCompactHookInput = {
|
|
4045
|
+
...createBaseHookInput(undefined),
|
|
4046
|
+
hook_event_name: 'PostCompact',
|
|
4047
|
+
trigger: compactData.trigger,
|
|
4048
|
+
compact_summary: compactData.compactSummary,
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
const results = await executeHooksOutsideREPL({
|
|
4052
|
+
hookInput,
|
|
4053
|
+
matchQuery: compactData.trigger,
|
|
4054
|
+
signal,
|
|
4055
|
+
timeoutMs,
|
|
4056
|
+
})
|
|
4057
|
+
|
|
4058
|
+
if (results.length === 0) {
|
|
4059
|
+
return {}
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
const displayMessages: string[] = []
|
|
4063
|
+
for (const result of results) {
|
|
4064
|
+
if (result.succeeded) {
|
|
4065
|
+
if (result.output.trim()) {
|
|
4066
|
+
displayMessages.push(
|
|
4067
|
+
`PostCompact [${result.command}] completed successfully: ${result.output.trim()}`,
|
|
4068
|
+
)
|
|
4069
|
+
} else {
|
|
4070
|
+
displayMessages.push(
|
|
4071
|
+
`PostCompact [${result.command}] completed successfully`,
|
|
4072
|
+
)
|
|
4073
|
+
}
|
|
4074
|
+
} else {
|
|
4075
|
+
if (result.output.trim()) {
|
|
4076
|
+
displayMessages.push(
|
|
4077
|
+
`PostCompact [${result.command}] failed: ${result.output.trim()}`,
|
|
4078
|
+
)
|
|
4079
|
+
} else {
|
|
4080
|
+
displayMessages.push(`PostCompact [${result.command}] failed`)
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
return {
|
|
4086
|
+
userDisplayMessage:
|
|
4087
|
+
displayMessages.length > 0 ? displayMessages.join('\n') : undefined,
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
/**
|
|
4092
|
+
* Execute session end hooks if configured
|
|
4093
|
+
* @param reason The reason for ending the session
|
|
4094
|
+
* @param options Optional parameters including app state functions and signal
|
|
4095
|
+
* @returns Promise that resolves when all hooks complete
|
|
4096
|
+
*/
|
|
4097
|
+
export async function executeSessionEndHooks(
|
|
4098
|
+
reason: ExitReason,
|
|
4099
|
+
options?: {
|
|
4100
|
+
getAppState?: () => AppState
|
|
4101
|
+
setAppState?: (updater: (prev: AppState) => AppState) => void
|
|
4102
|
+
signal?: AbortSignal
|
|
4103
|
+
timeoutMs?: number
|
|
4104
|
+
},
|
|
4105
|
+
): Promise<void> {
|
|
4106
|
+
const {
|
|
4107
|
+
getAppState,
|
|
4108
|
+
setAppState,
|
|
4109
|
+
signal,
|
|
4110
|
+
timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4111
|
+
} = options || {}
|
|
4112
|
+
|
|
4113
|
+
const hookInput: SessionEndHookInput = {
|
|
4114
|
+
...createBaseHookInput(undefined),
|
|
4115
|
+
hook_event_name: 'SessionEnd',
|
|
4116
|
+
reason,
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
const results = await executeHooksOutsideREPL({
|
|
4120
|
+
getAppState,
|
|
4121
|
+
hookInput,
|
|
4122
|
+
matchQuery: reason,
|
|
4123
|
+
signal,
|
|
4124
|
+
timeoutMs,
|
|
4125
|
+
})
|
|
4126
|
+
|
|
4127
|
+
// During shutdown, Ink is unmounted so we can write directly to stderr
|
|
4128
|
+
for (const result of results) {
|
|
4129
|
+
if (!result.succeeded && result.output) {
|
|
4130
|
+
process.stderr.write(
|
|
4131
|
+
`SessionEnd hook [${result.command}] failed: ${result.output}\n`,
|
|
4132
|
+
)
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
// Clear session hooks after execution
|
|
4137
|
+
if (setAppState) {
|
|
4138
|
+
const sessionId = getSessionId()
|
|
4139
|
+
clearSessionHooks(setAppState, sessionId)
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
/**
|
|
4144
|
+
* Execute permission request hooks if configured
|
|
4145
|
+
* These hooks are called when a permission dialog would be displayed to the user.
|
|
4146
|
+
* Hooks can approve or deny the permission request programmatically.
|
|
4147
|
+
* @param toolName The name of the tool requesting permission
|
|
4148
|
+
* @param toolUseID The ID of the tool use
|
|
4149
|
+
* @param toolInput The input that would be passed to the tool
|
|
4150
|
+
* @param toolUseContext ToolUseContext for the request
|
|
4151
|
+
* @param permissionMode Optional permission mode from toolPermissionContext
|
|
4152
|
+
* @param permissionSuggestions Optional permission suggestions (the "always allow" options)
|
|
4153
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
4154
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
4155
|
+
* @returns Async generator that yields progress messages and returns aggregated result
|
|
4156
|
+
*/
|
|
4157
|
+
export async function* executePermissionRequestHooks<ToolInput>(
|
|
4158
|
+
toolName: string,
|
|
4159
|
+
toolUseID: string,
|
|
4160
|
+
toolInput: ToolInput,
|
|
4161
|
+
toolUseContext: ToolUseContext,
|
|
4162
|
+
permissionMode?: string,
|
|
4163
|
+
permissionSuggestions?: PermissionUpdate[],
|
|
4164
|
+
signal?: AbortSignal,
|
|
4165
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4166
|
+
requestPrompt?: (
|
|
4167
|
+
sourceName: string,
|
|
4168
|
+
toolInputSummary?: string | null,
|
|
4169
|
+
) => (request: PromptRequest) => Promise<PromptResponse>,
|
|
4170
|
+
toolInputSummary?: string | null,
|
|
4171
|
+
): AsyncGenerator<AggregatedHookResult> {
|
|
4172
|
+
logForDebugging(`executePermissionRequestHooks called for tool: ${toolName}`)
|
|
4173
|
+
|
|
4174
|
+
const hookInput: PermissionRequestHookInput = {
|
|
4175
|
+
...createBaseHookInput(permissionMode, undefined, toolUseContext),
|
|
4176
|
+
hook_event_name: 'PermissionRequest',
|
|
4177
|
+
tool_name: toolName,
|
|
4178
|
+
tool_input: toolInput,
|
|
4179
|
+
permission_suggestions: permissionSuggestions,
|
|
4180
|
+
}
|
|
4181
|
+
|
|
4182
|
+
yield* executeHooks({
|
|
4183
|
+
hookInput,
|
|
4184
|
+
toolUseID,
|
|
4185
|
+
matchQuery: toolName,
|
|
4186
|
+
signal,
|
|
4187
|
+
timeoutMs,
|
|
4188
|
+
toolUseContext,
|
|
4189
|
+
requestPrompt,
|
|
4190
|
+
toolInputSummary,
|
|
4191
|
+
})
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
export type ConfigChangeSource =
|
|
4195
|
+
| 'user_settings'
|
|
4196
|
+
| 'project_settings'
|
|
4197
|
+
| 'local_settings'
|
|
4198
|
+
| 'policy_settings'
|
|
4199
|
+
| 'skills'
|
|
4200
|
+
|
|
4201
|
+
/**
|
|
4202
|
+
* Execute config change hooks when configuration files change during a session.
|
|
4203
|
+
* Fired by file watchers when settings, skills, or commands change on disk.
|
|
4204
|
+
* Enables enterprise admins to audit/log configuration changes for security.
|
|
4205
|
+
*
|
|
4206
|
+
* Policy settings are enterprise-managed and must never be blockable by hooks.
|
|
4207
|
+
* Hooks still fire (for audit logging) but blocking results are ignored — callers
|
|
4208
|
+
* will always see an empty result for policy sources.
|
|
4209
|
+
*
|
|
4210
|
+
* @param source The type of config that changed
|
|
4211
|
+
* @param filePath Optional path to the changed file
|
|
4212
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
4213
|
+
*/
|
|
4214
|
+
export async function executeConfigChangeHooks(
|
|
4215
|
+
source: ConfigChangeSource,
|
|
4216
|
+
filePath?: string,
|
|
4217
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4218
|
+
): Promise<HookOutsideReplResult[]> {
|
|
4219
|
+
const hookInput: ConfigChangeHookInput = {
|
|
4220
|
+
...createBaseHookInput(undefined),
|
|
4221
|
+
hook_event_name: 'ConfigChange',
|
|
4222
|
+
source,
|
|
4223
|
+
file_path: filePath,
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
const results = await executeHooksOutsideREPL({
|
|
4227
|
+
hookInput,
|
|
4228
|
+
timeoutMs,
|
|
4229
|
+
matchQuery: source,
|
|
4230
|
+
})
|
|
4231
|
+
|
|
4232
|
+
// Policy settings are enterprise-managed — hooks fire for audit logging
|
|
4233
|
+
// but must never block policy changes from being applied
|
|
4234
|
+
if (source === 'policy_settings') {
|
|
4235
|
+
return results.map(r => ({ ...r, blocked: false }))
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
return results
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
async function executeEnvHooks(
|
|
4242
|
+
hookInput: HookInput,
|
|
4243
|
+
timeoutMs: number,
|
|
4244
|
+
): Promise<{
|
|
4245
|
+
results: HookOutsideReplResult[]
|
|
4246
|
+
watchPaths: string[]
|
|
4247
|
+
systemMessages: string[]
|
|
4248
|
+
}> {
|
|
4249
|
+
const results = await executeHooksOutsideREPL({ hookInput, timeoutMs })
|
|
4250
|
+
if (results.length > 0) {
|
|
4251
|
+
invalidateSessionEnvCache()
|
|
4252
|
+
}
|
|
4253
|
+
const watchPaths = results.flatMap(r => r.watchPaths ?? [])
|
|
4254
|
+
const systemMessages = results
|
|
4255
|
+
.map(r => r.systemMessage)
|
|
4256
|
+
.filter((m): m is string => !!m)
|
|
4257
|
+
return { results, watchPaths, systemMessages }
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
export function executeCwdChangedHooks(
|
|
4261
|
+
oldCwd: string,
|
|
4262
|
+
newCwd: string,
|
|
4263
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4264
|
+
): Promise<{
|
|
4265
|
+
results: HookOutsideReplResult[]
|
|
4266
|
+
watchPaths: string[]
|
|
4267
|
+
systemMessages: string[]
|
|
4268
|
+
}> {
|
|
4269
|
+
const hookInput: CwdChangedHookInput = {
|
|
4270
|
+
...createBaseHookInput(undefined),
|
|
4271
|
+
hook_event_name: 'CwdChanged',
|
|
4272
|
+
old_cwd: oldCwd,
|
|
4273
|
+
new_cwd: newCwd,
|
|
4274
|
+
}
|
|
4275
|
+
return executeEnvHooks(hookInput, timeoutMs)
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
export function executeFileChangedHooks(
|
|
4279
|
+
filePath: string,
|
|
4280
|
+
event: 'change' | 'add' | 'unlink',
|
|
4281
|
+
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4282
|
+
): Promise<{
|
|
4283
|
+
results: HookOutsideReplResult[]
|
|
4284
|
+
watchPaths: string[]
|
|
4285
|
+
systemMessages: string[]
|
|
4286
|
+
}> {
|
|
4287
|
+
const hookInput: FileChangedHookInput = {
|
|
4288
|
+
...createBaseHookInput(undefined),
|
|
4289
|
+
hook_event_name: 'FileChanged',
|
|
4290
|
+
file_path: filePath,
|
|
4291
|
+
event,
|
|
4292
|
+
}
|
|
4293
|
+
return executeEnvHooks(hookInput, timeoutMs)
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
export type InstructionsLoadReason =
|
|
4297
|
+
| 'session_start'
|
|
4298
|
+
| 'nested_traversal'
|
|
4299
|
+
| 'path_glob_match'
|
|
4300
|
+
| 'include'
|
|
4301
|
+
| 'compact'
|
|
4302
|
+
|
|
4303
|
+
export type InstructionsMemoryType = 'User' | 'Project' | 'Local' | 'Managed'
|
|
4304
|
+
|
|
4305
|
+
/**
|
|
4306
|
+
* Check if InstructionsLoaded hooks are configured (without executing them).
|
|
4307
|
+
* Callers should check this before invoking executeInstructionsLoadedHooks to avoid
|
|
4308
|
+
* building hook inputs for every instruction file when no hook is configured.
|
|
4309
|
+
*
|
|
4310
|
+
* Checks both settings-file hooks (getHooksConfigFromSnapshot) and registered
|
|
4311
|
+
* hooks (plugin hooks + SDK callback hooks via registerHookCallbacks). Session-
|
|
4312
|
+
* derived hooks (structured output enforcement etc.) are internal and not checked.
|
|
4313
|
+
*/
|
|
4314
|
+
export function hasInstructionsLoadedHook(): boolean {
|
|
4315
|
+
const snapshotHooks = getHooksConfigFromSnapshot()?.['InstructionsLoaded']
|
|
4316
|
+
if (snapshotHooks && snapshotHooks.length > 0) return true
|
|
4317
|
+
const registeredHooks = getRegisteredHooks()?.['InstructionsLoaded']
|
|
4318
|
+
if (registeredHooks && registeredHooks.length > 0) return true
|
|
4319
|
+
return false
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
/**
|
|
4323
|
+
* Execute InstructionsLoaded hooks when an instruction file (CLAUDE.md or
|
|
4324
|
+
* .claude/rules/*.md) is loaded into context. Fire-and-forget — this hook is
|
|
4325
|
+
* for observability/audit only and does not support blocking.
|
|
4326
|
+
*
|
|
4327
|
+
* Dispatch sites:
|
|
4328
|
+
* - Eager load at session start (getMemoryFiles in claudemd.ts)
|
|
4329
|
+
* - Eager reload after compaction (getMemoryFiles cache cleared by
|
|
4330
|
+
* runPostCompactCleanup; next call reports load_reason: 'compact')
|
|
4331
|
+
* - Lazy load when Claude touches a file that triggers nested CLAUDE.md or
|
|
4332
|
+
* conditional rules with paths: frontmatter (memoryFilesToAttachments in
|
|
4333
|
+
* attachments.ts)
|
|
4334
|
+
*/
|
|
4335
|
+
export async function executeInstructionsLoadedHooks(
|
|
4336
|
+
filePath: string,
|
|
4337
|
+
memoryType: InstructionsMemoryType,
|
|
4338
|
+
loadReason: InstructionsLoadReason,
|
|
4339
|
+
options?: {
|
|
4340
|
+
globs?: string[]
|
|
4341
|
+
triggerFilePath?: string
|
|
4342
|
+
parentFilePath?: string
|
|
4343
|
+
timeoutMs?: number
|
|
4344
|
+
},
|
|
4345
|
+
): Promise<void> {
|
|
4346
|
+
const {
|
|
4347
|
+
globs,
|
|
4348
|
+
triggerFilePath,
|
|
4349
|
+
parentFilePath,
|
|
4350
|
+
timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4351
|
+
} = options ?? {}
|
|
4352
|
+
|
|
4353
|
+
const hookInput: InstructionsLoadedHookInput = {
|
|
4354
|
+
...createBaseHookInput(undefined),
|
|
4355
|
+
hook_event_name: 'InstructionsLoaded',
|
|
4356
|
+
file_path: filePath,
|
|
4357
|
+
memory_type: memoryType,
|
|
4358
|
+
load_reason: loadReason,
|
|
4359
|
+
globs,
|
|
4360
|
+
trigger_file_path: triggerFilePath,
|
|
4361
|
+
parent_file_path: parentFilePath,
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
await executeHooksOutsideREPL({
|
|
4365
|
+
hookInput,
|
|
4366
|
+
timeoutMs,
|
|
4367
|
+
matchQuery: loadReason,
|
|
4368
|
+
})
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4371
|
+
/** Result of an elicitation hook execution (non-REPL path). */
|
|
4372
|
+
export type ElicitationHookResult = {
|
|
4373
|
+
elicitationResponse?: ElicitationResponse
|
|
4374
|
+
blockingError?: HookBlockingError
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
/** Result of an elicitation-result hook execution (non-REPL path). */
|
|
4378
|
+
export type ElicitationResultHookResult = {
|
|
4379
|
+
elicitationResultResponse?: ElicitationResponse
|
|
4380
|
+
blockingError?: HookBlockingError
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
/**
|
|
4384
|
+
* Parse elicitation-specific fields from a HookOutsideReplResult.
|
|
4385
|
+
* Mirrors the relevant branches of processHookJSONOutput for Elicitation
|
|
4386
|
+
* and ElicitationResult hook events.
|
|
4387
|
+
*/
|
|
4388
|
+
function parseElicitationHookOutput(
|
|
4389
|
+
result: HookOutsideReplResult,
|
|
4390
|
+
expectedEventName: 'Elicitation' | 'ElicitationResult',
|
|
4391
|
+
): {
|
|
4392
|
+
response?: ElicitationResponse
|
|
4393
|
+
blockingError?: HookBlockingError
|
|
4394
|
+
} {
|
|
4395
|
+
// Exit code 2 = blocking (same as executeHooks path)
|
|
4396
|
+
if (result.blocked && !result.succeeded) {
|
|
4397
|
+
return {
|
|
4398
|
+
blockingError: {
|
|
4399
|
+
blockingError: result.output || `Elicitation blocked by hook`,
|
|
4400
|
+
command: result.command,
|
|
4401
|
+
},
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
if (!result.output.trim()) {
|
|
4406
|
+
return {}
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
// Try to parse JSON output for structured elicitation response
|
|
4410
|
+
const trimmed = result.output.trim()
|
|
4411
|
+
if (!trimmed.startsWith('{')) {
|
|
4412
|
+
return {}
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
try {
|
|
4416
|
+
const parsed = hookJSONOutputSchema().parse(JSON.parse(trimmed))
|
|
4417
|
+
if (isAsyncHookJSONOutput(parsed)) {
|
|
4418
|
+
return {}
|
|
4419
|
+
}
|
|
4420
|
+
if (!isSyncHookJSONOutput(parsed)) {
|
|
4421
|
+
return {}
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4424
|
+
// Check for top-level decision: 'block' (exit code 0 + JSON block)
|
|
4425
|
+
if (parsed.decision === 'block' || result.blocked) {
|
|
4426
|
+
return {
|
|
4427
|
+
blockingError: {
|
|
4428
|
+
blockingError: parsed.reason || 'Elicitation blocked by hook',
|
|
4429
|
+
command: result.command,
|
|
4430
|
+
},
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
const specific = parsed.hookSpecificOutput
|
|
4435
|
+
if (!specific || specific.hookEventName !== expectedEventName) {
|
|
4436
|
+
return {}
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
if (!specific.action) {
|
|
4440
|
+
return {}
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
const response: ElicitationResponse = {
|
|
4444
|
+
action: specific.action,
|
|
4445
|
+
content: specific.content as ElicitationResponse['content'] | undefined,
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
const out: {
|
|
4449
|
+
response?: ElicitationResponse
|
|
4450
|
+
blockingError?: HookBlockingError
|
|
4451
|
+
} = { response }
|
|
4452
|
+
|
|
4453
|
+
if (specific.action === 'decline') {
|
|
4454
|
+
out.blockingError = {
|
|
4455
|
+
blockingError:
|
|
4456
|
+
parsed.reason ||
|
|
4457
|
+
(expectedEventName === 'Elicitation'
|
|
4458
|
+
? 'Elicitation denied by hook'
|
|
4459
|
+
: 'Elicitation result blocked by hook'),
|
|
4460
|
+
command: result.command,
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
return out
|
|
4465
|
+
} catch {
|
|
4466
|
+
return {}
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
export async function executeElicitationHooks({
|
|
4471
|
+
serverName,
|
|
4472
|
+
message,
|
|
4473
|
+
requestedSchema,
|
|
4474
|
+
permissionMode,
|
|
4475
|
+
signal,
|
|
4476
|
+
timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4477
|
+
mode,
|
|
4478
|
+
url,
|
|
4479
|
+
elicitationId,
|
|
4480
|
+
}: {
|
|
4481
|
+
serverName: string
|
|
4482
|
+
message: string
|
|
4483
|
+
requestedSchema?: Record<string, unknown>
|
|
4484
|
+
permissionMode?: string
|
|
4485
|
+
signal?: AbortSignal
|
|
4486
|
+
timeoutMs?: number
|
|
4487
|
+
mode?: 'form' | 'url'
|
|
4488
|
+
url?: string
|
|
4489
|
+
elicitationId?: string
|
|
4490
|
+
}): Promise<ElicitationHookResult> {
|
|
4491
|
+
const hookInput: ElicitationHookInput = {
|
|
4492
|
+
...createBaseHookInput(permissionMode),
|
|
4493
|
+
hook_event_name: 'Elicitation',
|
|
4494
|
+
mcp_server_name: serverName,
|
|
4495
|
+
message,
|
|
4496
|
+
mode,
|
|
4497
|
+
url,
|
|
4498
|
+
elicitation_id: elicitationId,
|
|
4499
|
+
requested_schema: requestedSchema,
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
const results = await executeHooksOutsideREPL({
|
|
4503
|
+
hookInput,
|
|
4504
|
+
matchQuery: serverName,
|
|
4505
|
+
signal,
|
|
4506
|
+
timeoutMs,
|
|
4507
|
+
})
|
|
4508
|
+
|
|
4509
|
+
let elicitationResponse: ElicitationResponse | undefined
|
|
4510
|
+
let blockingError: HookBlockingError | undefined
|
|
4511
|
+
|
|
4512
|
+
for (const result of results) {
|
|
4513
|
+
const parsed = parseElicitationHookOutput(result, 'Elicitation')
|
|
4514
|
+
if (parsed.blockingError) {
|
|
4515
|
+
blockingError = parsed.blockingError
|
|
4516
|
+
}
|
|
4517
|
+
if (parsed.response) {
|
|
4518
|
+
elicitationResponse = parsed.response
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
return { elicitationResponse, blockingError }
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
export async function executeElicitationResultHooks({
|
|
4526
|
+
serverName,
|
|
4527
|
+
action,
|
|
4528
|
+
content,
|
|
4529
|
+
permissionMode,
|
|
4530
|
+
signal,
|
|
4531
|
+
timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4532
|
+
mode,
|
|
4533
|
+
elicitationId,
|
|
4534
|
+
}: {
|
|
4535
|
+
serverName: string
|
|
4536
|
+
action: 'accept' | 'decline' | 'cancel'
|
|
4537
|
+
content?: Record<string, unknown>
|
|
4538
|
+
permissionMode?: string
|
|
4539
|
+
signal?: AbortSignal
|
|
4540
|
+
timeoutMs?: number
|
|
4541
|
+
mode?: 'form' | 'url'
|
|
4542
|
+
elicitationId?: string
|
|
4543
|
+
}): Promise<ElicitationResultHookResult> {
|
|
4544
|
+
const hookInput: ElicitationResultHookInput = {
|
|
4545
|
+
...createBaseHookInput(permissionMode),
|
|
4546
|
+
hook_event_name: 'ElicitationResult',
|
|
4547
|
+
mcp_server_name: serverName,
|
|
4548
|
+
elicitation_id: elicitationId,
|
|
4549
|
+
mode,
|
|
4550
|
+
action,
|
|
4551
|
+
content,
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
const results = await executeHooksOutsideREPL({
|
|
4555
|
+
hookInput,
|
|
4556
|
+
matchQuery: serverName,
|
|
4557
|
+
signal,
|
|
4558
|
+
timeoutMs,
|
|
4559
|
+
})
|
|
4560
|
+
|
|
4561
|
+
let elicitationResultResponse: ElicitationResponse | undefined
|
|
4562
|
+
let blockingError: HookBlockingError | undefined
|
|
4563
|
+
|
|
4564
|
+
for (const result of results) {
|
|
4565
|
+
const parsed = parseElicitationHookOutput(result, 'ElicitationResult')
|
|
4566
|
+
if (parsed.blockingError) {
|
|
4567
|
+
blockingError = parsed.blockingError
|
|
4568
|
+
}
|
|
4569
|
+
if (parsed.response) {
|
|
4570
|
+
elicitationResultResponse = parsed.response
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
return { elicitationResultResponse, blockingError }
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
/**
|
|
4578
|
+
* Execute status line command if configured
|
|
4579
|
+
* @param statusLineInput The structured status input that will be converted to JSON
|
|
4580
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
4581
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
4582
|
+
* @returns The status line text to display, or undefined if no command configured
|
|
4583
|
+
*/
|
|
4584
|
+
export async function executeStatusLineCommand(
|
|
4585
|
+
statusLineInput: StatusLineCommandInput,
|
|
4586
|
+
signal?: AbortSignal,
|
|
4587
|
+
timeoutMs: number = 5000, // Short timeout for status line
|
|
4588
|
+
logResult: boolean = false,
|
|
4589
|
+
): Promise<string | undefined> {
|
|
4590
|
+
// Check if all hooks (including statusLine) are disabled by managed settings
|
|
4591
|
+
if (shouldDisableAllHooksIncludingManaged()) {
|
|
4592
|
+
return undefined
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4595
|
+
// SECURITY: ALL hooks require workspace trust in interactive mode
|
|
4596
|
+
// This centralized check prevents RCE vulnerabilities for all current and future hooks
|
|
4597
|
+
if (shouldSkipHookDueToTrust()) {
|
|
4598
|
+
logForDebugging(
|
|
4599
|
+
`Skipping StatusLine command execution - workspace trust not accepted`,
|
|
4600
|
+
)
|
|
4601
|
+
return undefined
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
// When disableAllHooks is set in non-managed settings, only managed statusLine runs
|
|
4605
|
+
// (non-managed settings cannot disable managed commands, but non-managed commands are disabled)
|
|
4606
|
+
let statusLine
|
|
4607
|
+
if (shouldAllowManagedHooksOnly()) {
|
|
4608
|
+
statusLine = getSettingsForSource('policySettings')?.statusLine
|
|
4609
|
+
} else {
|
|
4610
|
+
statusLine = getSettings_DEPRECATED()?.statusLine
|
|
4611
|
+
}
|
|
4612
|
+
|
|
4613
|
+
if (!statusLine || statusLine.type !== 'command') {
|
|
4614
|
+
return undefined
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
// Use provided signal or create a default one
|
|
4618
|
+
const abortSignal = signal || AbortSignal.timeout(timeoutMs)
|
|
4619
|
+
|
|
4620
|
+
try {
|
|
4621
|
+
// Convert status input to JSON
|
|
4622
|
+
const jsonInput = jsonStringify(statusLineInput)
|
|
4623
|
+
|
|
4624
|
+
const result = await execCommandHook(
|
|
4625
|
+
statusLine,
|
|
4626
|
+
'StatusLine',
|
|
4627
|
+
'statusLine',
|
|
4628
|
+
jsonInput,
|
|
4629
|
+
abortSignal,
|
|
4630
|
+
randomUUID(),
|
|
4631
|
+
)
|
|
4632
|
+
|
|
4633
|
+
if (result.aborted) {
|
|
4634
|
+
return undefined
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
// For successful hooks (exit code 0), use stdout
|
|
4638
|
+
if (result.status === 0) {
|
|
4639
|
+
// Trim and split output into lines, then join with newlines
|
|
4640
|
+
const output = result.stdout
|
|
4641
|
+
.trim()
|
|
4642
|
+
.split('\n')
|
|
4643
|
+
.flatMap(line => line.trim() || [])
|
|
4644
|
+
.join('\n')
|
|
4645
|
+
|
|
4646
|
+
if (output) {
|
|
4647
|
+
if (logResult) {
|
|
4648
|
+
logForDebugging(
|
|
4649
|
+
`StatusLine [${statusLine.command}] completed with status ${result.status}`,
|
|
4650
|
+
)
|
|
4651
|
+
}
|
|
4652
|
+
return output
|
|
4653
|
+
}
|
|
4654
|
+
} else if (logResult) {
|
|
4655
|
+
logForDebugging(
|
|
4656
|
+
`StatusLine [${statusLine.command}] completed with status ${result.status}`,
|
|
4657
|
+
{ level: 'warn' },
|
|
4658
|
+
)
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
return undefined
|
|
4662
|
+
} catch (error) {
|
|
4663
|
+
logForDebugging(`Status hook failed: ${error}`, { level: 'error' })
|
|
4664
|
+
return undefined
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
/**
|
|
4669
|
+
* Execute file suggestion command if configured
|
|
4670
|
+
* @param fileSuggestionInput The structured input that will be converted to JSON
|
|
4671
|
+
* @param signal Optional AbortSignal to cancel hook execution
|
|
4672
|
+
* @param timeoutMs Optional timeout in milliseconds for hook execution
|
|
4673
|
+
* @returns Array of file paths, or empty array if no command configured
|
|
4674
|
+
*/
|
|
4675
|
+
export async function executeFileSuggestionCommand(
|
|
4676
|
+
fileSuggestionInput: FileSuggestionCommandInput,
|
|
4677
|
+
signal?: AbortSignal,
|
|
4678
|
+
timeoutMs: number = 5000, // Short timeout for typeahead suggestions
|
|
4679
|
+
): Promise<string[]> {
|
|
4680
|
+
// Check if all hooks are disabled by managed settings
|
|
4681
|
+
if (shouldDisableAllHooksIncludingManaged()) {
|
|
4682
|
+
return []
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
// SECURITY: ALL hooks require workspace trust in interactive mode
|
|
4686
|
+
// This centralized check prevents RCE vulnerabilities for all current and future hooks
|
|
4687
|
+
if (shouldSkipHookDueToTrust()) {
|
|
4688
|
+
logForDebugging(
|
|
4689
|
+
`Skipping FileSuggestion command execution - workspace trust not accepted`,
|
|
4690
|
+
)
|
|
4691
|
+
return []
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
// When disableAllHooks is set in non-managed settings, only managed fileSuggestion runs
|
|
4695
|
+
// (non-managed settings cannot disable managed commands, but non-managed commands are disabled)
|
|
4696
|
+
let fileSuggestion
|
|
4697
|
+
if (shouldAllowManagedHooksOnly()) {
|
|
4698
|
+
fileSuggestion = getSettingsForSource('policySettings')?.fileSuggestion
|
|
4699
|
+
} else {
|
|
4700
|
+
fileSuggestion = getSettings_DEPRECATED()?.fileSuggestion
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
if (!fileSuggestion || fileSuggestion.type !== 'command') {
|
|
4704
|
+
return []
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
// Use provided signal or create a default one
|
|
4708
|
+
const abortSignal = signal || AbortSignal.timeout(timeoutMs)
|
|
4709
|
+
|
|
4710
|
+
try {
|
|
4711
|
+
const jsonInput = jsonStringify(fileSuggestionInput)
|
|
4712
|
+
|
|
4713
|
+
const hook = { type: 'command' as const, command: fileSuggestion.command }
|
|
4714
|
+
|
|
4715
|
+
const result = await execCommandHook(
|
|
4716
|
+
hook,
|
|
4717
|
+
'FileSuggestion',
|
|
4718
|
+
'FileSuggestion',
|
|
4719
|
+
jsonInput,
|
|
4720
|
+
abortSignal,
|
|
4721
|
+
randomUUID(),
|
|
4722
|
+
)
|
|
4723
|
+
|
|
4724
|
+
if (result.aborted || result.status !== 0) {
|
|
4725
|
+
return []
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
return result.stdout
|
|
4729
|
+
.split('\n')
|
|
4730
|
+
.map(line => line.trim())
|
|
4731
|
+
.filter(Boolean)
|
|
4732
|
+
} catch (error) {
|
|
4733
|
+
logForDebugging(`File suggestion helper failed: ${error}`, {
|
|
4734
|
+
level: 'error',
|
|
4735
|
+
})
|
|
4736
|
+
return []
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
async function executeFunctionHook({
|
|
4741
|
+
hook,
|
|
4742
|
+
messages,
|
|
4743
|
+
hookName,
|
|
4744
|
+
toolUseID,
|
|
4745
|
+
hookEvent,
|
|
4746
|
+
timeoutMs,
|
|
4747
|
+
signal,
|
|
4748
|
+
}: {
|
|
4749
|
+
hook: FunctionHook
|
|
4750
|
+
messages: Message[]
|
|
4751
|
+
hookName: string
|
|
4752
|
+
toolUseID: string
|
|
4753
|
+
hookEvent: HookEvent
|
|
4754
|
+
timeoutMs: number
|
|
4755
|
+
signal?: AbortSignal
|
|
4756
|
+
}): Promise<HookResult> {
|
|
4757
|
+
const callbackTimeoutMs = hook.timeout ?? timeoutMs
|
|
4758
|
+
const { signal: abortSignal, cleanup } = createCombinedAbortSignal(signal, {
|
|
4759
|
+
timeoutMs: callbackTimeoutMs,
|
|
4760
|
+
})
|
|
4761
|
+
|
|
4762
|
+
try {
|
|
4763
|
+
// Check if already aborted
|
|
4764
|
+
if (abortSignal.aborted) {
|
|
4765
|
+
cleanup()
|
|
4766
|
+
return {
|
|
4767
|
+
outcome: 'cancelled',
|
|
4768
|
+
hook,
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
// Execute callback with abort signal
|
|
4773
|
+
const passed = await new Promise<boolean>((resolve, reject) => {
|
|
4774
|
+
// Handle abort signal
|
|
4775
|
+
const onAbort = () => reject(new Error('Function hook cancelled'))
|
|
4776
|
+
abortSignal.addEventListener('abort', onAbort)
|
|
4777
|
+
|
|
4778
|
+
// Execute callback
|
|
4779
|
+
Promise.resolve(hook.callback(messages, abortSignal))
|
|
4780
|
+
.then(result => {
|
|
4781
|
+
abortSignal.removeEventListener('abort', onAbort)
|
|
4782
|
+
resolve(result)
|
|
4783
|
+
})
|
|
4784
|
+
.catch(error => {
|
|
4785
|
+
abortSignal.removeEventListener('abort', onAbort)
|
|
4786
|
+
reject(error)
|
|
4787
|
+
})
|
|
4788
|
+
})
|
|
4789
|
+
|
|
4790
|
+
cleanup()
|
|
4791
|
+
|
|
4792
|
+
if (passed) {
|
|
4793
|
+
return {
|
|
4794
|
+
outcome: 'success',
|
|
4795
|
+
hook,
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
return {
|
|
4799
|
+
blockingError: {
|
|
4800
|
+
blockingError: hook.errorMessage,
|
|
4801
|
+
command: 'function',
|
|
4802
|
+
},
|
|
4803
|
+
outcome: 'blocking',
|
|
4804
|
+
hook,
|
|
4805
|
+
}
|
|
4806
|
+
} catch (error) {
|
|
4807
|
+
cleanup()
|
|
4808
|
+
|
|
4809
|
+
// Handle cancellation
|
|
4810
|
+
if (
|
|
4811
|
+
error instanceof Error &&
|
|
4812
|
+
(error.message === 'Function hook cancelled' ||
|
|
4813
|
+
error.name === 'AbortError')
|
|
4814
|
+
) {
|
|
4815
|
+
return {
|
|
4816
|
+
outcome: 'cancelled',
|
|
4817
|
+
hook,
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
// Log for monitoring
|
|
4822
|
+
logError(error)
|
|
4823
|
+
return {
|
|
4824
|
+
message: createAttachmentMessage({
|
|
4825
|
+
type: 'hook_error_during_execution',
|
|
4826
|
+
hookName,
|
|
4827
|
+
toolUseID,
|
|
4828
|
+
hookEvent,
|
|
4829
|
+
content:
|
|
4830
|
+
error instanceof Error
|
|
4831
|
+
? error.message
|
|
4832
|
+
: 'Function hook execution error',
|
|
4833
|
+
}),
|
|
4834
|
+
outcome: 'non_blocking_error',
|
|
4835
|
+
hook,
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
async function executeHookCallback({
|
|
4841
|
+
toolUseID,
|
|
4842
|
+
hook,
|
|
4843
|
+
hookEvent,
|
|
4844
|
+
hookInput,
|
|
4845
|
+
signal,
|
|
4846
|
+
hookIndex,
|
|
4847
|
+
toolUseContext,
|
|
4848
|
+
}: {
|
|
4849
|
+
toolUseID: string
|
|
4850
|
+
hook: HookCallback
|
|
4851
|
+
hookEvent: HookEvent
|
|
4852
|
+
hookInput: HookInput
|
|
4853
|
+
signal: AbortSignal
|
|
4854
|
+
hookIndex?: number
|
|
4855
|
+
toolUseContext?: ToolUseContext
|
|
4856
|
+
}): Promise<HookResult> {
|
|
4857
|
+
// Create context for callbacks that need state access
|
|
4858
|
+
const context = toolUseContext
|
|
4859
|
+
? {
|
|
4860
|
+
getAppState: toolUseContext.getAppState,
|
|
4861
|
+
updateAttributionState: toolUseContext.updateAttributionState,
|
|
4862
|
+
}
|
|
4863
|
+
: undefined
|
|
4864
|
+
const json = await hook.callback(
|
|
4865
|
+
hookInput,
|
|
4866
|
+
toolUseID,
|
|
4867
|
+
signal,
|
|
4868
|
+
hookIndex,
|
|
4869
|
+
context,
|
|
4870
|
+
)
|
|
4871
|
+
if (isAsyncHookJSONOutput(json)) {
|
|
4872
|
+
return {
|
|
4873
|
+
outcome: 'success',
|
|
4874
|
+
hook,
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
const processed = processHookJSONOutput({
|
|
4879
|
+
json,
|
|
4880
|
+
command: 'callback',
|
|
4881
|
+
// TODO: If the hook came from a plugin, use the full path to the plugin for easier debugging
|
|
4882
|
+
hookName: `${hookEvent}:Callback`,
|
|
4883
|
+
toolUseID,
|
|
4884
|
+
hookEvent,
|
|
4885
|
+
expectedHookEvent: hookEvent,
|
|
4886
|
+
// Callbacks don't have stdout/stderr/exitCode
|
|
4887
|
+
stdout: undefined,
|
|
4888
|
+
stderr: undefined,
|
|
4889
|
+
exitCode: undefined,
|
|
4890
|
+
})
|
|
4891
|
+
return {
|
|
4892
|
+
...processed,
|
|
4893
|
+
outcome: 'success',
|
|
4894
|
+
hook,
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4898
|
+
/**
|
|
4899
|
+
* Check if WorktreeCreate hooks are configured (without executing them).
|
|
4900
|
+
*
|
|
4901
|
+
* Checks both settings-file hooks (getHooksConfigFromSnapshot) and registered
|
|
4902
|
+
* hooks (plugin hooks + SDK callback hooks via registerHookCallbacks).
|
|
4903
|
+
*
|
|
4904
|
+
* Must mirror the managedOnly filtering in getHooksConfig() — when
|
|
4905
|
+
* shouldAllowManagedHooksOnly() is true, plugin hooks (pluginRoot set) are
|
|
4906
|
+
* skipped at execution, so we must also skip them here. Otherwise this returns
|
|
4907
|
+
* true but executeWorktreeCreateHook() finds no matching hooks and throws,
|
|
4908
|
+
* blocking the git-worktree fallback.
|
|
4909
|
+
*/
|
|
4910
|
+
export function hasWorktreeCreateHook(): boolean {
|
|
4911
|
+
const snapshotHooks = getHooksConfigFromSnapshot()?.['WorktreeCreate']
|
|
4912
|
+
if (snapshotHooks && snapshotHooks.length > 0) return true
|
|
4913
|
+
const registeredHooks = getRegisteredHooks()?.['WorktreeCreate']
|
|
4914
|
+
if (!registeredHooks || registeredHooks.length === 0) return false
|
|
4915
|
+
// Mirror getHooksConfig(): skip plugin hooks in managed-only mode
|
|
4916
|
+
const managedOnly = shouldAllowManagedHooksOnly()
|
|
4917
|
+
return registeredHooks.some(
|
|
4918
|
+
matcher => !(managedOnly && 'pluginRoot' in matcher),
|
|
4919
|
+
)
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
/**
|
|
4923
|
+
* Execute WorktreeCreate hooks.
|
|
4924
|
+
* Returns the worktree path from hook stdout.
|
|
4925
|
+
* Throws if hooks fail or produce no output.
|
|
4926
|
+
* Callers should check hasWorktreeCreateHook() before calling this.
|
|
4927
|
+
*/
|
|
4928
|
+
export async function executeWorktreeCreateHook(
|
|
4929
|
+
name: string,
|
|
4930
|
+
): Promise<{ worktreePath: string }> {
|
|
4931
|
+
const hookInput = {
|
|
4932
|
+
...createBaseHookInput(undefined),
|
|
4933
|
+
hook_event_name: 'WorktreeCreate' as const,
|
|
4934
|
+
name,
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
const results = await executeHooksOutsideREPL({
|
|
4938
|
+
hookInput,
|
|
4939
|
+
timeoutMs: TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4940
|
+
})
|
|
4941
|
+
|
|
4942
|
+
// Find the first successful result with non-empty output
|
|
4943
|
+
const successfulResult = results.find(
|
|
4944
|
+
r => r.succeeded && r.output.trim().length > 0,
|
|
4945
|
+
)
|
|
4946
|
+
|
|
4947
|
+
if (!successfulResult) {
|
|
4948
|
+
const failedOutputs = results
|
|
4949
|
+
.filter(r => !r.succeeded)
|
|
4950
|
+
.map(r => `${r.command}: ${r.output.trim() || 'no output'}`)
|
|
4951
|
+
throw new Error(
|
|
4952
|
+
`WorktreeCreate hook failed: ${failedOutputs.join('; ') || 'no successful output'}`,
|
|
4953
|
+
)
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
const worktreePath = successfulResult.output.trim()
|
|
4957
|
+
return { worktreePath }
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4960
|
+
/**
|
|
4961
|
+
* Execute WorktreeRemove hooks if configured.
|
|
4962
|
+
* Returns true if hooks were configured and ran, false if no hooks are configured.
|
|
4963
|
+
*
|
|
4964
|
+
* Checks both settings-file hooks (getHooksConfigFromSnapshot) and registered
|
|
4965
|
+
* hooks (plugin hooks + SDK callback hooks via registerHookCallbacks).
|
|
4966
|
+
*/
|
|
4967
|
+
export async function executeWorktreeRemoveHook(
|
|
4968
|
+
worktreePath: string,
|
|
4969
|
+
): Promise<boolean> {
|
|
4970
|
+
const snapshotHooks = getHooksConfigFromSnapshot()?.['WorktreeRemove']
|
|
4971
|
+
const registeredHooks = getRegisteredHooks()?.['WorktreeRemove']
|
|
4972
|
+
const hasSnapshotHooks = snapshotHooks && snapshotHooks.length > 0
|
|
4973
|
+
const hasRegisteredHooks = registeredHooks && registeredHooks.length > 0
|
|
4974
|
+
if (!hasSnapshotHooks && !hasRegisteredHooks) {
|
|
4975
|
+
return false
|
|
4976
|
+
}
|
|
4977
|
+
|
|
4978
|
+
const hookInput = {
|
|
4979
|
+
...createBaseHookInput(undefined),
|
|
4980
|
+
hook_event_name: 'WorktreeRemove' as const,
|
|
4981
|
+
worktree_path: worktreePath,
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4984
|
+
const results = await executeHooksOutsideREPL({
|
|
4985
|
+
hookInput,
|
|
4986
|
+
timeoutMs: TOOL_HOOK_EXECUTION_TIMEOUT_MS,
|
|
4987
|
+
})
|
|
4988
|
+
|
|
4989
|
+
if (results.length === 0) {
|
|
4990
|
+
return false
|
|
4991
|
+
}
|
|
4992
|
+
|
|
4993
|
+
for (const result of results) {
|
|
4994
|
+
if (!result.succeeded) {
|
|
4995
|
+
logForDebugging(
|
|
4996
|
+
`WorktreeRemove hook failed [${result.command}]: ${result.output.trim()}`,
|
|
4997
|
+
{ level: 'error' },
|
|
4998
|
+
)
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
return true
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
function getHookDefinitionsForTelemetry(
|
|
5006
|
+
matchedHooks: MatchedHook[],
|
|
5007
|
+
): Array<{ type: string; command?: string; prompt?: string; name?: string }> {
|
|
5008
|
+
return matchedHooks.map(({ hook }) => {
|
|
5009
|
+
if (hook.type === 'command') {
|
|
5010
|
+
return { type: 'command', command: hook.command }
|
|
5011
|
+
} else if (hook.type === 'prompt') {
|
|
5012
|
+
return { type: 'prompt', prompt: hook.prompt }
|
|
5013
|
+
} else if (hook.type === 'http') {
|
|
5014
|
+
return { type: 'http', command: hook.url }
|
|
5015
|
+
} else if (hook.type === 'function') {
|
|
5016
|
+
return { type: 'function', name: 'function' }
|
|
5017
|
+
} else if (hook.type === 'callback') {
|
|
5018
|
+
return { type: 'callback', name: 'callback' }
|
|
5019
|
+
}
|
|
5020
|
+
return { type: 'unknown' }
|
|
5021
|
+
})
|
|
5022
|
+
}
|