mixdog 0.8.0 → 0.9.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 +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { listRegisteredPlugins } from './plugin-admin.mjs';
|
|
3
5
|
|
|
4
6
|
const DEFAULT_EVENTS = Object.freeze([
|
|
5
7
|
'runtime:start',
|
|
@@ -9,11 +11,83 @@ const DEFAULT_EVENTS = Object.freeze([
|
|
|
9
11
|
'turn:error',
|
|
10
12
|
'tool:planned',
|
|
11
13
|
'tool:before',
|
|
14
|
+
'tool:ask',
|
|
12
15
|
'tool:deny',
|
|
13
16
|
'tool:modify',
|
|
14
17
|
'hook:error',
|
|
18
|
+
'SessionStart',
|
|
19
|
+
'UserPromptSubmit',
|
|
20
|
+
'PreToolUse',
|
|
21
|
+
'PostToolUse',
|
|
22
|
+
'Stop',
|
|
15
23
|
]);
|
|
16
24
|
|
|
25
|
+
const DEFAULT_COMMAND_TIMEOUT_S = 600;
|
|
26
|
+
const USER_PROMPT_TIMEOUT_S = 30;
|
|
27
|
+
const MESSAGE_DISPLAY_TIMEOUT_S = 10;
|
|
28
|
+
const DEFAULT_PROMPT_TIMEOUT_S = 30;
|
|
29
|
+
const DEFAULT_AGENT_TIMEOUT_S = 60;
|
|
30
|
+
const MAX_OUTPUT_CHARS = 10_000;
|
|
31
|
+
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
const TOOL_IF_EVENTS = new Set([
|
|
34
|
+
'PreToolUse',
|
|
35
|
+
'PostToolUse',
|
|
36
|
+
'PostToolUseFailure',
|
|
37
|
+
'PermissionRequest',
|
|
38
|
+
'PermissionDenied',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const NO_MATCHER_EVENTS = new Set([
|
|
42
|
+
'UserPromptSubmit',
|
|
43
|
+
'PostToolBatch',
|
|
44
|
+
'Stop',
|
|
45
|
+
'TeammateIdle',
|
|
46
|
+
'TaskCreated',
|
|
47
|
+
'TaskCompleted',
|
|
48
|
+
'WorktreeCreate',
|
|
49
|
+
'WorktreeRemove',
|
|
50
|
+
'CwdChanged',
|
|
51
|
+
'MessageDisplay',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const EXIT2_BLOCK_EVENTS = new Set([
|
|
55
|
+
'PreToolUse',
|
|
56
|
+
'PermissionRequest',
|
|
57
|
+
'UserPromptSubmit',
|
|
58
|
+
'UserPromptExpansion',
|
|
59
|
+
'Stop',
|
|
60
|
+
'SubagentStop',
|
|
61
|
+
'TeammateIdle',
|
|
62
|
+
'TaskCreated',
|
|
63
|
+
'TaskCompleted',
|
|
64
|
+
'ConfigChange',
|
|
65
|
+
'PostToolBatch',
|
|
66
|
+
'PreCompact',
|
|
67
|
+
'Elicitation',
|
|
68
|
+
'ElicitationResult',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const TOP_LEVEL_DECISION_EVENTS = new Set([
|
|
72
|
+
'UserPromptSubmit',
|
|
73
|
+
'UserPromptExpansion',
|
|
74
|
+
'PostToolUse',
|
|
75
|
+
'PostToolUseFailure',
|
|
76
|
+
'PostToolBatch',
|
|
77
|
+
'Stop',
|
|
78
|
+
'SubagentStop',
|
|
79
|
+
'ConfigChange',
|
|
80
|
+
'PreCompact',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const PLAIN_STDOUT_CONTEXT_EVENTS = new Set([
|
|
84
|
+
'SessionStart',
|
|
85
|
+
'UserPromptSubmit',
|
|
86
|
+
'UserPromptExpansion',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const SUPPORTED_HANDLER_TYPES = new Set(['command', 'http']);
|
|
90
|
+
|
|
17
91
|
function compactValue(value) {
|
|
18
92
|
if (value == null) return value;
|
|
19
93
|
if (typeof value === 'string') return value.length > 180 ? `${value.slice(0, 180)}...` : value;
|
|
@@ -36,8 +110,8 @@ function compactValue(value) {
|
|
|
36
110
|
function summarizePayload(payload = {}) {
|
|
37
111
|
if (!payload || typeof payload !== 'object') return String(payload ?? '');
|
|
38
112
|
const parts = [];
|
|
39
|
-
if (payload.sessionId) parts.push(`session=${payload.sessionId}`);
|
|
40
|
-
if (payload.name) parts.push(`name=${payload.name}`);
|
|
113
|
+
if (payload.sessionId || payload.session_id) parts.push(`session=${payload.sessionId || payload.session_id}`);
|
|
114
|
+
if (payload.name || payload.tool_name) parts.push(`name=${payload.name || payload.tool_name}`);
|
|
41
115
|
if (payload.provider || payload.model) parts.push([payload.provider, payload.model].filter(Boolean).join('/'));
|
|
42
116
|
if (payload.prompt) parts.push(`prompt=${String(payload.prompt).slice(0, 60).replace(/\s+/g, ' ')}`);
|
|
43
117
|
if (payload.reason) parts.push(`reason=${String(payload.reason).slice(0, 120)}`);
|
|
@@ -59,6 +133,543 @@ function normalizeRules(raw) {
|
|
|
59
133
|
return [];
|
|
60
134
|
}
|
|
61
135
|
|
|
136
|
+
function uniqueHookEntries(entries) {
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry?.path) continue;
|
|
141
|
+
const resolved = resolve(entry.path);
|
|
142
|
+
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
143
|
+
if (seen.has(key)) continue;
|
|
144
|
+
seen.add(key);
|
|
145
|
+
out.push({ ...entry, path: resolved });
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cleanHookId(value) {
|
|
151
|
+
return String(value ?? '').trim()
|
|
152
|
+
.toLowerCase()
|
|
153
|
+
.replace(/[^a-z0-9_.-]+/g, '-')
|
|
154
|
+
.replace(/^-+|-+$/g, '');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function pluginHookConfigEntries(dataDir) {
|
|
158
|
+
if (!dataDir) return [];
|
|
159
|
+
let plugins = [];
|
|
160
|
+
try {
|
|
161
|
+
plugins = listRegisteredPlugins({ dataDir });
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const entries = [];
|
|
166
|
+
for (const plugin of plugins || []) {
|
|
167
|
+
const root = String(plugin?.root || '').trim();
|
|
168
|
+
if (!root || !existsSync(root)) continue;
|
|
169
|
+
const id = cleanHookId(plugin.id || plugin.name || plugin.title || root.split(/[\\/]/).pop());
|
|
170
|
+
const pluginData = join(dataDir, 'plugins', 'data', id || 'plugin');
|
|
171
|
+
entries.push({
|
|
172
|
+
path: join(root, 'hooks', 'hooks.json'),
|
|
173
|
+
pluginRoot: root,
|
|
174
|
+
pluginData,
|
|
175
|
+
sourceType: 'plugin',
|
|
176
|
+
});
|
|
177
|
+
entries.push({
|
|
178
|
+
path: join(root, '.mixdog', 'hooks.json'),
|
|
179
|
+
pluginRoot: root,
|
|
180
|
+
pluginData,
|
|
181
|
+
sourceType: 'plugin',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return entries;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function hookConfigEntries(dataDir, cwd) {
|
|
188
|
+
if (process.env.MIXDOG_HOOKS_FILE) {
|
|
189
|
+
return uniqueHookEntries([{ path: process.env.MIXDOG_HOOKS_FILE, sourceType: 'env' }]);
|
|
190
|
+
}
|
|
191
|
+
const projectDir = cwd ? resolve(cwd) : process.cwd();
|
|
192
|
+
return uniqueHookEntries([
|
|
193
|
+
projectDir ? { path: join(projectDir, '.mixdog', 'hooks.json'), sourceType: 'project' } : null,
|
|
194
|
+
projectDir ? { path: join(projectDir, '.mixdog', 'hooks', 'hooks.json'), sourceType: 'project' } : null,
|
|
195
|
+
dataDir ? { path: join(dataDir, 'hooks.json'), sourceType: 'data' } : null,
|
|
196
|
+
dataDir ? { path: join(dataDir, 'hooks', 'hooks.json'), sourceType: 'data' } : null,
|
|
197
|
+
...pluginHookConfigEntries(dataDir),
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isStandardConfig(parsed) {
|
|
202
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
|
|
203
|
+
const hooks = parsed.hooks;
|
|
204
|
+
if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return false;
|
|
205
|
+
const values = Object.values(hooks);
|
|
206
|
+
if (values.length === 0) return false;
|
|
207
|
+
return values.every((groups) => (
|
|
208
|
+
Array.isArray(groups)
|
|
209
|
+
&& groups.every((g) => g && typeof g === 'object' && Array.isArray(g.hooks))
|
|
210
|
+
));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const SIMPLE_MATCHER_RE = /^[A-Za-z0-9_ ,|]*$/;
|
|
214
|
+
function matcherFires(matcher, field) {
|
|
215
|
+
if (matcher == null) return true;
|
|
216
|
+
const text = String(matcher).trim();
|
|
217
|
+
if (text === '' || text === '*') return true;
|
|
218
|
+
const value = String(field ?? '');
|
|
219
|
+
if (SIMPLE_MATCHER_RE.test(text)) {
|
|
220
|
+
const items = text.split(/[|,]/).map((s) => s.trim()).filter(Boolean);
|
|
221
|
+
if (items.length === 0) return true;
|
|
222
|
+
return items.includes(value);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return new RegExp(text).test(value);
|
|
226
|
+
} catch {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function globToRegExp(glob) {
|
|
232
|
+
let out = '^';
|
|
233
|
+
for (const ch of String(glob || '')) {
|
|
234
|
+
if (ch === '*') out += '.*';
|
|
235
|
+
else if (ch === '?') out += '.';
|
|
236
|
+
else out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
237
|
+
}
|
|
238
|
+
return new RegExp(`${out}$`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function primaryArgFor(toolName, toolInput) {
|
|
242
|
+
const input = toolInput && typeof toolInput === 'object' ? toolInput : {};
|
|
243
|
+
if (toolName === 'Bash' || toolName === 'bash' || toolName === 'shell') return input.command ?? '';
|
|
244
|
+
if (input.file_path != null) return input.file_path;
|
|
245
|
+
if (input.path != null) return input.path;
|
|
246
|
+
if (input.command != null) return input.command;
|
|
247
|
+
if (input.file != null) return input.file;
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function ifConditionPasses(ifExpr, eventName, toolName, toolInput) {
|
|
252
|
+
if (ifExpr == null || String(ifExpr).trim() === '') return true;
|
|
253
|
+
if (!TOOL_IF_EVENTS.has(eventName)) return false;
|
|
254
|
+
const m = /^\s*([A-Za-z0-9_*]+)\s*\(([^)]*)\)\s*$/.exec(String(ifExpr));
|
|
255
|
+
if (!m) return true;
|
|
256
|
+
const [, tool, pattern] = m;
|
|
257
|
+
if (tool !== '*' && tool !== toolName) return false;
|
|
258
|
+
try {
|
|
259
|
+
return globToRegExp(pattern.trim()).test(String(primaryArgFor(toolName, toolInput)));
|
|
260
|
+
} catch {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function resolvePlaceholders(str, projectDir, pluginData, pluginRoot = null) {
|
|
266
|
+
if (typeof str !== 'string') return str;
|
|
267
|
+
const resolvedProject = projectDir || process.cwd();
|
|
268
|
+
const resolvedPluginRoot = pluginRoot || resolvedProject;
|
|
269
|
+
const resolvedPluginData = pluginData || resolvedProject;
|
|
270
|
+
return str
|
|
271
|
+
.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, resolvedProject)
|
|
272
|
+
.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, resolvedPluginRoot)
|
|
273
|
+
.replace(/\$\{CLAUDE_PLUGIN_DATA\}/g, resolvedPluginData)
|
|
274
|
+
.replace(/\$\{MIXDOG_PROJECT_DIR\}/g, resolvedProject)
|
|
275
|
+
.replace(/\$\{MIXDOG_PLUGIN_ROOT\}/g, resolvedPluginRoot)
|
|
276
|
+
.replace(/\$\{MIXDOG_PLUGIN_DATA\}/g, resolvedPluginData);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseStandardConfig(parsed, source, meta = {}) {
|
|
280
|
+
const events = {};
|
|
281
|
+
const hooks = parsed.hooks || {};
|
|
282
|
+
for (const [eventName, groups] of Object.entries(hooks)) {
|
|
283
|
+
if (!Array.isArray(groups)) continue;
|
|
284
|
+
const cleanGroups = [];
|
|
285
|
+
for (const group of groups) {
|
|
286
|
+
if (!group || typeof group !== 'object') continue;
|
|
287
|
+
const handlers = Array.isArray(group.hooks)
|
|
288
|
+
? group.hooks
|
|
289
|
+
.filter((h) => h && typeof h === 'object' && h.type)
|
|
290
|
+
.map((h) => ({
|
|
291
|
+
...h,
|
|
292
|
+
_source: source,
|
|
293
|
+
...(meta.pluginRoot ? { _pluginRoot: meta.pluginRoot } : {}),
|
|
294
|
+
...(meta.pluginData ? { _pluginData: meta.pluginData } : {}),
|
|
295
|
+
}))
|
|
296
|
+
: [];
|
|
297
|
+
if (handlers.length === 0) continue;
|
|
298
|
+
cleanGroups.push({ matcher: group.matcher, hooks: handlers, _source: source });
|
|
299
|
+
}
|
|
300
|
+
if (cleanGroups.length > 0) events[eventName] = cleanGroups;
|
|
301
|
+
}
|
|
302
|
+
return events;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function mergeEvents(target, source) {
|
|
306
|
+
for (const [eventName, groups] of Object.entries(source || {})) {
|
|
307
|
+
if (!Array.isArray(target[eventName])) target[eventName] = [];
|
|
308
|
+
target[eventName].push(...groups);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildEventPayload(eventName, input = {}) {
|
|
313
|
+
const payload = {
|
|
314
|
+
session_id: input.sessionId ?? input.session_id ?? null,
|
|
315
|
+
cwd: input.cwd ?? null,
|
|
316
|
+
hook_event_name: eventName,
|
|
317
|
+
};
|
|
318
|
+
if (input.transcriptPath != null || input.transcript_path != null) {
|
|
319
|
+
payload.transcript_path = input.transcriptPath ?? input.transcript_path;
|
|
320
|
+
}
|
|
321
|
+
if (input.permissionMode != null || input.permission_mode != null) {
|
|
322
|
+
payload.permission_mode = input.permissionMode ?? input.permission_mode;
|
|
323
|
+
}
|
|
324
|
+
if (input.effort != null) payload.effort = input.effort;
|
|
325
|
+
const toolName = input.name ?? input.tool_name;
|
|
326
|
+
if (toolName != null) payload.tool_name = toolName;
|
|
327
|
+
const toolInput = input.args ?? input.tool_input;
|
|
328
|
+
if (toolInput != null) payload.tool_input = toolInput;
|
|
329
|
+
if (input.toolCallId != null || input.tool_use_id != null) {
|
|
330
|
+
payload.tool_use_id = input.toolCallId ?? input.tool_use_id;
|
|
331
|
+
}
|
|
332
|
+
if (input.toolResponse != null || input.tool_response != null || input.result != null) {
|
|
333
|
+
payload.tool_response = input.toolResponse ?? input.tool_response ?? input.result;
|
|
334
|
+
}
|
|
335
|
+
for (const key of [
|
|
336
|
+
'prompt',
|
|
337
|
+
'source',
|
|
338
|
+
'model',
|
|
339
|
+
'session_title',
|
|
340
|
+
'message',
|
|
341
|
+
'notification_type',
|
|
342
|
+
'trigger',
|
|
343
|
+
'load_reason',
|
|
344
|
+
'file_path',
|
|
345
|
+
'memory_type',
|
|
346
|
+
'command_name',
|
|
347
|
+
'command_args',
|
|
348
|
+
'command_source',
|
|
349
|
+
'expansion_type',
|
|
350
|
+
'stop_reason',
|
|
351
|
+
'error_type',
|
|
352
|
+
]) {
|
|
353
|
+
if (input[key] != null) payload[key] = input[key];
|
|
354
|
+
}
|
|
355
|
+
return payload;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function matchFieldFor(eventName, payload) {
|
|
359
|
+
if (NO_MATCHER_EVENTS.has(eventName)) return null;
|
|
360
|
+
if (eventName === 'SessionStart') return payload.source ?? '';
|
|
361
|
+
if (eventName === 'Setup') return payload.trigger ?? '';
|
|
362
|
+
if (eventName === 'SessionEnd') return payload.reason ?? '';
|
|
363
|
+
if (eventName === 'Notification') return payload.notification_type ?? payload.type ?? '';
|
|
364
|
+
if (eventName === 'SubagentStart' || eventName === 'SubagentStop') return payload.agent_type ?? '';
|
|
365
|
+
if (eventName === 'PreCompact' || eventName === 'PostCompact') return payload.trigger ?? '';
|
|
366
|
+
if (eventName === 'ConfigChange') return payload.source ?? '';
|
|
367
|
+
if (eventName === 'InstructionsLoaded') return payload.load_reason ?? '';
|
|
368
|
+
if (eventName === 'UserPromptExpansion') return payload.command_name ?? '';
|
|
369
|
+
if (eventName === 'StopFailure') return payload.error_type ?? '';
|
|
370
|
+
return payload.tool_name ?? '';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function handlerTimeoutS(handler, eventName) {
|
|
374
|
+
if (Number.isFinite(handler.timeout) && handler.timeout > 0) return handler.timeout;
|
|
375
|
+
if (handler.type === 'prompt') return DEFAULT_PROMPT_TIMEOUT_S;
|
|
376
|
+
if (handler.type === 'agent') return DEFAULT_AGENT_TIMEOUT_S;
|
|
377
|
+
if (eventName === 'UserPromptSubmit') return USER_PROMPT_TIMEOUT_S;
|
|
378
|
+
if (eventName === 'MessageDisplay') return MESSAGE_DISPLAY_TIMEOUT_S;
|
|
379
|
+
return DEFAULT_COMMAND_TIMEOUT_S;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function defaultShellKind() {
|
|
383
|
+
return process.platform === 'win32' ? 'powershell' : 'bash';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function commandSpawnSpec(handler, projectDir, pluginData) {
|
|
387
|
+
const command = resolvePlaceholders(handler.command, projectDir, pluginData, handler._pluginRoot || null);
|
|
388
|
+
if (Array.isArray(handler.args)) {
|
|
389
|
+
return {
|
|
390
|
+
command,
|
|
391
|
+
args: handler.args.map((a) => resolvePlaceholders(String(a), projectDir, pluginData, handler._pluginRoot || null)),
|
|
392
|
+
shellKind: 'exec',
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const shellKind = handler.shell === 'powershell' || handler.shell === 'bash'
|
|
396
|
+
? handler.shell
|
|
397
|
+
: defaultShellKind();
|
|
398
|
+
if (shellKind === 'powershell') {
|
|
399
|
+
return {
|
|
400
|
+
command: process.platform === 'win32' ? 'powershell.exe' : 'pwsh',
|
|
401
|
+
args: ['-NoProfile', '-NonInteractive', '-Command', command],
|
|
402
|
+
shellKind,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
command: process.platform === 'win32' ? 'bash.exe' : 'bash',
|
|
407
|
+
args: ['-lc', command],
|
|
408
|
+
shellKind,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function hookEnv(projectDir, pluginData, payload, pluginRoot = null) {
|
|
413
|
+
const resolvedProject = projectDir || process.cwd();
|
|
414
|
+
const resolvedPluginRoot = pluginRoot || resolvedProject;
|
|
415
|
+
const resolvedPluginData = pluginData || resolvedProject;
|
|
416
|
+
const env = {
|
|
417
|
+
...process.env,
|
|
418
|
+
MIXDOG_PROJECT_DIR: resolvedProject,
|
|
419
|
+
MIXDOG_PLUGIN_ROOT: resolvedPluginRoot,
|
|
420
|
+
MIXDOG_PLUGIN_DATA: resolvedPluginData,
|
|
421
|
+
CLAUDE_PROJECT_DIR: resolvedProject,
|
|
422
|
+
CLAUDE_PLUGIN_ROOT: resolvedPluginRoot,
|
|
423
|
+
CLAUDE_PLUGIN_DATA: resolvedPluginData,
|
|
424
|
+
};
|
|
425
|
+
const effortLevel = payload?.effort?.level || payload?.effort;
|
|
426
|
+
if (effortLevel) env.CLAUDE_EFFORT = String(effortLevel);
|
|
427
|
+
return env;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function runCommandHandler(handler, payload, eventName, pluginData, onSpawnError = null) {
|
|
431
|
+
const projectDir = payload.cwd || process.cwd();
|
|
432
|
+
const effectivePluginData = handler._pluginData || pluginData || null;
|
|
433
|
+
const stdin = JSON.stringify(payload);
|
|
434
|
+
const timeoutMs = Math.round(handlerTimeoutS(handler, eventName) * 1000);
|
|
435
|
+
const spec = commandSpawnSpec(handler, projectDir, effectivePluginData);
|
|
436
|
+
const baseOpts = {
|
|
437
|
+
cwd: existsSync(projectDir) ? projectDir : undefined,
|
|
438
|
+
env: hookEnv(projectDir, effectivePluginData, payload, handler._pluginRoot || null),
|
|
439
|
+
windowsHide: true,
|
|
440
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
if (handler.async === true || handler.asyncRewake === true) {
|
|
444
|
+
try {
|
|
445
|
+
const child = spawn(spec.command, spec.args, {
|
|
446
|
+
...baseOpts,
|
|
447
|
+
detached: true,
|
|
448
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
449
|
+
});
|
|
450
|
+
child.on('error', (error) => {
|
|
451
|
+
if (typeof onSpawnError === 'function') onSpawnError(error);
|
|
452
|
+
});
|
|
453
|
+
child.stdin?.end(stdin);
|
|
454
|
+
child.unref?.();
|
|
455
|
+
return Promise.resolve({ exitCode: 0, stdout: '', stderr: '', async: true });
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return Promise.resolve({
|
|
458
|
+
exitCode: -1,
|
|
459
|
+
stdout: '',
|
|
460
|
+
stderr: error?.message || String(error),
|
|
461
|
+
timedOut: false,
|
|
462
|
+
spawnError: error,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return new Promise((resolveRun) => {
|
|
468
|
+
let child;
|
|
469
|
+
let settled = false;
|
|
470
|
+
let stdout = '';
|
|
471
|
+
let stderr = '';
|
|
472
|
+
let stdoutBytes = 0;
|
|
473
|
+
let stderrBytes = 0;
|
|
474
|
+
let timedOut = false;
|
|
475
|
+
let timer = null;
|
|
476
|
+
const finish = (result) => {
|
|
477
|
+
if (settled) return;
|
|
478
|
+
settled = true;
|
|
479
|
+
if (timer) clearTimeout(timer);
|
|
480
|
+
resolveRun(result);
|
|
481
|
+
};
|
|
482
|
+
try {
|
|
483
|
+
child = spawn(spec.command, spec.args, baseOpts);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
finish({
|
|
486
|
+
exitCode: -1,
|
|
487
|
+
stdout: '',
|
|
488
|
+
stderr: error?.message || String(error),
|
|
489
|
+
timedOut: false,
|
|
490
|
+
spawnError: error,
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
timer = setTimeout(() => {
|
|
495
|
+
timedOut = true;
|
|
496
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
497
|
+
finish({
|
|
498
|
+
exitCode: -1,
|
|
499
|
+
stdout,
|
|
500
|
+
stderr: stderr || `hook command timed out after ${timeoutMs}ms`,
|
|
501
|
+
timedOut,
|
|
502
|
+
spawnError: null,
|
|
503
|
+
});
|
|
504
|
+
}, timeoutMs);
|
|
505
|
+
timer.unref?.();
|
|
506
|
+
child.stdout?.on('data', (chunk) => {
|
|
507
|
+
stdoutBytes += chunk.length;
|
|
508
|
+
if (stdoutBytes <= MAX_BUFFER_BYTES) stdout += chunk.toString('utf8');
|
|
509
|
+
});
|
|
510
|
+
child.stderr?.on('data', (chunk) => {
|
|
511
|
+
stderrBytes += chunk.length;
|
|
512
|
+
if (stderrBytes <= MAX_BUFFER_BYTES) stderr += chunk.toString('utf8');
|
|
513
|
+
});
|
|
514
|
+
child.on('error', (error) => {
|
|
515
|
+
finish({
|
|
516
|
+
exitCode: -1,
|
|
517
|
+
stdout,
|
|
518
|
+
stderr: stderr || error?.message || String(error),
|
|
519
|
+
timedOut: false,
|
|
520
|
+
spawnError: error,
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
child.on('close', (code) => {
|
|
524
|
+
finish({
|
|
525
|
+
exitCode: timedOut ? -1 : (typeof code === 'number' ? code : 0),
|
|
526
|
+
stdout,
|
|
527
|
+
stderr,
|
|
528
|
+
timedOut,
|
|
529
|
+
spawnError: null,
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
try {
|
|
533
|
+
child.stdin?.end(stdin);
|
|
534
|
+
} catch {}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function resolveHeaderValue(value, allowed) {
|
|
539
|
+
if (typeof value !== 'string') return String(value ?? '');
|
|
540
|
+
return value.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (_m, braced, bare) => {
|
|
541
|
+
const name = braced || bare;
|
|
542
|
+
return allowed.has(name) ? (process.env[name] || '') : '';
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function runHttpHandler(handler, payload, eventName) {
|
|
547
|
+
if (typeof fetch !== 'function') {
|
|
548
|
+
return { exitCode: -1, stdout: '', stderr: 'fetch is not available', timedOut: false, spawnError: new Error('fetch is not available') };
|
|
549
|
+
}
|
|
550
|
+
const timeoutMs = Math.round(handlerTimeoutS(handler, eventName) * 1000);
|
|
551
|
+
const controller = new AbortController();
|
|
552
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
553
|
+
timer.unref?.();
|
|
554
|
+
try {
|
|
555
|
+
const allowed = new Set(Array.isArray(handler.allowedEnvVars) ? handler.allowedEnvVars.map(String) : []);
|
|
556
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
557
|
+
if (handler.headers && typeof handler.headers === 'object' && !Array.isArray(handler.headers)) {
|
|
558
|
+
for (const [key, value] of Object.entries(handler.headers)) {
|
|
559
|
+
headers[key] = resolveHeaderValue(value, allowed);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const response = await fetch(handler.url, {
|
|
563
|
+
method: 'POST',
|
|
564
|
+
headers,
|
|
565
|
+
body: JSON.stringify(payload),
|
|
566
|
+
signal: controller.signal,
|
|
567
|
+
});
|
|
568
|
+
const text = await response.text();
|
|
569
|
+
if (!response.ok) {
|
|
570
|
+
return { exitCode: 1, stdout: text, stderr: `HTTP ${response.status} ${response.statusText}`.trim(), timedOut: false, spawnError: null };
|
|
571
|
+
}
|
|
572
|
+
return { exitCode: 0, stdout: text, stderr: '', timedOut: false, spawnError: null };
|
|
573
|
+
} catch (error) {
|
|
574
|
+
const aborted = error?.name === 'AbortError';
|
|
575
|
+
return {
|
|
576
|
+
exitCode: -1,
|
|
577
|
+
stdout: '',
|
|
578
|
+
stderr: aborted ? `HTTP hook timed out: ${handler.url}` : (error?.message || String(error)),
|
|
579
|
+
timedOut: aborted,
|
|
580
|
+
spawnError: aborted ? null : error,
|
|
581
|
+
};
|
|
582
|
+
} finally {
|
|
583
|
+
clearTimeout(timer);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function limitText(text) {
|
|
588
|
+
const value = String(text || '');
|
|
589
|
+
if (value.length <= MAX_OUTPUT_CHARS) return value;
|
|
590
|
+
return `${value.slice(0, MAX_OUTPUT_CHARS)}\n... [hook output truncated; original ${value.length} chars]`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function parseHandlerOutput(run, eventName) {
|
|
594
|
+
const out = {
|
|
595
|
+
block: false,
|
|
596
|
+
reason: null,
|
|
597
|
+
permissionDecision: null,
|
|
598
|
+
updatedInput: null,
|
|
599
|
+
updatedToolOutput: null,
|
|
600
|
+
additionalContext: null,
|
|
601
|
+
systemMessage: null,
|
|
602
|
+
suppressOutput: false,
|
|
603
|
+
continueFlag: undefined,
|
|
604
|
+
askReason: null,
|
|
605
|
+
};
|
|
606
|
+
if (run.timedOut || run.spawnError || run.async) return out;
|
|
607
|
+
if (run.exitCode === 2) {
|
|
608
|
+
if (EXIT2_BLOCK_EVENTS.has(eventName)) {
|
|
609
|
+
out.block = true;
|
|
610
|
+
out.reason = limitText((run.stderr || '').trim()) || `blocked by ${eventName} hook`;
|
|
611
|
+
}
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
if (run.exitCode !== 0) return out;
|
|
615
|
+
const rawText = (run.stdout || '').trim();
|
|
616
|
+
if (!rawText) return out;
|
|
617
|
+
if (!(rawText.startsWith('{') || rawText.startsWith('['))) {
|
|
618
|
+
if (PLAIN_STDOUT_CONTEXT_EVENTS.has(eventName)) out.additionalContext = limitText(rawText);
|
|
619
|
+
return out;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const json = JSON.parse(rawText);
|
|
623
|
+
if (!json || typeof json !== 'object' || Array.isArray(json)) return out;
|
|
624
|
+
if (json.continue === false) {
|
|
625
|
+
out.continueFlag = false;
|
|
626
|
+
out.block = true;
|
|
627
|
+
out.reason = limitText(json.stopReason || json.reason || `stopped by ${eventName} hook`);
|
|
628
|
+
}
|
|
629
|
+
if (json.decision === 'block' && TOP_LEVEL_DECISION_EVENTS.has(eventName)) {
|
|
630
|
+
out.block = true;
|
|
631
|
+
out.reason = limitText(json.reason || out.reason || `blocked by ${eventName} hook`);
|
|
632
|
+
}
|
|
633
|
+
if (json.suppressOutput) out.suppressOutput = true;
|
|
634
|
+
if (typeof json.systemMessage === 'string') out.systemMessage = limitText(json.systemMessage);
|
|
635
|
+
if (typeof json.additionalContext === 'string') out.additionalContext = limitText(json.additionalContext);
|
|
636
|
+
|
|
637
|
+
const hso = json.hookSpecificOutput;
|
|
638
|
+
const hsoMatches = hso && typeof hso === 'object'
|
|
639
|
+
&& (!hso.hookEventName || hso.hookEventName === eventName);
|
|
640
|
+
if (hsoMatches) {
|
|
641
|
+
if (typeof hso.additionalContext === 'string') out.additionalContext = limitText(hso.additionalContext);
|
|
642
|
+
if (hso.updatedInput && typeof hso.updatedInput === 'object' && !Array.isArray(hso.updatedInput)) {
|
|
643
|
+
out.updatedInput = hso.updatedInput;
|
|
644
|
+
}
|
|
645
|
+
if (hso.updatedToolOutput != null) out.updatedToolOutput = hso.updatedToolOutput;
|
|
646
|
+
if (hso.permissionDecision) out.permissionDecision = String(hso.permissionDecision).toLowerCase();
|
|
647
|
+
if (hso.permissionDecisionReason) out.reason = out.reason || limitText(hso.permissionDecisionReason);
|
|
648
|
+
if (eventName === 'PermissionRequest' && hso.decision && typeof hso.decision === 'object') {
|
|
649
|
+
const behavior = String(hso.decision.behavior || '').toLowerCase();
|
|
650
|
+
if (behavior === 'deny') {
|
|
651
|
+
out.block = true;
|
|
652
|
+
out.reason = out.reason || limitText(hso.decision.reason || `denied by ${eventName} hook`);
|
|
653
|
+
}
|
|
654
|
+
if (hso.decision.updatedInput && typeof hso.decision.updatedInput === 'object' && !Array.isArray(hso.decision.updatedInput)) {
|
|
655
|
+
out.updatedInput = hso.decision.updatedInput;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (eventName === 'PreToolUse') {
|
|
660
|
+
if (out.permissionDecision === 'deny') {
|
|
661
|
+
out.block = true;
|
|
662
|
+
out.reason = out.reason || `denied by ${eventName} hook`;
|
|
663
|
+
} else if (out.permissionDecision === 'ask') {
|
|
664
|
+
out.askReason = out.reason || `ask requested by ${eventName} hook`;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
if (PLAIN_STDOUT_CONTEXT_EVENTS.has(eventName)) out.additionalContext = limitText(rawText);
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
672
|
+
|
|
62
673
|
function matchesList(pattern, value) {
|
|
63
674
|
if (pattern == null || pattern === '*' || pattern === 'all') return true;
|
|
64
675
|
const items = Array.isArray(pattern) ? pattern : [pattern];
|
|
@@ -95,6 +706,9 @@ function decisionFromRule(rule, input) {
|
|
|
95
706
|
: { ...(input.args || {}), ...(rule.patch || {}) };
|
|
96
707
|
return { action: 'modify', args: nextArgs, reason: rule.reason || rule.message || `modified by hook rule for ${input.name}` };
|
|
97
708
|
}
|
|
709
|
+
if (action === 'ask') {
|
|
710
|
+
return { action: 'ask', reason: rule.reason || rule.message || `approval requested by hook rule for ${input.name}` };
|
|
711
|
+
}
|
|
98
712
|
return { action: 'allow', reason: rule.reason || rule.message || null };
|
|
99
713
|
}
|
|
100
714
|
|
|
@@ -111,15 +725,44 @@ function summarizeRule(rule, index) {
|
|
|
111
725
|
};
|
|
112
726
|
}
|
|
113
727
|
|
|
728
|
+
function handlerDedupeKey(handler) {
|
|
729
|
+
if (!handler || typeof handler !== 'object') return '';
|
|
730
|
+
if (handler.type === 'command') {
|
|
731
|
+
return `command:${handler._pluginRoot || ''}:${handler.command || ''}:${JSON.stringify(handler.args || null)}`;
|
|
732
|
+
}
|
|
733
|
+
if (handler.type === 'http') return `http:${handler.url || ''}`;
|
|
734
|
+
return `${handler.type || 'unknown'}:${JSON.stringify(handler)}`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function shellCountFor(handler) {
|
|
738
|
+
if (handler?.type === 'command') {
|
|
739
|
+
if (handler.args) return 'exec';
|
|
740
|
+
return handler.shell === 'powershell' || handler.shell === 'bash' ? handler.shell : defaultShellKind();
|
|
741
|
+
}
|
|
742
|
+
return handler?.type || 'unknown';
|
|
743
|
+
}
|
|
744
|
+
|
|
114
745
|
export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {}) {
|
|
115
746
|
const recent = [];
|
|
116
747
|
const counts = new Map(DEFAULT_EVENTS.map((name) => [name, 0]));
|
|
117
748
|
const rulesPath = hookRulesPath(dataDir);
|
|
749
|
+
const pluginData = dataDir || null;
|
|
118
750
|
let rulesCache = { mtimeMs: -1, rules: [] };
|
|
751
|
+
let configCache = {
|
|
752
|
+
key: '',
|
|
753
|
+
standard: false,
|
|
754
|
+
disabled: false,
|
|
755
|
+
events: {},
|
|
756
|
+
legacyRules: [],
|
|
757
|
+
sources: [],
|
|
758
|
+
errors: [],
|
|
759
|
+
};
|
|
760
|
+
let lastCwd = process.cwd();
|
|
119
761
|
|
|
120
762
|
function emit(name, payload = {}) {
|
|
121
763
|
const eventName = String(name || '').trim();
|
|
122
764
|
if (!eventName) return null;
|
|
765
|
+
if (payload?.cwd) lastCwd = payload.cwd;
|
|
123
766
|
counts.set(eventName, (counts.get(eventName) || 0) + 1);
|
|
124
767
|
const entry = {
|
|
125
768
|
ts: new Date().toISOString(),
|
|
@@ -147,6 +790,161 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
|
|
|
147
790
|
return rulesCache.rules;
|
|
148
791
|
}
|
|
149
792
|
|
|
793
|
+
function loadConfig(cwd = lastCwd) {
|
|
794
|
+
const entries = hookConfigEntries(dataDir, cwd);
|
|
795
|
+
const parts = [];
|
|
796
|
+
for (const { path: p } of entries) {
|
|
797
|
+
try {
|
|
798
|
+
const st = existsSync(p) ? statSync(p) : null;
|
|
799
|
+
parts.push(`${p}:${st ? st.mtimeMs : 'absent'}`);
|
|
800
|
+
} catch {
|
|
801
|
+
parts.push(`${p}:error`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const key = parts.join('|');
|
|
805
|
+
if (configCache.key === key) return configCache;
|
|
806
|
+
|
|
807
|
+
const events = {};
|
|
808
|
+
const legacyRules = [];
|
|
809
|
+
const sources = [];
|
|
810
|
+
const errors = [];
|
|
811
|
+
let disabled = false;
|
|
812
|
+
let disableSeen = false;
|
|
813
|
+
for (const entry of entries) {
|
|
814
|
+
const filePath = entry.path;
|
|
815
|
+
if (!existsSync(filePath)) continue;
|
|
816
|
+
let parsed = null;
|
|
817
|
+
try {
|
|
818
|
+
parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
819
|
+
} catch (error) {
|
|
820
|
+
errors.push({ file: filePath, error: error?.message || String(error) });
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
sources.push(filePath);
|
|
824
|
+
if (Object.prototype.hasOwnProperty.call(parsed || {}, 'disableAllHooks')) {
|
|
825
|
+
disabled = parsed.disableAllHooks === true;
|
|
826
|
+
disableSeen = true;
|
|
827
|
+
}
|
|
828
|
+
if (isStandardConfig(parsed)) {
|
|
829
|
+
mergeEvents(events, parseStandardConfig(parsed, filePath, entry));
|
|
830
|
+
} else {
|
|
831
|
+
legacyRules.push(...normalizeRules(parsed).filter((rule) => rule && typeof rule === 'object'));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
configCache = {
|
|
835
|
+
key,
|
|
836
|
+
standard: Object.keys(events).length > 0,
|
|
837
|
+
disabled: disableSeen ? disabled : false,
|
|
838
|
+
events,
|
|
839
|
+
legacyRules,
|
|
840
|
+
sources,
|
|
841
|
+
errors,
|
|
842
|
+
};
|
|
843
|
+
for (const err of errors) {
|
|
844
|
+
emit('hook:error', { error: `failed to parse hooks file ${err.file}: ${err.error}` });
|
|
845
|
+
}
|
|
846
|
+
return configCache;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function selectHandlers(eventName, payload) {
|
|
850
|
+
const cfg = loadConfig(payload.cwd || lastCwd);
|
|
851
|
+
if (cfg.disabled || !cfg.standard) return [];
|
|
852
|
+
const groups = cfg.events[eventName];
|
|
853
|
+
if (!Array.isArray(groups)) return [];
|
|
854
|
+
const field = matchFieldFor(eventName, payload);
|
|
855
|
+
const handlers = [];
|
|
856
|
+
const seen = new Set();
|
|
857
|
+
for (const group of groups) {
|
|
858
|
+
if (NO_MATCHER_EVENTS.has(eventName) || matcherFires(group.matcher, field)) {
|
|
859
|
+
for (const handler of group.hooks) {
|
|
860
|
+
const key = handlerDedupeKey(handler);
|
|
861
|
+
if (seen.has(key)) continue;
|
|
862
|
+
seen.add(key);
|
|
863
|
+
handlers.push(handler);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return handlers;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function runOneHandler(handler, eventName, payload) {
|
|
871
|
+
if (!handler || typeof handler !== 'object') return null;
|
|
872
|
+
if (!ifConditionPasses(handler.if, eventName, payload.tool_name, payload.tool_input)) return null;
|
|
873
|
+
const type = String(handler.type || '').trim();
|
|
874
|
+
if (!SUPPORTED_HANDLER_TYPES.has(type)) {
|
|
875
|
+
emit('hook:error', { name: payload.tool_name || eventName, error: `unsupported hook type: ${type || '(missing)'}` });
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
if (type === 'command') {
|
|
879
|
+
if (!handler.command) return null;
|
|
880
|
+
const reportSpawnError = (error) => {
|
|
881
|
+
emit('hook:error', {
|
|
882
|
+
name: payload.tool_name || eventName,
|
|
883
|
+
error: `hook spawn failed: ${error?.message || error}`,
|
|
884
|
+
});
|
|
885
|
+
};
|
|
886
|
+
return await runCommandHandler(handler, payload, eventName, pluginData, reportSpawnError);
|
|
887
|
+
}
|
|
888
|
+
if (type === 'http') {
|
|
889
|
+
if (!handler.url) return null;
|
|
890
|
+
return await runHttpHandler(handler, payload, eventName);
|
|
891
|
+
}
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async function runEventHandlers(eventName, payload) {
|
|
896
|
+
const handlers = selectHandlers(eventName, payload);
|
|
897
|
+
const agg = {
|
|
898
|
+
blocked: false,
|
|
899
|
+
reason: null,
|
|
900
|
+
updatedInput: null,
|
|
901
|
+
updatedToolOutput: null,
|
|
902
|
+
additionalContext: [],
|
|
903
|
+
systemMessage: null,
|
|
904
|
+
ask: false,
|
|
905
|
+
askReason: null,
|
|
906
|
+
handlersRun: handlers.length,
|
|
907
|
+
};
|
|
908
|
+
const results = await Promise.all(handlers.map(async (handler) => {
|
|
909
|
+
try {
|
|
910
|
+
return { handler, run: await runOneHandler(handler, eventName, payload) };
|
|
911
|
+
} catch (error) {
|
|
912
|
+
emit('hook:error', { name: payload.tool_name || eventName, error: error?.message || String(error) });
|
|
913
|
+
return { handler, run: null };
|
|
914
|
+
}
|
|
915
|
+
}));
|
|
916
|
+
|
|
917
|
+
for (const { handler, run } of results) {
|
|
918
|
+
if (!run) continue;
|
|
919
|
+
if (run.timedOut) {
|
|
920
|
+
emit('hook:error', { name: payload.tool_name || eventName, error: `hook ${shellCountFor(handler)} timed out: ${handler.command || handler.url || handler.type}` });
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (run.spawnError) {
|
|
924
|
+
emit('hook:error', { name: payload.tool_name || eventName, error: `hook spawn failed: ${run.spawnError.message || run.spawnError}` });
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (run.exitCode && run.exitCode !== 0 && run.exitCode !== 2) {
|
|
928
|
+
emit('hook:error', { name: payload.tool_name || eventName, error: (run.stderr || '').trim() || `hook exited ${run.exitCode}` });
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
const parsed = parseHandlerOutput(run, eventName);
|
|
932
|
+
if (parsed.additionalContext) agg.additionalContext.push(parsed.additionalContext);
|
|
933
|
+
if (parsed.systemMessage && !agg.systemMessage) agg.systemMessage = parsed.systemMessage;
|
|
934
|
+
if (parsed.updatedInput && !agg.updatedInput) agg.updatedInput = parsed.updatedInput;
|
|
935
|
+
if (parsed.updatedToolOutput != null && agg.updatedToolOutput == null) agg.updatedToolOutput = parsed.updatedToolOutput;
|
|
936
|
+
if (parsed.askReason && !agg.ask && !agg.blocked) {
|
|
937
|
+
agg.ask = true;
|
|
938
|
+
agg.askReason = parsed.askReason;
|
|
939
|
+
}
|
|
940
|
+
if (parsed.block && !agg.blocked) {
|
|
941
|
+
agg.blocked = true;
|
|
942
|
+
agg.reason = parsed.reason;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return agg;
|
|
946
|
+
}
|
|
947
|
+
|
|
150
948
|
function saveRules(rules) {
|
|
151
949
|
if (!rulesPath) throw new Error('hooks rules path is not configured');
|
|
152
950
|
mkdirSync(dirname(rulesPath), { recursive: true });
|
|
@@ -154,6 +952,7 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
|
|
|
154
952
|
writeFileSync(rulesPath, `${JSON.stringify({ toolBefore: cleanRules }, null, 2)}\n`, 'utf8');
|
|
155
953
|
const stat = statSync(rulesPath);
|
|
156
954
|
rulesCache = { mtimeMs: stat.mtimeMs, rules: cleanRules };
|
|
955
|
+
configCache.key = '';
|
|
157
956
|
return listRules();
|
|
158
957
|
}
|
|
159
958
|
|
|
@@ -163,8 +962,8 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
|
|
|
163
962
|
|
|
164
963
|
function addRule(rule = {}) {
|
|
165
964
|
const action = String(rule.action || rule.decision || '').trim().toLowerCase();
|
|
166
|
-
if (!action || !['allow', 'deny', 'block', 'modify', 'rewrite'].includes(action)) {
|
|
167
|
-
throw new Error('hook rule action must be allow, deny, block, modify, or
|
|
965
|
+
if (!action || !['allow', 'deny', 'block', 'modify', 'rewrite', 'ask'].includes(action)) {
|
|
966
|
+
throw new Error('hook rule action must be allow, deny, block, modify, rewrite, or ask');
|
|
168
967
|
}
|
|
169
968
|
const next = {
|
|
170
969
|
tool: rule.tool || rule.name || '*',
|
|
@@ -195,31 +994,99 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
|
|
|
195
994
|
}
|
|
196
995
|
|
|
197
996
|
async function beforeTool(input = {}) {
|
|
997
|
+
if (input?.cwd) lastCwd = input.cwd;
|
|
198
998
|
emit('tool:before', {
|
|
199
|
-
sessionId: input.sessionId || null,
|
|
200
|
-
name: input.name || 'tool',
|
|
201
|
-
callId: input.toolCallId || input.callId || null,
|
|
202
|
-
args: input.args || null,
|
|
999
|
+
sessionId: input.sessionId || input.session_id || null,
|
|
1000
|
+
name: input.name || input.tool_name || 'tool',
|
|
1001
|
+
callId: input.toolCallId || input.callId || input.tool_use_id || null,
|
|
1002
|
+
args: input.args || input.tool_input || null,
|
|
203
1003
|
});
|
|
204
1004
|
try {
|
|
205
|
-
const
|
|
206
|
-
|
|
1005
|
+
const cfg = loadConfig(input.cwd || lastCwd);
|
|
1006
|
+
if (cfg.disabled) return null;
|
|
1007
|
+
if (!cfg.disabled) {
|
|
1008
|
+
const payload = buildEventPayload('PreToolUse', input);
|
|
1009
|
+
const agg = await runEventHandlers('PreToolUse', payload);
|
|
1010
|
+
if (agg.blocked) {
|
|
1011
|
+
emit('tool:deny', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: agg.reason });
|
|
1012
|
+
return { action: 'deny', reason: agg.reason };
|
|
1013
|
+
}
|
|
1014
|
+
if (agg.updatedInput) {
|
|
1015
|
+
emit('tool:modify', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: agg.reason });
|
|
1016
|
+
return { action: 'modify', args: agg.updatedInput, reason: agg.reason };
|
|
1017
|
+
}
|
|
1018
|
+
if (agg.ask) {
|
|
1019
|
+
emit('tool:ask', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: agg.askReason });
|
|
1020
|
+
return { action: 'ask', reason: agg.askReason };
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const rules = Array.isArray(cfg.legacyRules) && cfg.legacyRules.length
|
|
1025
|
+
? cfg.legacyRules
|
|
1026
|
+
: loadRules();
|
|
1027
|
+
const rule = rules.find((candidate) => ruleMatches(candidate, {
|
|
1028
|
+
name: input.name || input.tool_name,
|
|
1029
|
+
args: input.args || input.tool_input,
|
|
1030
|
+
cwd: input.cwd,
|
|
1031
|
+
}));
|
|
207
1032
|
if (!rule) return null;
|
|
208
|
-
const decision = decisionFromRule(rule,
|
|
1033
|
+
const decision = decisionFromRule(rule, {
|
|
1034
|
+
name: input.name || input.tool_name,
|
|
1035
|
+
args: input.args || input.tool_input,
|
|
1036
|
+
cwd: input.cwd,
|
|
1037
|
+
});
|
|
209
1038
|
if (decision.action === 'deny') {
|
|
210
|
-
emit('tool:deny', { sessionId: input.sessionId || null, name: input.name || 'tool', reason: decision.reason });
|
|
1039
|
+
emit('tool:deny', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: decision.reason });
|
|
211
1040
|
} else if (decision.action === 'modify') {
|
|
212
|
-
emit('tool:modify', { sessionId: input.sessionId || null, name: input.name || 'tool', reason: decision.reason });
|
|
1041
|
+
emit('tool:modify', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: decision.reason });
|
|
1042
|
+
} else if (decision.action === 'ask') {
|
|
1043
|
+
emit('tool:ask', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: decision.reason });
|
|
213
1044
|
}
|
|
214
1045
|
return decision;
|
|
215
1046
|
} catch (error) {
|
|
216
|
-
emit('hook:error', { name: input.name || 'tool', error: error?.message || String(error) });
|
|
1047
|
+
emit('hook:error', { name: input.name || input.tool_name || 'tool', error: error?.message || String(error) });
|
|
217
1048
|
return null;
|
|
218
1049
|
}
|
|
219
1050
|
}
|
|
220
1051
|
|
|
1052
|
+
async function dispatch(eventName, payload = {}) {
|
|
1053
|
+
const name = String(eventName || '').trim();
|
|
1054
|
+
if (!name) return {};
|
|
1055
|
+
if (payload?.cwd) lastCwd = payload.cwd;
|
|
1056
|
+
emit(name, payload);
|
|
1057
|
+
try {
|
|
1058
|
+
const std = buildEventPayload(name, payload);
|
|
1059
|
+
const agg = await runEventHandlers(name, std);
|
|
1060
|
+
const out = {
|
|
1061
|
+
blocked: agg.blocked || undefined,
|
|
1062
|
+
reason: agg.reason || undefined,
|
|
1063
|
+
additionalContext: agg.additionalContext.length ? agg.additionalContext : undefined,
|
|
1064
|
+
systemMessage: agg.systemMessage || undefined,
|
|
1065
|
+
updatedInput: agg.updatedInput || undefined,
|
|
1066
|
+
handlersRun: agg.handlersRun || undefined,
|
|
1067
|
+
};
|
|
1068
|
+
if (agg.updatedToolOutput != null) out.updatedToolOutput = agg.updatedToolOutput;
|
|
1069
|
+
return out;
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
emit('hook:error', { name, error: error?.message || String(error) });
|
|
1072
|
+
return {};
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
221
1076
|
function status() {
|
|
222
|
-
let
|
|
1077
|
+
let cfg = {
|
|
1078
|
+
standard: false,
|
|
1079
|
+
disabled: false,
|
|
1080
|
+
events: {},
|
|
1081
|
+
sources: [],
|
|
1082
|
+
errors: [],
|
|
1083
|
+
};
|
|
1084
|
+
try {
|
|
1085
|
+
cfg = loadConfig(lastCwd);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
emit('hook:error', { error: error?.message || String(error) });
|
|
1088
|
+
}
|
|
1089
|
+
let ruleCount = 0;
|
|
223
1090
|
let rules = [];
|
|
224
1091
|
try {
|
|
225
1092
|
rules = listRules();
|
|
@@ -227,20 +1094,29 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
|
|
|
227
1094
|
} catch (error) {
|
|
228
1095
|
emit('hook:error', { error: error?.message || String(error) });
|
|
229
1096
|
}
|
|
1097
|
+
const configuredEvents = Object.keys(cfg.events || {});
|
|
230
1098
|
return {
|
|
231
|
-
enabled: true,
|
|
232
|
-
mode: 'standalone-
|
|
1099
|
+
enabled: cfg.disabled !== true,
|
|
1100
|
+
mode: 'standalone-standard',
|
|
1101
|
+
configMode: cfg.standard ? 'standard' : 'legacy',
|
|
233
1102
|
rulesPath,
|
|
1103
|
+
configSources: cfg.sources || [],
|
|
234
1104
|
ruleCount,
|
|
235
1105
|
rules,
|
|
236
|
-
|
|
1106
|
+
configuredEvents,
|
|
1107
|
+
events: [...new Set([...DEFAULT_EVENTS, ...counts.keys(), ...configuredEvents])],
|
|
237
1108
|
counts: Object.fromEntries([...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]))),
|
|
238
1109
|
recent: [...recent].reverse(),
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
1110
|
+
errors: cfg.errors || [],
|
|
1111
|
+
note: cfg.disabled
|
|
1112
|
+
? 'Hooks are disabled by disableAllHooks.'
|
|
1113
|
+
: (cfg.standard
|
|
1114
|
+
? `Standard Mixdog hooks active for events: ${configuredEvents.join(', ') || '(none)'}.`
|
|
1115
|
+
: (ruleCount > 0
|
|
1116
|
+
? 'Legacy before-tool hook rules are active. Rules may allow, deny, or modify tool arguments.'
|
|
1117
|
+
: 'No hook rules configured; lifecycle and tool events are recorded in observer mode.')),
|
|
242
1118
|
};
|
|
243
1119
|
}
|
|
244
1120
|
|
|
245
|
-
return { addRule, beforeTool, deleteRule, emit, listRules, setRuleEnabled, status };
|
|
1121
|
+
return { addRule, beforeTool, deleteRule, dispatch, emit, listRules, setRuleEnabled, status };
|
|
246
1122
|
}
|