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,723 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, appendFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { appendFile as _appendFile } from "fs";
|
|
3
|
+
import { join, isAbsolute } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { DATA_DIR, DEFAULT_HOLIDAY_COUNTRY, isInQuietWindow } from "./config.mjs";
|
|
7
|
+
import { runScript as execScript, ensureNopluginDir } from "./executor.mjs";
|
|
8
|
+
import { withFileLockSync } from "../../shared/atomic-file.mjs";
|
|
9
|
+
import { makeBridgeLlm } from '../../agent/orchestrator/smart-bridge/bridge-llm.mjs';
|
|
10
|
+
|
|
11
|
+
const schedulerLlm = makeBridgeLlm({ taskType: 'scheduler-task', role: 'scheduler-task', sourceType: 'scheduler' });
|
|
12
|
+
const SCHEDULE_LOG = join(DATA_DIR, "schedule.log");
|
|
13
|
+
// Buffered async logger — coalesces per-line appends into batched writes.
|
|
14
|
+
let _schedLogBuf = [];
|
|
15
|
+
let _schedLogTimer = null;
|
|
16
|
+
function _flushScheduleLog() {
|
|
17
|
+
_schedLogTimer = null;
|
|
18
|
+
if (_schedLogBuf.length === 0) return;
|
|
19
|
+
const lines = _schedLogBuf.join("");
|
|
20
|
+
_schedLogBuf = [];
|
|
21
|
+
_appendFile(SCHEDULE_LOG, lines, () => {});
|
|
22
|
+
}
|
|
23
|
+
function _flushSchedLogSync() {
|
|
24
|
+
if (_schedLogBuf.length === 0) return;
|
|
25
|
+
const lines = _schedLogBuf.join("");
|
|
26
|
+
_schedLogBuf = [];
|
|
27
|
+
try { appendFileSync(SCHEDULE_LOG, lines); } catch {}
|
|
28
|
+
}
|
|
29
|
+
process.on('exit', _flushSchedLogSync);
|
|
30
|
+
// Note: do not install a module-level SIGTERM handler that calls
|
|
31
|
+
// process.exit() here. The channels worker owns shutdown sequencing
|
|
32
|
+
// (drain queues, persist baselines, release the scheduler lock, etc.)
|
|
33
|
+
// and a library-level exit(0) preempts that drain. The `exit` listener
|
|
34
|
+
// above still flushes pending log lines synchronously when the worker
|
|
35
|
+
// finishes its own shutdown.
|
|
36
|
+
const SCHEDULE_STATE_FILE = join(DATA_DIR, "schedule-state.json");
|
|
37
|
+
function readScheduleState() {
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(SCHEDULE_STATE_FILE, "utf8");
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
return (parsed && typeof parsed === "object") ? parsed : {};
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function writeScheduleState(state) {
|
|
47
|
+
writeFileSync(SCHEDULE_STATE_FILE, JSON.stringify(state ?? {}, null, 2));
|
|
48
|
+
}
|
|
49
|
+
function logSchedule(msg) {
|
|
50
|
+
process.stderr.write(`mixdog scheduler: ${msg}\n`);
|
|
51
|
+
_schedLogBuf.push(`[${new Date().toISOString()}] ${msg}\n`);
|
|
52
|
+
if (!_schedLogTimer) _schedLogTimer = setTimeout(_flushScheduleLog, 2000);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
import { isHoliday } from "./holidays.mjs";
|
|
56
|
+
import { tryRead } from "./settings.mjs";
|
|
57
|
+
// node-cron is an optional runtime dep. If the module isn't installed
|
|
58
|
+
// (e.g. a fresh v0.6.190 where node_modules predates the package.json
|
|
59
|
+
// bump), cron expressions are disabled (cron stays null below) instead
|
|
60
|
+
// of crashing the whole channels worker.
|
|
61
|
+
let cron = null;
|
|
62
|
+
try {
|
|
63
|
+
const mod = await import("node-cron");
|
|
64
|
+
cron = mod.default || mod;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
process.stderr.write(`mixdog scheduler: node-cron unavailable, cron expressions disabled (${err?.code || err?.message || err})\n`);
|
|
67
|
+
}
|
|
68
|
+
const TICK_INTERVAL = 6e4;
|
|
69
|
+
// All schedule `time` values must be valid 5- or 6-field cron expressions
|
|
70
|
+
// (node-cron format). Legacy formats (HH:MM, everyNm, hourly, daily) are
|
|
71
|
+
// no longer accepted — migrate to cron: "MM HH * * *", "*/N * * * *", etc.
|
|
72
|
+
function isCronExpression(time) {
|
|
73
|
+
if (typeof time !== "string" || !time) return false;
|
|
74
|
+
if (!cron) return false;
|
|
75
|
+
const tokens = time.trim().split(/\s+/);
|
|
76
|
+
if (tokens.length !== 5 && tokens.length !== 6) return false;
|
|
77
|
+
try { return cron.validate(time); } catch { return false; }
|
|
78
|
+
}
|
|
79
|
+
/** Validate a cron expression and throw a descriptive error if invalid.
|
|
80
|
+
* Used by schedule_control / schedules POST before accepting input. */
|
|
81
|
+
export function validateCronExpression(time) {
|
|
82
|
+
if (typeof time !== "string" || !time) throw new Error(`invalid cron expression: ${JSON.stringify(time)}`);
|
|
83
|
+
if (!cron) throw new Error(`cron expression "${time}" rejected: node-cron is not available (install node-cron to use cron expressions)`);
|
|
84
|
+
const tokens = time.trim().split(/\s+/);
|
|
85
|
+
if (tokens.length !== 5 && tokens.length !== 6) {
|
|
86
|
+
throw new Error(`invalid cron expression "${time}": expected 5 or 6 fields, got ${tokens.length}. Legacy formats (HH:MM, everyNm, hourly, daily) are no longer supported — use a cron expression instead.`);
|
|
87
|
+
}
|
|
88
|
+
let valid = false;
|
|
89
|
+
try { valid = cron.validate(time); } catch (e) {
|
|
90
|
+
throw new Error(`invalid cron expression "${time}": ${e?.message || e}`);
|
|
91
|
+
}
|
|
92
|
+
if (!valid) throw new Error(`invalid cron expression "${time}": failed node-cron validation. Legacy formats (HH:MM, everyNm, hourly, daily) are no longer supported — use a cron expression instead.`);
|
|
93
|
+
}
|
|
94
|
+
// Build a {hhmm, dateStr, dow} snapshot in the given IANA TZ. Falls
|
|
95
|
+
// back to local Date math when tz is absent.
|
|
96
|
+
function tzSnapshot(now, tz) {
|
|
97
|
+
if (!tz) {
|
|
98
|
+
return {
|
|
99
|
+
hhmm: `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`,
|
|
100
|
+
dateStr: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`,
|
|
101
|
+
dow: now.getDay(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
105
|
+
hour12: false, timeZone: tz,
|
|
106
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
107
|
+
hour: "2-digit", minute: "2-digit", weekday: "short",
|
|
108
|
+
}).formatToParts(now).reduce((acc, p) => { acc[p.type] = p.value; return acc; }, {});
|
|
109
|
+
const hour = parts.hour === "24" ? "00" : parts.hour;
|
|
110
|
+
const dowMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
111
|
+
return {
|
|
112
|
+
hhmm: `${hour}:${parts.minute}`,
|
|
113
|
+
dateStr: `${parts.year}-${parts.month}-${parts.day}`,
|
|
114
|
+
dow: dowMap[parts.weekday] ?? now.getDay(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
class Scheduler {
|
|
118
|
+
nonInteractive;
|
|
119
|
+
interactive;
|
|
120
|
+
channelsConfig;
|
|
121
|
+
promptsDir;
|
|
122
|
+
tickTimer = null;
|
|
123
|
+
lastFired = /* @__PURE__ */ new Map();
|
|
124
|
+
// name -> "YYYY-MM-DDTHH:MM"
|
|
125
|
+
running = /* @__PURE__ */ new Set();
|
|
126
|
+
injectFn = null;
|
|
127
|
+
sendFn = null;
|
|
128
|
+
pendingCheck = null;
|
|
129
|
+
// Activity tracking
|
|
130
|
+
lastActivity = 0;
|
|
131
|
+
// timestamp of last inbound message
|
|
132
|
+
deferred = /* @__PURE__ */ new Map();
|
|
133
|
+
// name -> deferred-until timestamp
|
|
134
|
+
skippedToday = /* @__PURE__ */ new Set();
|
|
135
|
+
// names skipped for today
|
|
136
|
+
skippedTodayDate = "";
|
|
137
|
+
// "YYYY-MM-DD" local date the skippedToday set belongs to
|
|
138
|
+
holidayCountry = null;
|
|
139
|
+
// ISO country code for holiday check
|
|
140
|
+
holidayChecked = "";
|
|
141
|
+
// "YYYY-MM-DD" last checked date
|
|
142
|
+
todayIsHoliday = false;
|
|
143
|
+
// cached result for today
|
|
144
|
+
quietSchedule = null;
|
|
145
|
+
// global quiet hours "HH:MM-HH:MM"
|
|
146
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
147
|
+
// name -> node-cron ScheduledTask for cron-expression entries
|
|
148
|
+
//
|
|
149
|
+
// 0.1.62 wiring:
|
|
150
|
+
// `topConfig` is the normalized top-level channels config from
|
|
151
|
+
// loadConfig()/applyDefaults() — it carries `quiet`, `schedules`
|
|
152
|
+
// at the top level. `channelsConfig` is still accepted separately
|
|
153
|
+
// (channel metadata only, not quiet config) because resolveChannel()
|
|
154
|
+
// needs the channel-label → platform-id map.
|
|
155
|
+
constructor(nonInteractive, interactive, channelsConfig, topConfig) {
|
|
156
|
+
this.nonInteractive = nonInteractive.filter((s) => s.enabled !== false);
|
|
157
|
+
this.interactive = interactive.filter((s) => s.enabled !== false);
|
|
158
|
+
this.channelsConfig = channelsConfig ?? null;
|
|
159
|
+
this.promptsDir = join(DATA_DIR, "prompts");
|
|
160
|
+
this._applyQuietConfig(topConfig);
|
|
161
|
+
}
|
|
162
|
+
/** Resolve quiet/schedules flags from the top-level config
|
|
163
|
+
* (0.1.62 shape: `topConfig.quiet`, `topConfig.schedules`). Falls
|
|
164
|
+
* through silently to defaults (empty schedule, holidays off,
|
|
165
|
+
* respect flag default true) when topConfig is missing or malformed
|
|
166
|
+
* — defensive, not legacy. */
|
|
167
|
+
_applyQuietConfig(topConfig) {
|
|
168
|
+
const cfg = (topConfig && typeof topConfig === "object") ? topConfig : null;
|
|
169
|
+
const quietSrc = cfg?.quiet ?? null;
|
|
170
|
+
const schedulesSrc = cfg?.schedules ?? null;
|
|
171
|
+
// Holidays: prefer an explicit ISO country code. Legacy boolean `true`
|
|
172
|
+
// from older setup UI builds is normalized to the local default country.
|
|
173
|
+
const hol = quietSrc?.holidays;
|
|
174
|
+
if (hol === true) {
|
|
175
|
+
this.holidayCountry = DEFAULT_HOLIDAY_COUNTRY;
|
|
176
|
+
} else if (typeof hol === "string" && hol) {
|
|
177
|
+
this.holidayCountry = hol.trim().toUpperCase();
|
|
178
|
+
} else {
|
|
179
|
+
this.holidayCountry = null;
|
|
180
|
+
}
|
|
181
|
+
// Quiet window string "HH:MM-HH:MM" from topConfig.quiet.schedule.
|
|
182
|
+
this.quietSchedule = quietSrc?.schedule ?? null;
|
|
183
|
+
// Opt-in flag: default true when unspecified, matching applyDefaults.
|
|
184
|
+
this.respectQuietSchedules = schedulesSrc?.respectQuiet !== false;
|
|
185
|
+
}
|
|
186
|
+
setInjectHandler(fn) {
|
|
187
|
+
this.injectFn = fn;
|
|
188
|
+
}
|
|
189
|
+
setSendHandler(fn) {
|
|
190
|
+
this.sendFn = fn;
|
|
191
|
+
}
|
|
192
|
+
setPendingCheck(fn) {
|
|
193
|
+
this.pendingCheck = typeof fn === 'function' ? fn : null;
|
|
194
|
+
}
|
|
195
|
+
noteActivity() {
|
|
196
|
+
this.lastActivity = Date.now();
|
|
197
|
+
}
|
|
198
|
+
/** Defer a schedule by N minutes from now */
|
|
199
|
+
defer(name, minutes) {
|
|
200
|
+
const mins = Number(minutes);
|
|
201
|
+
if (!Number.isFinite(mins) || mins <= 0) {
|
|
202
|
+
throw new Error(`defer: minutes must be a positive number, got ${JSON.stringify(minutes)}`);
|
|
203
|
+
}
|
|
204
|
+
const allSchedules = [...this.nonInteractive, ...this.interactive];
|
|
205
|
+
const exists = allSchedules.some(s => s.name === name);
|
|
206
|
+
if (!exists) throw new Error(`defer: unknown schedule "${name}" — use schedule_status to list valid names`);
|
|
207
|
+
this.deferred.set(name, Date.now() + mins * 6e4);
|
|
208
|
+
}
|
|
209
|
+
/** Skip a schedule for the rest of today */
|
|
210
|
+
skipToday(name) {
|
|
211
|
+
const allSchedules = [...this.nonInteractive, ...this.interactive];
|
|
212
|
+
const exists = allSchedules.some(s => s.name === name);
|
|
213
|
+
if (!exists) throw new Error(`skip_today: unknown schedule "${name}" — use schedule_status to list valid names`);
|
|
214
|
+
this.rolloverSkippedTodayIfNeeded();
|
|
215
|
+
this.skippedToday.add(name);
|
|
216
|
+
}
|
|
217
|
+
/** Roll the skippedToday bucket over when the local date has changed */
|
|
218
|
+
rolloverSkippedTodayIfNeeded() {
|
|
219
|
+
const today = new Date().toLocaleDateString('sv-SE');
|
|
220
|
+
if (this.skippedTodayDate !== today) {
|
|
221
|
+
this.skippedToday.clear();
|
|
222
|
+
this.skippedTodayDate = today;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Check if a schedule should be skipped (deferred or skipped today) */
|
|
226
|
+
shouldSkip(name) {
|
|
227
|
+
this.rolloverSkippedTodayIfNeeded();
|
|
228
|
+
if (this.skippedToday.has(name)) return true;
|
|
229
|
+
const until = this.deferred.get(name);
|
|
230
|
+
if (until && Date.now() < until) return true;
|
|
231
|
+
if (until && Date.now() >= until) this.deferred.delete(name);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/** Get current session activity state.
|
|
235
|
+
* Returns { lastActivityMs, pendingWork } — callers apply their own
|
|
236
|
+
* thresholds. pendingWork is true when pendingCheck() reports work in
|
|
237
|
+
* flight. lastActivityMs is 0 when no activity has been recorded. */
|
|
238
|
+
getSessionState() {
|
|
239
|
+
let pendingWork = false;
|
|
240
|
+
try {
|
|
241
|
+
if (this.pendingCheck) pendingWork = !!this.pendingCheck();
|
|
242
|
+
} catch { /* probe failure is not fatal */ }
|
|
243
|
+
return { lastActivityMs: this.lastActivity, pendingWork };
|
|
244
|
+
}
|
|
245
|
+
/** Returns true when the session is considered idle (no pending work and
|
|
246
|
+
* lastActivityMs is either 0 or older than the given threshold).
|
|
247
|
+
* threshold defaults to 15 minutes but callers should pass their own. */
|
|
248
|
+
isSessionIdle(thresholdMs = 15 * 6e4) {
|
|
249
|
+
const { lastActivityMs, pendingWork } = this.getSessionState();
|
|
250
|
+
if (pendingWork) return false;
|
|
251
|
+
if (lastActivityMs === 0) return true;
|
|
252
|
+
return Date.now() - lastActivityMs >= thresholdMs;
|
|
253
|
+
}
|
|
254
|
+
/** Get time context for prompt enrichment */
|
|
255
|
+
getTimeContext() {
|
|
256
|
+
const now = /* @__PURE__ */ new Date();
|
|
257
|
+
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
258
|
+
const dow = now.getDay();
|
|
259
|
+
return {
|
|
260
|
+
hour: now.getHours(),
|
|
261
|
+
dayOfWeek: days[dow],
|
|
262
|
+
isWeekend: dow === 0 || dow === 6
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Wrap prompt with session context metadata */
|
|
266
|
+
wrapPrompt(name, prompt, type) {
|
|
267
|
+
const { lastActivityMs, pendingWork } = this.getSessionState();
|
|
268
|
+
const state = pendingWork ? "active" : lastActivityMs === 0 ? "idle" : "recent";
|
|
269
|
+
const time = this.getTimeContext();
|
|
270
|
+
const header = [
|
|
271
|
+
`[schedule: ${name} | type: ${type} | session: ${state}]`,
|
|
272
|
+
`[time: ${time.dayOfWeek} ${String(time.hour).padStart(2, "0")}:${String((/* @__PURE__ */ new Date()).getMinutes()).padStart(2, "0")} | weekend: ${time.isWeekend}]`,
|
|
273
|
+
`Before starting any work, briefly tell the user what you're about to do in one short sentence.`
|
|
274
|
+
].join("\n");
|
|
275
|
+
return `${header}
|
|
276
|
+
|
|
277
|
+
${prompt}`;
|
|
278
|
+
}
|
|
279
|
+
static SCHEDULER_LOCK = join(tmpdir(), "mixdog-scheduler.lock");
|
|
280
|
+
static INSTANCE_UUID = randomUUID();
|
|
281
|
+
start() {
|
|
282
|
+
if (this.tickTimer) return;
|
|
283
|
+
const total = this.nonInteractive.length + this.interactive.length;
|
|
284
|
+
if (total === 0) {
|
|
285
|
+
process.stderr.write("mixdog scheduler: no schedules configured\n");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
ensureNopluginDir();
|
|
289
|
+
const lockContent = `${process.pid}
|
|
290
|
+
${Date.now()}
|
|
291
|
+
${Scheduler.INSTANCE_UUID}`;
|
|
292
|
+
let acquiredSchedulerLock = false;
|
|
293
|
+
withFileLockSync(`${Scheduler.SCHEDULER_LOCK}.acquire`, () => {
|
|
294
|
+
try {
|
|
295
|
+
writeFileSync(Scheduler.SCHEDULER_LOCK, lockContent, { flag: "wx" });
|
|
296
|
+
acquiredSchedulerLock = true;
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (err.code === "EEXIST") {
|
|
299
|
+
try {
|
|
300
|
+
const content = readFileSync(Scheduler.SCHEDULER_LOCK, "utf8");
|
|
301
|
+
const lines = content.split("\n");
|
|
302
|
+
const pid = parseInt(lines[0]);
|
|
303
|
+
let isAlive = false;
|
|
304
|
+
try {
|
|
305
|
+
process.kill(pid, 0);
|
|
306
|
+
isAlive = true;
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
if (isAlive) {
|
|
310
|
+
// No heartbeat: lock age cannot distinguish a long-running
|
|
311
|
+
// healthy owner from PID-reuse, so an age-only reclaim
|
|
312
|
+
// would double-schedule cron jobs while the original
|
|
313
|
+
// owner is still firing. Only proceed to reclaim when
|
|
314
|
+
// process.kill(pid, 0) actually proves the PID is dead —
|
|
315
|
+
// not by guessing from `lockAge > 1h`.
|
|
316
|
+
process.stderr.write(`mixdog scheduler: another session (PID ${pid}) owns the scheduler, skipping
|
|
317
|
+
`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
// Reclaim runs under the shared atomic-file acquisition guard.
|
|
323
|
+
// That guard serializes this scheduler acquisition path's
|
|
324
|
+
// check/unlink/wx sequence, so a second reclaimer cannot delete
|
|
325
|
+
// a fresh lock in the path gap between stale unlink and create.
|
|
326
|
+
try { unlinkSync(Scheduler.SCHEDULER_LOCK); } catch {}
|
|
327
|
+
try {
|
|
328
|
+
writeFileSync(Scheduler.SCHEDULER_LOCK, lockContent, { flag: "wx" });
|
|
329
|
+
acquiredSchedulerLock = true;
|
|
330
|
+
} catch (e2) {
|
|
331
|
+
if (e2.code === "EEXIST") {
|
|
332
|
+
process.stderr.write(`mixdog scheduler: lock reclaimed by another session during reclaim, skipping
|
|
333
|
+
`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
throw e2;
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}, { timeoutMs: 60000, staleMs: 30000 });
|
|
343
|
+
if (!acquiredSchedulerLock) return;
|
|
344
|
+
process.on("exit", () => {
|
|
345
|
+
// Verify ownership before unlink: an exiting process whose lock
|
|
346
|
+
// was already reclaimed by a newer owner (PID-reuse / restart race)
|
|
347
|
+
// must NOT delete the new owner's lock file. Read-verify-then-unlink
|
|
348
|
+
// mirrors memory/index.mjs releaseLock().
|
|
349
|
+
try {
|
|
350
|
+
const content = readFileSync(Scheduler.SCHEDULER_LOCK, "utf8");
|
|
351
|
+
const lockedPid = parseInt(content.split("\n")[0]);
|
|
352
|
+
if (lockedPid === process.pid) unlinkSync(Scheduler.SCHEDULER_LOCK);
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
logSchedule(`${this.nonInteractive.length} non-interactive, ${this.interactive.length} interactive
|
|
357
|
+
`);
|
|
358
|
+
this.registerCronJobs();
|
|
359
|
+
this.tick();
|
|
360
|
+
this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
|
|
361
|
+
}
|
|
362
|
+
/** Register cron-expression entries with node-cron. All schedule entries
|
|
363
|
+
* must use cron expressions. Entries that fail cron validation are skipped
|
|
364
|
+
* with a logged error. */
|
|
365
|
+
registerCronJobs() {
|
|
366
|
+
const all = [
|
|
367
|
+
...this.nonInteractive.map((s) => ({ schedule: s, type: "non-interactive" })),
|
|
368
|
+
...this.interactive.map((s) => ({ schedule: s, type: "interactive" })),
|
|
369
|
+
];
|
|
370
|
+
for (const { schedule: s, type } of all) {
|
|
371
|
+
if (!isCronExpression(s.time)) continue;
|
|
372
|
+
try {
|
|
373
|
+
const task = cron.schedule(s.time, () => this.onCronFire(s, type), {
|
|
374
|
+
timezone: s.timezone || undefined,
|
|
375
|
+
name: s.name,
|
|
376
|
+
});
|
|
377
|
+
this.cronJobs.set(s.name, task);
|
|
378
|
+
logSchedule(`registered cron "${s.name}" = "${s.time}"${s.timezone ? ` tz=${s.timezone}` : ""}\n`);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
process.stderr.write(`mixdog scheduler: failed to register cron "${s.name}" (${s.time}): ${err}\n`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Fire path for a cron-triggered entry. Applies day/quiet/holiday
|
|
385
|
+
* guards against the schedule's TZ (or local when absent). */
|
|
386
|
+
async onCronFire(schedule, type) {
|
|
387
|
+
const now = /* @__PURE__ */ new Date();
|
|
388
|
+
const tz = schedule.timezone || null;
|
|
389
|
+
const snap = tzSnapshot(now, tz);
|
|
390
|
+
const isWeekend = snap.dow === 0 || snap.dow === 6;
|
|
391
|
+
const days = schedule.days ?? "daily";
|
|
392
|
+
if (!this.matchesDays(days, snap.dow, isWeekend)) return;
|
|
393
|
+
if (this.holidayCountry) {
|
|
394
|
+
try {
|
|
395
|
+
const holiday = await isHoliday(this.tzDate(now, tz), this.holidayCountry);
|
|
396
|
+
if (holiday && (schedule.skipHolidays || days === "weekday")) {
|
|
397
|
+
logSchedule(`skipping "${schedule.name}" \u2014 public holiday\n`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
} catch {}
|
|
401
|
+
}
|
|
402
|
+
if ((schedule.dnd || this.respectQuietSchedules) && this.isQuietHours(now, tz)) return;
|
|
403
|
+
if (this.shouldSkip(schedule.name)) return;
|
|
404
|
+
// Record lastFired only when the fire actually proceeds past
|
|
405
|
+
// fireTimed's running/precondition guards (it resolves truthy on a
|
|
406
|
+
// real fire), so failed/skipped fires no longer display as fired.
|
|
407
|
+
this.fireTimed(schedule, type).then(
|
|
408
|
+
(fired) => { if (fired) this.lastFired.set(schedule.name, now.toISOString()); }
|
|
409
|
+
).catch(
|
|
410
|
+
(err) => process.stderr.write(`mixdog scheduler: ${schedule.name} failed: ${err}\n`)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
/** Produce a Date whose calendar day matches the TZ-adjusted dateStr,
|
|
414
|
+
* so holiday lookups by country work against the right day. */
|
|
415
|
+
tzDate(now, tz) {
|
|
416
|
+
if (!tz) return now;
|
|
417
|
+
const snap = tzSnapshot(now, tz);
|
|
418
|
+
return new Date(`${snap.dateStr}T12:00:00Z`);
|
|
419
|
+
}
|
|
420
|
+
stop() {
|
|
421
|
+
if (this.tickTimer) {
|
|
422
|
+
clearInterval(this.tickTimer);
|
|
423
|
+
this.tickTimer = null;
|
|
424
|
+
}
|
|
425
|
+
this.destroyCronJobs();
|
|
426
|
+
// Release the scheduler lock so a subsequent start() in the same
|
|
427
|
+
// process can re-acquire it. Without this, the wx-create in start()
|
|
428
|
+
// hits its own live lock (matching INSTANCE_UUID + recent mtime) and
|
|
429
|
+
// refuses to register cron jobs, leaving the scheduler silently idle
|
|
430
|
+
// after a reload/restart cycle. Read-verify-then-unlink so we don't
|
|
431
|
+
// delete another live owner's lock file (mirrors memory releaseLock).
|
|
432
|
+
try {
|
|
433
|
+
const content = readFileSync(Scheduler.SCHEDULER_LOCK, "utf8");
|
|
434
|
+
const lockedPid = parseInt(content.split("\n")[0]);
|
|
435
|
+
if (lockedPid === process.pid) unlinkSync(Scheduler.SCHEDULER_LOCK);
|
|
436
|
+
} catch {}
|
|
437
|
+
}
|
|
438
|
+
destroyCronJobs() {
|
|
439
|
+
for (const [, task] of this.cronJobs) {
|
|
440
|
+
try { task.destroy(); } catch {}
|
|
441
|
+
}
|
|
442
|
+
this.cronJobs.clear();
|
|
443
|
+
}
|
|
444
|
+
restart() {
|
|
445
|
+
if (this.tickTimer) {
|
|
446
|
+
clearInterval(this.tickTimer);
|
|
447
|
+
this.tickTimer = null;
|
|
448
|
+
}
|
|
449
|
+
this.destroyCronJobs();
|
|
450
|
+
// Read-verify-then-unlink so a non-owner reload can't delete the
|
|
451
|
+
// live owner's lock file (mirrors stop() and the exit handler).
|
|
452
|
+
try {
|
|
453
|
+
const content = readFileSync(Scheduler.SCHEDULER_LOCK, "utf8");
|
|
454
|
+
const lockedPid = parseInt(content.split("\n")[0]);
|
|
455
|
+
if (lockedPid === process.pid) unlinkSync(Scheduler.SCHEDULER_LOCK);
|
|
456
|
+
} catch {}
|
|
457
|
+
this.start();
|
|
458
|
+
}
|
|
459
|
+
reloadConfig(nonInteractive, interactive, channelsConfig, topConfig, options = {}) {
|
|
460
|
+
this.nonInteractive = nonInteractive.filter((s) => s.enabled !== false);
|
|
461
|
+
this.interactive = interactive.filter((s) => s.enabled !== false);
|
|
462
|
+
this.channelsConfig = channelsConfig ?? null;
|
|
463
|
+
this.promptsDir = join(DATA_DIR, "prompts");
|
|
464
|
+
this._applyQuietConfig(topConfig);
|
|
465
|
+
this.holidayChecked = "";
|
|
466
|
+
this.todayIsHoliday = false;
|
|
467
|
+
if (this.deferred.size > 0 || this.skippedToday.size > 0) {
|
|
468
|
+
process.stderr.write(`mixdog scheduler: reload clearing ${this.deferred.size} deferred, ${this.skippedToday.size} skipped
|
|
469
|
+
`);
|
|
470
|
+
}
|
|
471
|
+
this.deferred.clear();
|
|
472
|
+
this.skippedToday.clear();
|
|
473
|
+
if (options.restart === false) {
|
|
474
|
+
// Caller owns lifecycle; still drop stale cron bindings so they don't fire against old config.
|
|
475
|
+
this.destroyCronJobs();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
this.restart();
|
|
479
|
+
}
|
|
480
|
+
getStatus() {
|
|
481
|
+
const result = [];
|
|
482
|
+
for (const s of this.nonInteractive) {
|
|
483
|
+
result.push({
|
|
484
|
+
name: s.name,
|
|
485
|
+
time: s.time,
|
|
486
|
+
days: s.days ?? "daily",
|
|
487
|
+
type: "non-interactive",
|
|
488
|
+
running: false,
|
|
489
|
+
lastFired: this.lastFired.get(s.name) ?? null
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
for (const s of this.interactive) {
|
|
493
|
+
result.push({
|
|
494
|
+
name: s.name,
|
|
495
|
+
time: s.time,
|
|
496
|
+
days: s.days ?? "daily",
|
|
497
|
+
type: "interactive",
|
|
498
|
+
running: false,
|
|
499
|
+
lastFired: this.lastFired.get(s.name) ?? null
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
async triggerManual(name) {
|
|
505
|
+
const timed = [...this.nonInteractive, ...this.interactive].find((e) => e.name === name);
|
|
506
|
+
if (timed) {
|
|
507
|
+
if (this.running.has(name)) return `"${name}" is already running`;
|
|
508
|
+
const isNonInteractive = this.nonInteractive.includes(timed);
|
|
509
|
+
const now = /* @__PURE__ */ new Date();
|
|
510
|
+
const hhmm = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
511
|
+
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
512
|
+
// Match onCronFire: record lastFired only when fireTimed actually
|
|
513
|
+
// proceeds past its running/precondition guards (resolves truthy),
|
|
514
|
+
// and reflect a non-fire in the returned status.
|
|
515
|
+
const fired = await this.fireTimed(timed, isNonInteractive ? "non-interactive" : "interactive");
|
|
516
|
+
if (fired) {
|
|
517
|
+
this.lastFired.set(name, `${dateStr}T${hhmm}`);
|
|
518
|
+
return `triggered "${name}"`;
|
|
519
|
+
}
|
|
520
|
+
return `"${name}" did not fire (skipped or already running)`;
|
|
521
|
+
}
|
|
522
|
+
return `schedule "${name}" not found`;
|
|
523
|
+
}
|
|
524
|
+
// ── Tick ─────────────────────────────────────────────────────────────
|
|
525
|
+
tick() {
|
|
526
|
+
this.tickAsync().catch(
|
|
527
|
+
(err) => process.stderr.write(`mixdog scheduler: tick error: ${err}
|
|
528
|
+
`)
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
async tickAsync() {
|
|
532
|
+
const now = /* @__PURE__ */ new Date();
|
|
533
|
+
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
534
|
+
// All timed schedules are now handled exclusively by node-cron (registerCronJobs).
|
|
535
|
+
// tick() only drives the holiday cache refresh.
|
|
536
|
+
if (this.holidayCountry && this.holidayChecked !== dateStr) {
|
|
537
|
+
this.holidayChecked = dateStr;
|
|
538
|
+
try {
|
|
539
|
+
this.todayIsHoliday = await isHoliday(now, this.holidayCountry);
|
|
540
|
+
if (this.todayIsHoliday) {
|
|
541
|
+
process.stderr.write(`mixdog scheduler: today (${dateStr}) is a holiday \u2014 weekday schedules will be skipped
|
|
542
|
+
`);
|
|
543
|
+
}
|
|
544
|
+
} catch (err) {
|
|
545
|
+
process.stderr.write(`mixdog scheduler: holiday check failed: ${err}
|
|
546
|
+
`);
|
|
547
|
+
this.todayIsHoliday = false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/** Day abbreviation → JS day number (0=Sun...6=Sat) */
|
|
552
|
+
static DAY_ABBRS = {
|
|
553
|
+
sun: 0,
|
|
554
|
+
mon: 1,
|
|
555
|
+
tue: 2,
|
|
556
|
+
wed: 3,
|
|
557
|
+
thu: 4,
|
|
558
|
+
fri: 5,
|
|
559
|
+
sat: 6
|
|
560
|
+
};
|
|
561
|
+
/** Check if today matches the schedule's days setting */
|
|
562
|
+
matchesDays(days, dow, isWeekend) {
|
|
563
|
+
if (days === "daily") return true;
|
|
564
|
+
if (days === "weekday") return !isWeekend;
|
|
565
|
+
if (days === "weekend") return isWeekend;
|
|
566
|
+
const dayList = days.split(",").map((d) => d.trim().toLowerCase());
|
|
567
|
+
return dayList.some((d) => Scheduler.DAY_ABBRS[d] === dow);
|
|
568
|
+
}
|
|
569
|
+
/** Check if current time is within global quiet hours (quiet.schedule).
|
|
570
|
+
* tz optional — when set, HH:MM is evaluated in the given IANA zone.
|
|
571
|
+
*
|
|
572
|
+
* Delegates to the shared, now TZ-aware isInQuietWindow(cfg, now, tz)
|
|
573
|
+
* helper in lib/config.mjs. Holidays are passed as `false` here on
|
|
574
|
+
* purpose: public-holiday skips for the scheduler are handled by the
|
|
575
|
+
* separate holidayCountry / skipHolidays / days==="weekday" path in
|
|
576
|
+
* onCronFire, so this quiet-window check stays schedule-window only —
|
|
577
|
+
* identical to the prior local implementation. */
|
|
578
|
+
isQuietHours(now, tz) {
|
|
579
|
+
return isInQuietWindow({ schedule: this.quietSchedule, holidays: false }, now, tz);
|
|
580
|
+
}
|
|
581
|
+
// ── Fire timed schedule ─────────────────────────────────────────────
|
|
582
|
+
async fireTimed(schedule, type) {
|
|
583
|
+
const execMode = schedule.exec ?? "prompt";
|
|
584
|
+
if (execMode === "script" || execMode === "script+prompt") {
|
|
585
|
+
if (!schedule.script) {
|
|
586
|
+
process.stderr.write(`mixdog scheduler: no script specified for "${schedule.name}"
|
|
587
|
+
`);
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (this.running.has(schedule.name)) return false;
|
|
591
|
+
this.running.add(schedule.name);
|
|
592
|
+
const channelId2 = this.resolveChannel(schedule.channel);
|
|
593
|
+
logSchedule(`firing ${schedule.name} (${type}, exec=${execMode})
|
|
594
|
+
`);
|
|
595
|
+
try {
|
|
596
|
+
const scriptResult = await this.runScript(schedule.script);
|
|
597
|
+
if (execMode === "script") {
|
|
598
|
+
this.running.delete(schedule.name);
|
|
599
|
+
if (scriptResult && this.sendFn) {
|
|
600
|
+
await this.sendFn(channelId2, scriptResult).catch(
|
|
601
|
+
(err) => process.stderr.write(`mixdog scheduler: ${schedule.name} relay failed: ${err}
|
|
602
|
+
`)
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
process.stderr.write(`mixdog scheduler: ${schedule.name} script done
|
|
606
|
+
`);
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
const prompt2 = this.loadPrompt(schedule.prompt ?? `${schedule.name}.md`);
|
|
610
|
+
if (!prompt2) {
|
|
611
|
+
this.running.delete(schedule.name);
|
|
612
|
+
process.stderr.write(`mixdog scheduler: prompt not found for "${schedule.name}"
|
|
613
|
+
`);
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
const combinedPrompt = `${prompt2}
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
## Script Output
|
|
620
|
+
\`\`\`
|
|
621
|
+
${scriptResult}
|
|
622
|
+
\`\`\``;
|
|
623
|
+
this.running.delete(schedule.name);
|
|
624
|
+
return await this.fireTimedPrompt(schedule, type, combinedPrompt, channelId2);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
this.running.delete(schedule.name);
|
|
627
|
+
process.stderr.write(`mixdog scheduler: ${schedule.name} script error: ${err}
|
|
628
|
+
`);
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const prompt = this.resolvePrompt(schedule);
|
|
633
|
+
if (!prompt) {
|
|
634
|
+
process.stderr.write(`mixdog scheduler: prompt not found for "${schedule.name}"
|
|
635
|
+
`);
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
const channelId = this.resolveChannel(schedule.channel);
|
|
639
|
+
return await this.fireTimedPrompt(schedule, type, prompt, channelId);
|
|
640
|
+
}
|
|
641
|
+
/** Fire a timed schedule with the given prompt content */
|
|
642
|
+
async fireTimedPrompt(schedule, type, prompt, channelId) {
|
|
643
|
+
logSchedule(`firing ${schedule.name} (${type})
|
|
644
|
+
`);
|
|
645
|
+
if (type === "interactive") {
|
|
646
|
+
if (this.injectFn) {
|
|
647
|
+
this.injectFn(channelId, schedule.name, " ", {
|
|
648
|
+
instruction: prompt,
|
|
649
|
+
type: "schedule"
|
|
650
|
+
});
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
if (this.running.has(schedule.name)) return false;
|
|
656
|
+
this.running.add(schedule.name);
|
|
657
|
+
const presetId = schedule.model;
|
|
658
|
+
if (!presetId) {
|
|
659
|
+
this.running.delete(schedule.name);
|
|
660
|
+
logSchedule(`${schedule.name}: missing required "model" in schedule config — dispatch rejected\n`);
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
schedulerLlm({ prompt, preset: presetId, sourceName: schedule.name })
|
|
664
|
+
.then((result) => {
|
|
665
|
+
this.running.delete(schedule.name);
|
|
666
|
+
if (result && this.sendFn) {
|
|
667
|
+
this.sendFn(channelId, result).catch(
|
|
668
|
+
(err) => process.stderr.write(`mixdog scheduler: ${schedule.name} relay failed: ${err}\n`)
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
logSchedule(`${schedule.name} done\n`);
|
|
672
|
+
})
|
|
673
|
+
.catch((err) => {
|
|
674
|
+
this.running.delete(schedule.name);
|
|
675
|
+
logSchedule(`${schedule.name} LLM error: ${err.message}\n`);
|
|
676
|
+
});
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
// ── Script execution (delegates to shared executor) ────────────────
|
|
680
|
+
runScript(scriptName) {
|
|
681
|
+
return new Promise((resolve, reject) => {
|
|
682
|
+
execScript(`schedule:${scriptName}`, scriptName, (result, code) => {
|
|
683
|
+
if (code !== 0 && code !== null) {
|
|
684
|
+
reject(new Error(`script exited with code ${code}`));
|
|
685
|
+
} else {
|
|
686
|
+
resolve(result);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
692
|
+
/** Resolve a channel label to its platform ID via channelsConfig, fallback to raw value */
|
|
693
|
+
resolveChannel(label) {
|
|
694
|
+
const entry = this.channelsConfig?.[label];
|
|
695
|
+
if (entry?.channelId) return entry.channelId;
|
|
696
|
+
// Misconfigured label: no channelsConfig entry, so we fall back to
|
|
697
|
+
// using the raw label as the channel id. Warn once per label so the
|
|
698
|
+
// misconfiguration is diagnosable without spamming stderr.
|
|
699
|
+
if (label != null) {
|
|
700
|
+
this._channelFallbackWarned ??= new Set();
|
|
701
|
+
if (!this._channelFallbackWarned.has(label)) {
|
|
702
|
+
this._channelFallbackWarned.add(label);
|
|
703
|
+
process.stderr.write(`mixdog scheduler: channel label "${label}" not found in channelsConfig — using it as a raw channel id\n`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return label;
|
|
707
|
+
}
|
|
708
|
+
/** Resolve prompt: try file first, fall back to inline text */
|
|
709
|
+
resolvePrompt(schedule) {
|
|
710
|
+
const ref = schedule.prompt ?? `${schedule.name}.md`;
|
|
711
|
+
const fromFile = this.loadPrompt(ref);
|
|
712
|
+
if (fromFile) return fromFile;
|
|
713
|
+
if (schedule.prompt) return schedule.prompt;
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
loadPrompt(nameOrPath) {
|
|
717
|
+
const full = isAbsolute(nameOrPath) ? nameOrPath : join(this.promptsDir, nameOrPath);
|
|
718
|
+
return tryRead(full);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
export {
|
|
722
|
+
Scheduler
|
|
723
|
+
};
|