mixdog 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- 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/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
// Hook IPC daemon — Windows named-pipe server consumed by mixdog-shim.exe.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the per-spawn cold-start cost of `bun hooks/*.cjs` (≈86ms) with a
|
|
4
|
+
// single long-lived listener inside the channels worker. The shim is a tiny
|
|
5
|
+
// Rust .exe (~111KB, ~5-10ms cold) that connects, writes one JSON line, reads
|
|
6
|
+
// one JSON line back, and exits.
|
|
7
|
+
//
|
|
8
|
+
// Protocol (line-delimited JSON):
|
|
9
|
+
// client → server : <Claude-Code hook payload>\n
|
|
10
|
+
// server → client : <decision-json or "null">\n
|
|
11
|
+
//
|
|
12
|
+
// Each connection is handled independently. Long-running handlers (Discord
|
|
13
|
+
// permission polling, up to 2 minutes) do not block other connections.
|
|
14
|
+
//
|
|
15
|
+
// Failure model: dispatch errors emit "null" (fail-open). The shim itself
|
|
16
|
+
// also fails open when the pipe is unreachable.
|
|
17
|
+
|
|
18
|
+
import { createServer, createConnection } from 'node:net'
|
|
19
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync, readFileSync } from 'node:fs'
|
|
20
|
+
import { join, resolve as pathResolve } from 'node:path'
|
|
21
|
+
import { homedir, tmpdir } from 'node:os'
|
|
22
|
+
import { randomBytes } from 'node:crypto'
|
|
23
|
+
import { request as httpsRequest } from 'node:https'
|
|
24
|
+
import { createRequire } from 'node:module'
|
|
25
|
+
|
|
26
|
+
const moduleRequire = createRequire(import.meta.url)
|
|
27
|
+
|
|
28
|
+
// IPC transport path. Windows uses a named pipe (`\\.\pipe\…`); Unix uses a
|
|
29
|
+
// Unix domain socket under XDG_RUNTIME_DIR (or /tmp as fallback). Node's
|
|
30
|
+
// net.createServer().listen() accepts both transparently.
|
|
31
|
+
const PIPE_PATH = moduleRequire('../../../lib/hook-pipe-path.cjs')()
|
|
32
|
+
|
|
33
|
+
const RUNTIME_ROOT = join(tmpdir(), 'mixdog')
|
|
34
|
+
const SIGNAL_CONSUMER_MARKER = join(RUNTIME_ROOT, '.tool-exec-consumer')
|
|
35
|
+
const SUBAGENT_SIGNAL_CONSUMER_MARKER = join(RUNTIME_ROOT, '.tool-exec-subagent-consumer')
|
|
36
|
+
const SIGNAL_RE_GENERIC = /^tool-exec-\d+-[0-9a-f]+\.signal$/
|
|
37
|
+
const SIGNAL_RE_CAPTURE = /^tool-exec-(\d+)-[0-9a-f]+\.signal$/
|
|
38
|
+
const SWEEP_MARKER = join(RUNTIME_ROOT, '.tool-exec-sweep')
|
|
39
|
+
const SWEEP_INTERVAL_MS = 30_000
|
|
40
|
+
const SIGNAL_TTL_MS = 60_000
|
|
41
|
+
// Marketplace installs use two naming shapes for the MCP server name —
|
|
42
|
+
// `plugin_mixdog_mixdog__` (legacy / mixdog marketplace) and
|
|
43
|
+
// `plugin_mixdog_trib-plugin__` (trib-plugin marketplace). PreToolUse
|
|
44
|
+
// sandbox checks must recognise both or sandbox evaluation silently
|
|
45
|
+
// misses MCP tool names from the other install layout.
|
|
46
|
+
const MCP_PREFIXES = [
|
|
47
|
+
'mcp__plugin_mixdog_mixdog__',
|
|
48
|
+
'mcp__plugin_mixdog_trib-plugin__',
|
|
49
|
+
]
|
|
50
|
+
const NATIVE_FILE_LOOKUP_TOOLS = new Set(['Read', 'Grep', 'Glob', 'Search', 'LS'])
|
|
51
|
+
function isMcpToolName(name) {
|
|
52
|
+
if (!name) return false
|
|
53
|
+
return MCP_PREFIXES.some(p => name.startsWith(p))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const POLL_INTERVAL_MS = 2000
|
|
57
|
+
const SUBAGENT_TIMEOUT_MS = 120_000
|
|
58
|
+
const DEFAULT_DISPATCH_TIMEOUT_MS = 15_000
|
|
59
|
+
const SESSION_START_MEMORY_DISPATCH_TIMEOUT_MS = 125_000
|
|
60
|
+
const SESSION_START_TRACE_ENABLED =
|
|
61
|
+
process.env.MIXDOG_DEBUG_SESSION_START === '1' ||
|
|
62
|
+
process.env.MIXDOG_DEBUG_SESSION_START === 'true'
|
|
63
|
+
|
|
64
|
+
let _started = false
|
|
65
|
+
let _server = null
|
|
66
|
+
let _subagentSignalConsumers = 0
|
|
67
|
+
|
|
68
|
+
function refreshSubagentSignalConsumerMarker() {
|
|
69
|
+
try {
|
|
70
|
+
if (_subagentSignalConsumers > 0) {
|
|
71
|
+
try { mkdirSync(RUNTIME_ROOT, { recursive: true }) } catch {}
|
|
72
|
+
writeFileSync(SUBAGENT_SIGNAL_CONSUMER_MARKER, String(Date.now()))
|
|
73
|
+
} else {
|
|
74
|
+
try { unlinkSync(SUBAGENT_SIGNAL_CONSUMER_MARKER) } catch {}
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatError(err) {
|
|
80
|
+
const msg = (err && (err.stack || err.message)) || err
|
|
81
|
+
return String(msg || 'unknown').replace(/\s+/g, ' ').slice(0, 2000)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function traceSessionStart(message) {
|
|
85
|
+
if (!SESSION_START_TRACE_ENABLED) return
|
|
86
|
+
const line = `[${new Date().toISOString()}] [hook-pipe][session-start] ${message}\n`
|
|
87
|
+
try { process.stderr.write(line) } catch {}
|
|
88
|
+
try {
|
|
89
|
+
const dataDir = process.env.CLAUDE_PLUGIN_DATA ||
|
|
90
|
+
join(homedir(), '.claude', 'plugins', 'data', 'mixdog-trib-plugin')
|
|
91
|
+
mkdirSync(dataDir, { recursive: true })
|
|
92
|
+
appendFileSync(join(dataDir, 'session-start.log'), line)
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function dispatchTimeoutMsForPayload(payload) {
|
|
97
|
+
const event = payload?.hook_event_name || payload?.hookEventName || ''
|
|
98
|
+
if (event !== 'SessionStart') return DEFAULT_DISPATCH_TIMEOUT_MS
|
|
99
|
+
const argsArr = payload?._args || []
|
|
100
|
+
const partArg = argsArr.find(a => a.startsWith('--part='))
|
|
101
|
+
const part = partArg ? partArg.slice('--part='.length) : ''
|
|
102
|
+
return (part === 'core' || part === 'recap')
|
|
103
|
+
? SESSION_START_MEMORY_DISPATCH_TIMEOUT_MS
|
|
104
|
+
: DEFAULT_DISPATCH_TIMEOUT_MS
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── post-tool-use handler ────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function sweepStaleSignalsThrottled(now = Date.now()) {
|
|
110
|
+
try {
|
|
111
|
+
let lastSweep = 0
|
|
112
|
+
try { lastSweep = statSync(SWEEP_MARKER).mtimeMs } catch {}
|
|
113
|
+
if (now - lastSweep < SWEEP_INTERVAL_MS) return
|
|
114
|
+
try { writeFileSync(SWEEP_MARKER, String(now)) } catch {}
|
|
115
|
+
const entries = readdirSync(RUNTIME_ROOT)
|
|
116
|
+
for (const name of entries) {
|
|
117
|
+
if (!SIGNAL_RE_GENERIC.test(name)) continue
|
|
118
|
+
const p = join(RUNTIME_ROOT, name)
|
|
119
|
+
try {
|
|
120
|
+
const st = statSync(p)
|
|
121
|
+
if (now - st.mtimeMs > SIGNAL_TTL_MS) unlinkSync(p)
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handlePostToolUse(payload) {
|
|
128
|
+
const toolName = payload?.tool_name || payload?.toolName || ''
|
|
129
|
+
if (!toolName) return null
|
|
130
|
+
if (_subagentSignalConsumers <= 0 &&
|
|
131
|
+
!existsSync(SIGNAL_CONSUMER_MARKER) &&
|
|
132
|
+
!existsSync(SUBAGENT_SIGNAL_CONSUMER_MARKER)) {
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
const filePath = payload?.tool_input?.file_path || payload?.toolInput?.file_path || ''
|
|
136
|
+
const toolUseId = payload?.tool_use_id || payload?.toolUseId || ''
|
|
137
|
+
|
|
138
|
+
try { if (!existsSync(RUNTIME_ROOT)) mkdirSync(RUNTIME_ROOT, { recursive: true }) } catch {}
|
|
139
|
+
sweepStaleSignalsThrottled()
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const rand = randomBytes(4).toString('hex')
|
|
143
|
+
const signalFile = join(RUNTIME_ROOT, `tool-exec-${Date.now()}-${rand}.signal`)
|
|
144
|
+
writeFileSync(signalFile, JSON.stringify({ toolName, filePath, toolUseId, ts: Date.now() }))
|
|
145
|
+
} catch (err) {
|
|
146
|
+
process.stderr.write(`[hook-pipe] post-tool-use signal write failed: ${err?.message || err}\n`)
|
|
147
|
+
}
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── pre-mcp-sandbox handler ──────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
function handlePreMcpSandbox(payload) {
|
|
154
|
+
const toolName = payload?.tool_name || payload?.toolName || ''
|
|
155
|
+
if (!isMcpToolName(toolName)) return null
|
|
156
|
+
|
|
157
|
+
const toolInput = payload?.tool_input ?? payload?.toolInput ?? {}
|
|
158
|
+
|
|
159
|
+
let userCwdRaw = payload?.cwd || ''
|
|
160
|
+
if (!userCwdRaw) {
|
|
161
|
+
const dataDir = process.env.CLAUDE_PLUGIN_DATA || ''
|
|
162
|
+
if (dataDir) {
|
|
163
|
+
try { userCwdRaw = readFileSync(join(dataDir, 'user-cwd.txt'), 'utf8').trim() } catch {}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!userCwdRaw) userCwdRaw = process.cwd()
|
|
167
|
+
|
|
168
|
+
const userCwd = pathResolve(userCwdRaw)
|
|
169
|
+
const projectDir = payload?.projectDir || payload?.project_dir ||
|
|
170
|
+
process.env.CLAUDE_PROJECT_DIR || userCwd
|
|
171
|
+
const permissionMode = payload?.permissionMode || payload?.permission_mode || undefined
|
|
172
|
+
|
|
173
|
+
let settingsPerms, evaluatePermission
|
|
174
|
+
try {
|
|
175
|
+
const settingsLoader = moduleRequire('../../../hooks/lib/settings-loader.cjs')
|
|
176
|
+
settingsPerms = settingsLoader.loadPermissions(projectDir)
|
|
177
|
+
} catch (err) {
|
|
178
|
+
process.stderr.write(`[hook-pipe] pre-mcp-sandbox settings-loader unavailable: ${err?.message || err}\n`)
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
const effectiveMode = permissionMode || settingsPerms.defaultMode
|
|
182
|
+
|
|
183
|
+
// Bypass fast-path: bypassPermissions/auto are full-allow by design; when no
|
|
184
|
+
// user deny rules are set, skip the evaluator (the owner opted into bypass).
|
|
185
|
+
if ((effectiveMode === 'bypassPermissions' || effectiveMode === 'auto') &&
|
|
186
|
+
(!settingsPerms.deny || settingsPerms.deny.length === 0)) {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const ev = moduleRequire('../../../hooks/lib/permission-evaluator.cjs')
|
|
192
|
+
evaluatePermission = ev.evaluatePermission
|
|
193
|
+
} catch (err) {
|
|
194
|
+
process.stderr.write(`[hook-pipe] pre-mcp-sandbox evaluator unavailable: ${err?.message || err}\n`)
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const evalResult = evaluatePermission({ toolName, toolInput, permissionMode, projectDir, userCwd, permissions: settingsPerms })
|
|
199
|
+
const { decision, reason, updatedInput } = evalResult
|
|
200
|
+
|
|
201
|
+
if (effectiveMode === 'bypassPermissions' || effectiveMode === 'auto') {
|
|
202
|
+
if (decision === 'deny') return makeDecision('deny', reason)
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
if (decision === 'allow') return null
|
|
206
|
+
if (decision === 'ask') return makeDecision('ask', reason, updatedInput)
|
|
207
|
+
return makeDecision('deny', reason)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function handleNativeFileLookup(payload) {
|
|
211
|
+
const toolName = payload?.tool_name || payload?.toolName || ''
|
|
212
|
+
if (!NATIVE_FILE_LOOKUP_TOOLS.has(toolName)) return null
|
|
213
|
+
return makeDecision(
|
|
214
|
+
'deny',
|
|
215
|
+
`Native ${toolName} is disabled by Mixdog. Use the Mixdog MCP read/grep/glob/list tools instead.`
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function makeDecision(decision, reason, updatedInput) {
|
|
220
|
+
const out = {
|
|
221
|
+
hookSpecificOutput: {
|
|
222
|
+
hookEventName: 'PreToolUse',
|
|
223
|
+
permissionDecision: decision,
|
|
224
|
+
permissionDecisionReason: reason,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
if (updatedInput !== undefined) out.hookSpecificOutput.updatedInput = updatedInput
|
|
228
|
+
return out
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── pre-tool-subagent handler (Discord permission flow, async) ───────────────
|
|
232
|
+
|
|
233
|
+
function sanitize(value) {
|
|
234
|
+
return String(value).replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readDiscordConfig() {
|
|
238
|
+
try {
|
|
239
|
+
const { readSection } = moduleRequire('../../../lib/config-cjs.cjs')
|
|
240
|
+
return readSection('channels')
|
|
241
|
+
} catch { return {} }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isProtectedPath(filePath, cwd) {
|
|
245
|
+
if (!filePath) return false
|
|
246
|
+
const norm = pathResolve(filePath).replace(/\\/g, '/').toLowerCase()
|
|
247
|
+
const cwdNorm = (cwd || process.cwd()).replace(/\\/g, '/').toLowerCase()
|
|
248
|
+
const insideCwd = cwdNorm && (norm === cwdNorm || norm.startsWith(cwdNorm.endsWith('/') ? cwdNorm : cwdNorm + '/'))
|
|
249
|
+
return !insideCwd
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function findAndClaimSignal(toolName, filePath, toolUseId, hookStartedAt) {
|
|
253
|
+
let entries
|
|
254
|
+
try { entries = readdirSync(RUNTIME_ROOT) } catch { return null }
|
|
255
|
+
for (const name of entries) {
|
|
256
|
+
const m = SIGNAL_RE_CAPTURE.exec(name)
|
|
257
|
+
if (!m) continue
|
|
258
|
+
const ts = Number(m[1])
|
|
259
|
+
if (!Number.isFinite(ts) || ts < hookStartedAt) continue
|
|
260
|
+
const p = join(RUNTIME_ROOT, name)
|
|
261
|
+
let raw
|
|
262
|
+
try { raw = readFileSync(p, 'utf8') } catch { continue }
|
|
263
|
+
let parsed
|
|
264
|
+
try { parsed = JSON.parse(raw) } catch { continue }
|
|
265
|
+
if (parsed?.toolName !== toolName) continue
|
|
266
|
+
if (parsed?.filePath !== filePath) continue
|
|
267
|
+
if (toolUseId && parsed?.toolUseId !== toolUseId) continue
|
|
268
|
+
try { unlinkSync(p) } catch {}
|
|
269
|
+
return p
|
|
270
|
+
}
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function discordApi(method, apiPath, token, body) {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const data = body ? JSON.stringify(body) : ''
|
|
277
|
+
const headers = { 'Authorization': 'Bot ' + token, 'Content-Type': 'application/json' }
|
|
278
|
+
if (data) headers['Content-Length'] = Buffer.byteLength(data)
|
|
279
|
+
const req = httpsRequest({ hostname: 'discord.com', path: apiPath, method, headers },
|
|
280
|
+
res => { let out = ''; res.on('data', d => { out += d }); res.on('end', () => { try { resolve(JSON.parse(out)) } catch { resolve({}) } }) })
|
|
281
|
+
req.setTimeout(10_000, () => req.destroy())
|
|
282
|
+
req.on('error', reject)
|
|
283
|
+
if (data) req.write(data)
|
|
284
|
+
req.end()
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
|
289
|
+
|
|
290
|
+
async function handlePreToolSubagent(payload) {
|
|
291
|
+
if (process.env.MIXDOG_CHANNELS_NO_CONNECT) return null
|
|
292
|
+
|
|
293
|
+
const isSidechain = payload.isSidechain ?? payload.is_sidechain
|
|
294
|
+
const agentIdRaw = payload.agentId ?? payload.agent_id
|
|
295
|
+
const toolInput = payload.tool_input ?? payload.toolInput ?? {}
|
|
296
|
+
|
|
297
|
+
const isSubagent = isSidechain === true || Boolean(agentIdRaw)
|
|
298
|
+
if (!isSubagent) return null
|
|
299
|
+
|
|
300
|
+
const toolName = payload.tool_name || payload.toolName || ''
|
|
301
|
+
if (toolName !== 'Edit' && toolName !== 'Write' && toolName !== 'MultiEdit') return null
|
|
302
|
+
|
|
303
|
+
const mode = payload.permissionMode || payload.permission_mode || payload.mode
|
|
304
|
+
if (mode === 'bypassPermissions' || mode === 'acceptEdits' || mode === 'auto') return null
|
|
305
|
+
|
|
306
|
+
const hookCwd = payload.cwd || toolInput.cwd || process.cwd()
|
|
307
|
+
const filePath = toolInput.file_path || ''
|
|
308
|
+
const resolvedPath = filePath ? pathResolve(hookCwd, filePath) : ''
|
|
309
|
+
if (!isProtectedPath(resolvedPath, hookCwd)) return null
|
|
310
|
+
|
|
311
|
+
try { mkdirSync(RUNTIME_ROOT, { recursive: true }) } catch {}
|
|
312
|
+
const toolUseId = payload.tool_use_id ?? payload.toolUseId ?? ''
|
|
313
|
+
|
|
314
|
+
let routeMod
|
|
315
|
+
try {
|
|
316
|
+
routeMod = moduleRequire('../../../hooks/lib/permission-route.cjs')
|
|
317
|
+
} catch (err) {
|
|
318
|
+
process.stderr.write(`[hook-pipe] pre-tool-subagent permission-route unavailable: ${err?.message || err}\n`)
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
const route = routeMod.shouldRoutePermissionToDiscord()
|
|
322
|
+
if (route.route !== 'discord') {
|
|
323
|
+
process.stderr.write(`[hook-pipe] pre-tool-subagent discord-route=off agent=${agentIdRaw || 'unknown'} tool=${toolName} reason=${route.reason || 'inactive'}\n`)
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
process.stderr.write(`[hook-pipe] pre-tool-subagent discord-route=on agent=${agentIdRaw || 'unknown'} tool=${toolName}\n`)
|
|
327
|
+
|
|
328
|
+
let getDiscordToken
|
|
329
|
+
try {
|
|
330
|
+
({ getDiscordToken } = moduleRequire('../../../lib/config-cjs.cjs'))
|
|
331
|
+
} catch (err) {
|
|
332
|
+
process.stderr.write(`[hook-pipe] pre-tool-subagent config-cjs unavailable: ${err?.message || err}\n`)
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
335
|
+
const cfg = readDiscordConfig()
|
|
336
|
+
const token = getDiscordToken()
|
|
337
|
+
if (!token) return null
|
|
338
|
+
const agentId = agentIdRaw || 'unknown'
|
|
339
|
+
const mainCh = cfg && cfg.channelsConfig && cfg.channelsConfig.main
|
|
340
|
+
const channelId = mainCh && (typeof mainCh === 'string' ? null : mainCh.channelId)
|
|
341
|
+
if (!channelId) return null
|
|
342
|
+
|
|
343
|
+
const uuid = randomBytes(16).toString('hex')
|
|
344
|
+
const permissionInstanceIds = route.permissionInstanceIds || [route.permissionInstanceId].filter(Boolean)
|
|
345
|
+
if (!permissionInstanceIds.length) return null
|
|
346
|
+
const resultFiles = permissionInstanceIds.map((id) => join(RUNTIME_ROOT, `perm-${sanitize(id)}-${uuid}.result`))
|
|
347
|
+
_subagentSignalConsumers += 1
|
|
348
|
+
refreshSubagentSignalConsumerMarker()
|
|
349
|
+
|
|
350
|
+
const releaseSubagentConsumer = () => {
|
|
351
|
+
_subagentSignalConsumers = Math.max(0, _subagentSignalConsumers - 1)
|
|
352
|
+
refreshSubagentSignalConsumerMarker()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let detail = ''
|
|
356
|
+
if (toolName === 'Edit') {
|
|
357
|
+
detail = filePath + '\n' + (toolInput.old_string || '').substring(0, 200)
|
|
358
|
+
} else {
|
|
359
|
+
detail = filePath
|
|
360
|
+
}
|
|
361
|
+
const content = `🔐 **Sub-agent Permission**\nAgent: \`${agentId}\`\nTool: \`${toolName}\`\n\`\`\`\n${detail}\n\`\`\``
|
|
362
|
+
|
|
363
|
+
const body = {
|
|
364
|
+
content,
|
|
365
|
+
components: [{
|
|
366
|
+
type: 1,
|
|
367
|
+
components: [
|
|
368
|
+
{ type: 2, style: 3, label: 'Allow', custom_id: 'perm-' + uuid + '-allow' },
|
|
369
|
+
{ type: 2, style: 1, label: 'Session Allow', custom_id: 'perm-' + uuid + '-session' },
|
|
370
|
+
{ type: 2, style: 4, label: 'Deny', custom_id: 'perm-' + uuid + '-deny' },
|
|
371
|
+
]
|
|
372
|
+
}]
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let msgResult
|
|
376
|
+
try {
|
|
377
|
+
msgResult = await discordApi('POST', '/api/v10/channels/' + channelId + '/messages', token, body)
|
|
378
|
+
} catch (err) {
|
|
379
|
+
process.stderr.write(`[hook-pipe] discord POST failed: ${err?.message || err}\n`)
|
|
380
|
+
releaseSubagentConsumer()
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
383
|
+
const messageId = msgResult?.id
|
|
384
|
+
if (!messageId) {
|
|
385
|
+
releaseSubagentConsumer()
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const hookStartedAt = Date.now()
|
|
390
|
+
|
|
391
|
+
const patchAndFinish = async (suffix, decisionJson) => {
|
|
392
|
+
try {
|
|
393
|
+
await discordApi('PATCH', '/api/v10/channels/' + channelId + '/messages/' + messageId, token, {
|
|
394
|
+
content: content + suffix,
|
|
395
|
+
components: []
|
|
396
|
+
})
|
|
397
|
+
} catch {}
|
|
398
|
+
for (const resultFile of resultFiles) {
|
|
399
|
+
try { unlinkSync(resultFile) } catch {}
|
|
400
|
+
}
|
|
401
|
+
releaseSubagentConsumer()
|
|
402
|
+
return decisionJson || null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const startTime = Date.now()
|
|
406
|
+
while (Date.now() - startTime < SUBAGENT_TIMEOUT_MS) {
|
|
407
|
+
await sleep(POLL_INTERVAL_MS)
|
|
408
|
+
|
|
409
|
+
const resultFile = resultFiles.find((file) => existsSync(file))
|
|
410
|
+
if (resultFile) {
|
|
411
|
+
let decision
|
|
412
|
+
try {
|
|
413
|
+
const result = readFileSync(resultFile, 'utf8').trim()
|
|
414
|
+
if (result === 'allow' || result === 'session') {
|
|
415
|
+
decision = makeDecision('allow')
|
|
416
|
+
} else {
|
|
417
|
+
decision = makeDecision('deny', 'Denied from Discord')
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
decision = makeDecision('deny', 'Failed to read result')
|
|
421
|
+
}
|
|
422
|
+
return patchAndFinish('', decision)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const claimed = findAndClaimSignal(toolName, filePath, toolUseId, hookStartedAt)
|
|
426
|
+
if (claimed) {
|
|
427
|
+
return patchAndFinish('\n\n↩️ Resolved from terminal.', null)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return patchAndFinish(
|
|
432
|
+
'\n\n⚠️ Auto-denied due to timeout.',
|
|
433
|
+
makeDecision('deny', 'Timeout')
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── statusline handler (via dynamic ESM import) ──────────────────────────────
|
|
438
|
+
|
|
439
|
+
let _statusLineMod = null
|
|
440
|
+
let _statusLineLoadPromise = null
|
|
441
|
+
|
|
442
|
+
async function ensureStatusLineMod() {
|
|
443
|
+
if (_statusLineMod) return _statusLineMod
|
|
444
|
+
if (_statusLineLoadPromise) return _statusLineLoadPromise
|
|
445
|
+
_statusLineLoadPromise = import('../../../bin/statusline-lib.mjs')
|
|
446
|
+
.then(mod => { _statusLineMod = mod; return mod })
|
|
447
|
+
.catch(err => {
|
|
448
|
+
process.stderr.write(`[hook-pipe] statusline-lib import failed: ${err?.message || err}\n`)
|
|
449
|
+
_statusLineLoadPromise = null
|
|
450
|
+
return null
|
|
451
|
+
})
|
|
452
|
+
return _statusLineLoadPromise
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function handleStatusLine(payload) {
|
|
456
|
+
const mod = await ensureStatusLineMod()
|
|
457
|
+
if (!mod || typeof mod.renderStatusLine !== 'function') return null
|
|
458
|
+
try {
|
|
459
|
+
return await mod.renderStatusLine(JSON.stringify(payload || {}))
|
|
460
|
+
} catch (err) {
|
|
461
|
+
process.stderr.write(`[hook-pipe] statusline render failed: ${err?.message || err}\n`)
|
|
462
|
+
return null
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── SessionStart: rules/core/recap handlers (via require'd cjs) ──────────────
|
|
467
|
+
//
|
|
468
|
+
// session-start.cjs accesses fd 0 at the top level — we gate that behind
|
|
469
|
+
// MIXDOG_SKIP_TOP_STDIN so it doesn't consume the daemon's MCP stdio pipe.
|
|
470
|
+
// Each SessionStart slot gets a fresh CJS module instance. That keeps the
|
|
471
|
+
// module-globals (_event, PART, _emitSink) isolated, so rules/core/recap can
|
|
472
|
+
// run concurrently without a daemon-wide lock.
|
|
473
|
+
function loadSessionStartMod() {
|
|
474
|
+
const prev = process.env.MIXDOG_SKIP_TOP_STDIN
|
|
475
|
+
process.env.MIXDOG_SKIP_TOP_STDIN = '1'
|
|
476
|
+
const moduleId = moduleRequire.resolve('../../../hooks/session-start.cjs')
|
|
477
|
+
delete moduleRequire.cache[moduleId]
|
|
478
|
+
traceSessionStart('fresh require start path=../../../hooks/session-start.cjs')
|
|
479
|
+
try {
|
|
480
|
+
const mod = moduleRequire(moduleId)
|
|
481
|
+
delete moduleRequire.cache[moduleId]
|
|
482
|
+
traceSessionStart(`fresh require ok exports=${Object.keys(mod || {}).join(',')}`)
|
|
483
|
+
return mod
|
|
484
|
+
} catch (err) {
|
|
485
|
+
process.stderr.write(`[hook-pipe] session-start.cjs require failed: ${err?.message || err}\n`)
|
|
486
|
+
traceSessionStart(`require failed err=${formatError(err)}`)
|
|
487
|
+
return null
|
|
488
|
+
} finally {
|
|
489
|
+
if (prev === undefined) delete process.env.MIXDOG_SKIP_TOP_STDIN
|
|
490
|
+
else process.env.MIXDOG_SKIP_TOP_STDIN = prev
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function handleSessionStartPart(args, payload) {
|
|
495
|
+
if (payload?.isSidechain || payload?.is_sidechain) {
|
|
496
|
+
traceSessionStart(`skip reason=sidechain source=${payload?.source || ''}`)
|
|
497
|
+
return null
|
|
498
|
+
}
|
|
499
|
+
if (payload?.agentId || payload?.agent_id) {
|
|
500
|
+
traceSessionStart(`skip reason=agent source=${payload?.source || ''} agent=${payload?.agentId || payload?.agent_id || ''}`)
|
|
501
|
+
return null
|
|
502
|
+
}
|
|
503
|
+
if (payload?.kind && payload.kind !== 'interactive') {
|
|
504
|
+
traceSessionStart(`skip reason=kind source=${payload?.source || ''} kind=${payload.kind}`)
|
|
505
|
+
return null
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const partArg = (args || []).find(a => a.startsWith('--part='))
|
|
509
|
+
const part = partArg ? partArg.slice('--part='.length) : null
|
|
510
|
+
if (!part || (part !== 'rules' && part !== 'core' && part !== 'recap')) {
|
|
511
|
+
traceSessionStart(`skip reason=invalid-part source=${payload?.source || ''} args=${JSON.stringify(args || [])}`)
|
|
512
|
+
return null
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const mod = loadSessionStartMod()
|
|
516
|
+
if (!mod) {
|
|
517
|
+
traceSessionStart(`skip reason=require-null part=${part} source=${payload?.source || ''}`)
|
|
518
|
+
return null
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let buf = ''
|
|
522
|
+
let failed = false
|
|
523
|
+
const t0 = Date.now()
|
|
524
|
+
try {
|
|
525
|
+
traceSessionStart(
|
|
526
|
+
`run start part=${part} source=${payload?.source || ''} cwd=${payload?.cwd || ''} ` +
|
|
527
|
+
`sessionId=${payload?.session_id || payload?.sessionId || ''}`
|
|
528
|
+
)
|
|
529
|
+
try { mod.setEvent(payload || {}) } catch (err) {
|
|
530
|
+
failed = true
|
|
531
|
+
traceSessionStart(`setEvent failed part=${part} err=${formatError(err)}`)
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
if (typeof mod.setPart === 'function') mod.setPart(part)
|
|
535
|
+
else traceSessionStart(`setPart unavailable part=${part}`)
|
|
536
|
+
} catch (err) {
|
|
537
|
+
failed = true
|
|
538
|
+
traceSessionStart(`setPart failed part=${part} err=${formatError(err)}`)
|
|
539
|
+
}
|
|
540
|
+
try { mod.setEmitSink(s => { buf += String(s) }) } catch (err) {
|
|
541
|
+
failed = true
|
|
542
|
+
traceSessionStart(`setEmitSink failed part=${part} err=${formatError(err)}`)
|
|
543
|
+
}
|
|
544
|
+
if (part === 'rules') await mod.runRulesPart()
|
|
545
|
+
else if (part === 'core') await mod.runCorePart()
|
|
546
|
+
else if (part === 'recap') await mod.runRecapPart()
|
|
547
|
+
} catch (err) {
|
|
548
|
+
failed = true
|
|
549
|
+
process.stderr.write(`[hook-pipe] session-start ${part} failed: ${err?.message || err}\n`)
|
|
550
|
+
traceSessionStart(`run failed part=${part} err=${formatError(err)}`)
|
|
551
|
+
} finally {
|
|
552
|
+
try { mod.setEmitSink(null) } catch (err) {
|
|
553
|
+
failed = true
|
|
554
|
+
traceSessionStart(`clearEmitSink failed part=${part} err=${formatError(err)}`)
|
|
555
|
+
}
|
|
556
|
+
traceSessionStart(
|
|
557
|
+
`run done part=${part} source=${payload?.source || ''} ` +
|
|
558
|
+
`bytes=${Buffer.byteLength(buf, 'utf8')} elapsed=${Date.now() - t0}ms failed=${failed}`
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
return buf || null
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ── SessionStart: clear-active-session handler ───────────────────────────────
|
|
565
|
+
|
|
566
|
+
function handleSessionStartClear() {
|
|
567
|
+
// Clear the active orchestrator session pointer so each Claude Code session
|
|
568
|
+
// starts fresh. Stored sessions on disk are NOT deleted — only the pointer.
|
|
569
|
+
try {
|
|
570
|
+
const dataDir = process.env.CLAUDE_PLUGIN_DATA || ''
|
|
571
|
+
if (!dataDir) return null
|
|
572
|
+
const target = join(dataDir, 'active-session.txt')
|
|
573
|
+
try { unlinkSync(target) } catch {}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
process.stderr.write(`[hook-pipe] session-start clear failed: ${err?.message || err}\n`)
|
|
576
|
+
}
|
|
577
|
+
return null
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ── dispatch ─────────────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
async function dispatch(payload) {
|
|
583
|
+
const event = payload?.hook_event_name || payload?.hookEventName || ''
|
|
584
|
+
const tool = payload?.tool_name || payload?.toolName || ''
|
|
585
|
+
const argsArr = payload?._args || []
|
|
586
|
+
|
|
587
|
+
// CLI-arg-driven routing (statusline + future entry points without a
|
|
588
|
+
// hook_event_name field).
|
|
589
|
+
const kindArg = argsArr.find(a => a.startsWith('--kind='))
|
|
590
|
+
if (kindArg) {
|
|
591
|
+
const kind = kindArg.slice('--kind='.length)
|
|
592
|
+
if (kind === 'statusline') return await handleStatusLine(payload)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
if (event === 'PreToolUse') {
|
|
597
|
+
if (NATIVE_FILE_LOOKUP_TOOLS.has(tool)) {
|
|
598
|
+
return handleNativeFileLookup(payload)
|
|
599
|
+
}
|
|
600
|
+
if (tool === 'Edit' || tool === 'Write' || tool === 'MultiEdit') {
|
|
601
|
+
return await handlePreToolSubagent(payload)
|
|
602
|
+
}
|
|
603
|
+
if (isMcpToolName(tool)) {
|
|
604
|
+
return handlePreMcpSandbox(payload)
|
|
605
|
+
}
|
|
606
|
+
} else if (event === 'PostToolUse') {
|
|
607
|
+
return handlePostToolUse(payload)
|
|
608
|
+
} else if (event === 'SessionStart') {
|
|
609
|
+
const argsArr = payload?._args || []
|
|
610
|
+
const hasPart = argsArr.some(a => a.startsWith('--part='))
|
|
611
|
+
if (hasPart) {
|
|
612
|
+
return await handleSessionStartPart(argsArr, payload)
|
|
613
|
+
}
|
|
614
|
+
// No --part: clear-active-session entry.
|
|
615
|
+
return handleSessionStartClear()
|
|
616
|
+
}
|
|
617
|
+
} catch (err) {
|
|
618
|
+
process.stderr.write(`[hook-pipe] dispatch error: ${err?.message || err}\n`)
|
|
619
|
+
}
|
|
620
|
+
return null
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ── server ───────────────────────────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
export function startHookPipeServer() {
|
|
626
|
+
if (_server) return _server
|
|
627
|
+
|
|
628
|
+
_server = createServer((socket) => {
|
|
629
|
+
let buf = ''
|
|
630
|
+
let handled = false
|
|
631
|
+
// Resource guards: a connection that never sends a newline-terminated
|
|
632
|
+
// payload would otherwise grow buf unbounded and hold the socket open
|
|
633
|
+
// forever. Cap the buffered bytes and idle-close a stalled connection.
|
|
634
|
+
const MAX_BUF_BYTES = 1 << 20 // 1 MiB
|
|
635
|
+
const IDLE_TIMEOUT_MS = 30_000
|
|
636
|
+
socket.setTimeout(IDLE_TIMEOUT_MS, () => {
|
|
637
|
+
if (!handled) { try { socket.destroy() } catch {} }
|
|
638
|
+
})
|
|
639
|
+
socket.on('data', async (chunk) => {
|
|
640
|
+
if (handled) return
|
|
641
|
+
buf += chunk.toString('utf8')
|
|
642
|
+
if (Buffer.byteLength(buf, 'utf8') > MAX_BUF_BYTES) {
|
|
643
|
+
handled = true
|
|
644
|
+
process.stderr.write(`[hook-pipe] payload exceeded ${MAX_BUF_BYTES} bytes without newline; dropping connection\n`)
|
|
645
|
+
try { socket.destroy() } catch {}
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
const firstNl = buf.indexOf('\n')
|
|
649
|
+
if (firstNl < 0) return
|
|
650
|
+
const firstLine = buf.slice(0, firstNl)
|
|
651
|
+
|
|
652
|
+
// Optional `args=` prefix line. When present, the actual payload is the
|
|
653
|
+
// second line; otherwise the first line IS the payload.
|
|
654
|
+
let args = []
|
|
655
|
+
let payloadLine
|
|
656
|
+
if (firstLine.startsWith('args=')) {
|
|
657
|
+
const secondNl = buf.indexOf('\n', firstNl + 1)
|
|
658
|
+
if (secondNl < 0) return // wait for more
|
|
659
|
+
args = firstLine.slice(5).split(' ').filter(Boolean)
|
|
660
|
+
payloadLine = buf.slice(firstNl + 1, secondNl)
|
|
661
|
+
} else {
|
|
662
|
+
payloadLine = firstLine
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
handled = true
|
|
666
|
+
let payload = null
|
|
667
|
+
try { payload = payloadLine ? JSON.parse(payloadLine) : null } catch {}
|
|
668
|
+
if (payload && args.length > 0) payload._args = args
|
|
669
|
+
|
|
670
|
+
// Per-request deadline: a hung handler would otherwise hold the hook
|
|
671
|
+
// client waiting for EOF forever, stalling Claude Code's hook step. Race
|
|
672
|
+
// dispatch against a real timer; on timeout, write the no-op fallback and
|
|
673
|
+
// end the socket so the client unblocks.
|
|
674
|
+
const dispatchTimeoutMs = payload ? dispatchTimeoutMsForPayload(payload) : DEFAULT_DISPATCH_TIMEOUT_MS
|
|
675
|
+
let timedOut = false
|
|
676
|
+
let deadlineTimer = null
|
|
677
|
+
let reply = null
|
|
678
|
+
try {
|
|
679
|
+
if (payload) {
|
|
680
|
+
reply = await new Promise((resolve, reject) => {
|
|
681
|
+
deadlineTimer = setTimeout(() => {
|
|
682
|
+
timedOut = true
|
|
683
|
+
reject(new Error(`dispatch exceeded ${dispatchTimeoutMs}ms`))
|
|
684
|
+
}, dispatchTimeoutMs)
|
|
685
|
+
dispatch(payload).then(resolve, reject)
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
} catch (err) {
|
|
689
|
+
if (timedOut) {
|
|
690
|
+
process.stderr.write(`[hook-pipe] dispatch timed out after ${dispatchTimeoutMs}ms; writing no-op fallback\n`)
|
|
691
|
+
try { socket.write('null\n') } catch {}
|
|
692
|
+
try { socket.end() } catch {}
|
|
693
|
+
return
|
|
694
|
+
}
|
|
695
|
+
process.stderr.write(`[hook-pipe] handler threw: ${err?.message || err}\n`)
|
|
696
|
+
} finally {
|
|
697
|
+
if (deadlineTimer) { clearTimeout(deadlineTimer); deadlineTimer = null }
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Response shape:
|
|
701
|
+
// • object → JSON-stringified single line (legacy decision protocol)
|
|
702
|
+
// • string → raw text (multi-line session-start / statusline output)
|
|
703
|
+
// • null/undefined → "null" (no-op marker)
|
|
704
|
+
let out
|
|
705
|
+
if (reply == null) out = 'null'
|
|
706
|
+
else if (typeof reply === 'string') out = reply
|
|
707
|
+
else out = JSON.stringify(reply)
|
|
708
|
+
|
|
709
|
+
try { socket.write(out) } catch {}
|
|
710
|
+
if (!out.endsWith('\n')) { try { socket.write('\n') } catch {} }
|
|
711
|
+
try { socket.end() } catch {}
|
|
712
|
+
})
|
|
713
|
+
socket.on('error', () => {})
|
|
714
|
+
})
|
|
715
|
+
_server.on('error', (err) => {
|
|
716
|
+
const msg = String(err?.message || err || '')
|
|
717
|
+
if (err?.code === 'EADDRINUSE' || msg.includes('EADDRINUSE') || msg.includes('Failed to listen')) {
|
|
718
|
+
process.stderr.write(`[hook-pipe] ${PIPE_PATH} already owned by a peer daemon; standby for hook IPC\n`)
|
|
719
|
+
_server = null
|
|
720
|
+
_started = false
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
process.stderr.write(`[hook-pipe] server error: ${err?.message || err}\n`)
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
const beginListen = () => {
|
|
727
|
+
try {
|
|
728
|
+
_server.listen(PIPE_PATH, () => {
|
|
729
|
+
_started = true
|
|
730
|
+
process.stderr.write(`[hook-pipe] listening on ${PIPE_PATH}\n`)
|
|
731
|
+
})
|
|
732
|
+
} catch (err) {
|
|
733
|
+
process.stderr.write(`[hook-pipe] listen failed: ${err?.message || err}\n`)
|
|
734
|
+
_server = null
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (process.platform === 'win32') {
|
|
739
|
+
// Windows named pipes refuse a second listener with EADDRINUSE on their
|
|
740
|
+
// own, so no pre-listen probe is needed.
|
|
741
|
+
beginListen()
|
|
742
|
+
} else {
|
|
743
|
+
// Unix: a leftover socket file from a crashed prior daemon would make
|
|
744
|
+
// listen() fail with EADDRINUSE. But blindly unlinking would also steal
|
|
745
|
+
// the socket from a live sibling daemon, leaving it orphaned. Probe the
|
|
746
|
+
// path first — only unlink when nothing answers.
|
|
747
|
+
probeUnixSocketAlive(PIPE_PATH).then((alive) => {
|
|
748
|
+
if (alive) {
|
|
749
|
+
process.stderr.write(
|
|
750
|
+
`[hook-pipe] another mixdog daemon is already listening on ${PIPE_PATH}; refusing to start a second instance\n`
|
|
751
|
+
)
|
|
752
|
+
_server = null
|
|
753
|
+
return
|
|
754
|
+
}
|
|
755
|
+
try { unlinkSync(PIPE_PATH) } catch {}
|
|
756
|
+
beginListen()
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
return _server
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Best-effort liveness check for a Unix socket path. Resolves true when
|
|
763
|
+
// something is listening (connect succeeds), false when the path is dead
|
|
764
|
+
// (ECONNREFUSED) or absent (ENOENT). Other errors / timeout resolve true so
|
|
765
|
+
// we err on the side of NOT stealing a possibly-live peer's socket.
|
|
766
|
+
function probeUnixSocketAlive(socketPath) {
|
|
767
|
+
return new Promise((resolve) => {
|
|
768
|
+
let done = false
|
|
769
|
+
const finish = (alive) => {
|
|
770
|
+
if (done) return
|
|
771
|
+
done = true
|
|
772
|
+
try { client.destroy() } catch {}
|
|
773
|
+
clearTimeout(timer)
|
|
774
|
+
resolve(alive)
|
|
775
|
+
}
|
|
776
|
+
let client
|
|
777
|
+
try {
|
|
778
|
+
client = createConnection(socketPath)
|
|
779
|
+
} catch {
|
|
780
|
+
resolve(false)
|
|
781
|
+
return
|
|
782
|
+
}
|
|
783
|
+
const timer = setTimeout(() => finish(true), 300)
|
|
784
|
+
client.once('connect', () => finish(true))
|
|
785
|
+
client.once('error', (err) => {
|
|
786
|
+
const code = err && err.code
|
|
787
|
+
finish(!(code === 'ECONNREFUSED' || code === 'ENOENT'))
|
|
788
|
+
})
|
|
789
|
+
})
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function stopHookPipeServer() {
|
|
793
|
+
if (_server) {
|
|
794
|
+
try { _server.close() } catch {}
|
|
795
|
+
_server = null
|
|
796
|
+
_started = false
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function isHookPipeServerStarted() {
|
|
801
|
+
return _started
|
|
802
|
+
}
|