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.
Files changed (404) hide show
  1. package/.claude-plugin/marketplace.json +31 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.gitattributes +34 -0
  4. package/.mcp.json +14 -0
  5. package/ARCHITECTURE.md +77 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CONTRIBUTING.md +45 -0
  8. package/DATA-FLOW.md +79 -0
  9. package/LICENSE +21 -0
  10. package/README.md +389 -0
  11. package/SECURITY.md +138 -0
  12. package/UNINSTALL.md +112 -0
  13. package/agents/maintenance.md +5 -0
  14. package/agents/memory-classification.md +30 -0
  15. package/agents/scheduler-task.md +18 -0
  16. package/agents/webhook-handler.md +27 -0
  17. package/agents/worker.md +24 -0
  18. package/bin/bridge +133 -0
  19. package/bin/statusline-launcher.mjs +78 -0
  20. package/bin/statusline-lib.mjs +550 -0
  21. package/bin/statusline.mjs +607 -0
  22. package/bun.lock +802 -0
  23. package/commands/config.md +16 -0
  24. package/commands/doctor.md +13 -0
  25. package/commands/setup.md +17 -0
  26. package/defaults/cycle3-review-prompt.md +90 -0
  27. package/defaults/hidden-roles.json +65 -0
  28. package/defaults/memory-chunk-prompt.md +63 -0
  29. package/defaults/memory-promote-prompt.md +135 -0
  30. package/defaults/mixdog-config.template.json +27 -0
  31. package/defaults/user-workflow.json +8 -0
  32. package/defaults/user-workflow.md +12 -0
  33. package/hooks/hooks.json +73 -0
  34. package/hooks/lib/active-instance.cjs +77 -0
  35. package/hooks/lib/permission-evaluator.cjs +411 -0
  36. package/hooks/lib/permission-route.cjs +63 -0
  37. package/hooks/lib/permission-rules.cjs +170 -0
  38. package/hooks/lib/settings-loader.cjs +116 -0
  39. package/hooks/post-tool-use.cjs +84 -0
  40. package/hooks/pre-mcp-sandbox.cjs +158 -0
  41. package/hooks/pre-tool-subagent.cjs +253 -0
  42. package/hooks/session-start.cjs +1372 -0
  43. package/hooks/turn-timer.cjs +82 -0
  44. package/lib/claude-md-writer.cjs +386 -0
  45. package/lib/config-cjs.cjs +61 -0
  46. package/lib/hook-pipe-path.cjs +10 -0
  47. package/lib/keychain-cjs.cjs +263 -0
  48. package/lib/plugin-paths.cjs +61 -0
  49. package/lib/rules-builder.cjs +241 -0
  50. package/lib/text-utils.cjs +61 -0
  51. package/native/README.md +117 -0
  52. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  53. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  54. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  55. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  56. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  57. package/package.json +107 -0
  58. package/prompts/code-review.txt +16 -0
  59. package/prompts/security-audit.txt +17 -0
  60. package/rules/bridge/00-common.md +39 -0
  61. package/rules/bridge/20-skip-protocol.md +18 -0
  62. package/rules/bridge/30-explorer.md +33 -0
  63. package/rules/bridge/40-cycle1-agent.md +52 -0
  64. package/rules/bridge/41-cycle2-agent.md +62 -0
  65. package/rules/bridge/42-cycle3-agent.md +44 -0
  66. package/rules/lead/00-tool-lead.md +61 -0
  67. package/rules/lead/01-general.md +23 -0
  68. package/rules/lead/02-channels.md +49 -0
  69. package/rules/lead/03-team.md +27 -0
  70. package/rules/lead/04-workflow.md +20 -0
  71. package/rules/shared/00-language.md +14 -0
  72. package/rules/shared/01-tool.md +138 -0
  73. package/scripts/bootstrap.mjs +184 -0
  74. package/scripts/bridge-unify-smoke.mjs +308 -0
  75. package/scripts/build-runtime-linux.sh +348 -0
  76. package/scripts/build-runtime-macos.sh +217 -0
  77. package/scripts/build-runtime-windows.ps1 +242 -0
  78. package/scripts/builtin-utils-smoke.mjs +392 -0
  79. package/scripts/check-json.mjs +45 -0
  80. package/scripts/check-syntax-changed.mjs +102 -0
  81. package/scripts/check-syntax.mjs +58 -0
  82. package/scripts/code-graph-batch.test.mjs +33 -0
  83. package/scripts/config-preserve-smoke.mjs +180 -0
  84. package/scripts/doctor.mjs +484 -0
  85. package/scripts/edit-normalize-fuzz.mjs +130 -0
  86. package/scripts/edit-normalize-smoke.mjs +401 -0
  87. package/scripts/edit-operation-smoke.mjs +369 -0
  88. package/scripts/edit2-smoke.mjs +63 -0
  89. package/scripts/fuzzy-e2e.mjs +28 -0
  90. package/scripts/fuzzy-smoke.mjs +26 -0
  91. package/scripts/generate-runtime-manifest.mjs +166 -0
  92. package/scripts/guard-smoke.mjs +66 -0
  93. package/scripts/hidden-role-schema-smoke.mjs +162 -0
  94. package/scripts/hook-routing-smoke.mjs +29 -0
  95. package/scripts/inject-input.ps1 +204 -0
  96. package/scripts/io-complex-smoke.mjs +667 -0
  97. package/scripts/io-explore-bench.mjs +424 -0
  98. package/scripts/io-guardrails-smoke.mjs +205 -0
  99. package/scripts/io-mini-bench-baseline.json +11 -0
  100. package/scripts/io-mini-bench.mjs +216 -0
  101. package/scripts/io-route-harness.mjs +933 -0
  102. package/scripts/io-telemetry-report.mjs +691 -0
  103. package/scripts/mutation-bench.mjs +564 -0
  104. package/scripts/mutation-io-smoke.mjs +1081 -0
  105. package/scripts/native-patch-bridge-smoke.mjs +288 -0
  106. package/scripts/native-patch-smoke.mjs +304 -0
  107. package/scripts/patch-interior-context-smoke.mjs +49 -0
  108. package/scripts/patch-newline-utf8-smoke.mjs +157 -0
  109. package/scripts/perf-hook-smoke.mjs +71 -0
  110. package/scripts/permission-eval-smoke.mjs +426 -0
  111. package/scripts/prep-patch.mjs +53 -0
  112. package/scripts/prep-shim.mjs +96 -0
  113. package/scripts/provider-cache-smoke.mjs +687 -0
  114. package/scripts/report-runtime-health.mjs +132 -0
  115. package/scripts/run-mcp.mjs +1547 -0
  116. package/scripts/salvage-v4a-shatter.test.mjs +58 -0
  117. package/scripts/scoped-cache-io-smoke.mjs +103 -0
  118. package/scripts/shell-policy-round3-smoke.mjs +46 -0
  119. package/scripts/smoke-runtime-negative.ps1 +100 -0
  120. package/scripts/smoke-runtime-negative.sh +95 -0
  121. package/scripts/stall-policy-smoke.mjs +50 -0
  122. package/scripts/start-memory-worker.mjs +23 -0
  123. package/scripts/statusline-launcher-smoke.mjs +82 -0
  124. package/scripts/stress-atomic-write.mjs +1028 -0
  125. package/scripts/test-config-rmw-restore.mjs +122 -0
  126. package/scripts/test-fault-inject.mjs +164 -0
  127. package/scripts/test-large-file.mjs +174 -0
  128. package/scripts/tool-edge-smoke.mjs +209 -0
  129. package/scripts/uninstall.mjs +201 -0
  130. package/scripts/webhook-selfheal-smoke.mjs +29 -0
  131. package/scripts/write-overwrite-guard-smoke.mjs +56 -0
  132. package/server-main.mjs +3055 -0
  133. package/server.mjs +468 -0
  134. package/setup/config-merge.mjs +254 -0
  135. package/setup/install.mjs +120 -0
  136. package/setup/launch-core.mjs +507 -0
  137. package/setup/launch.mjs +101 -0
  138. package/setup/setup-server.mjs +3206 -0
  139. package/setup/setup.html +3693 -0
  140. package/skills/retro-skill-proposer/SKILL.md +92 -0
  141. package/skills/schedule-add/SKILL.md +77 -0
  142. package/skills/setup/SKILL.md +346 -0
  143. package/skills/webhook-add/SKILL.md +81 -0
  144. package/src/agent/bridge-stall-watchdog.mjs +337 -0
  145. package/src/agent/index.mjs +2138 -0
  146. package/src/agent/orchestrator/activity-bus.mjs +38 -0
  147. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
  148. package/src/agent/orchestrator/bridge-retry.mjs +220 -0
  149. package/src/agent/orchestrator/bridge-trace.mjs +583 -0
  150. package/src/agent/orchestrator/cache-mtime.mjs +58 -0
  151. package/src/agent/orchestrator/config.mjs +358 -0
  152. package/src/agent/orchestrator/context/collect.mjs +651 -0
  153. package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
  154. package/src/agent/orchestrator/drain-registry.mjs +50 -0
  155. package/src/agent/orchestrator/explore-validator.mjs +8 -0
  156. package/src/agent/orchestrator/internal-roles.mjs +118 -0
  157. package/src/agent/orchestrator/internal-tools.mjs +88 -0
  158. package/src/agent/orchestrator/jobs.mjs +116 -0
  159. package/src/agent/orchestrator/mcp/client.mjs +364 -0
  160. package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
  161. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
  162. package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
  163. package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
  164. package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
  165. package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
  166. package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
  167. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
  168. package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
  169. package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
  170. package/src/agent/orchestrator/providers/registry.mjs +192 -0
  171. package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
  172. package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
  173. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  174. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  175. package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
  176. package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
  177. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
  178. package/src/agent/orchestrator/session/cache/util.mjs +49 -0
  179. package/src/agent/orchestrator/session/loop.mjs +1478 -0
  180. package/src/agent/orchestrator/session/manager.mjs +1975 -0
  181. package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
  182. package/src/agent/orchestrator/session/result-classification.mjs +65 -0
  183. package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
  184. package/src/agent/orchestrator/session/store.mjs +624 -0
  185. package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
  186. package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  187. package/src/agent/orchestrator/session/trim.mjs +491 -0
  188. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
  189. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
  190. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
  191. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
  192. package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
  193. package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
  194. package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
  195. package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
  196. package/src/agent/orchestrator/stall-policy.mjs +195 -0
  197. package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
  198. package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
  199. package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
  200. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  201. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
  202. package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
  203. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
  204. package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
  205. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
  206. package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
  207. package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
  208. package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
  209. package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
  210. package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
  211. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
  212. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
  213. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
  214. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
  215. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
  216. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
  217. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
  218. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
  219. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
  220. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
  221. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
  222. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
  223. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
  224. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
  225. package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
  226. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
  227. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
  228. package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
  229. package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
  230. package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
  231. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
  232. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
  233. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
  234. package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
  235. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
  236. package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
  237. package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
  238. package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
  239. package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
  240. package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
  241. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  242. package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
  243. package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
  244. package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
  245. package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
  246. package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
  247. package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
  248. package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
  249. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
  250. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
  251. package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
  252. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  253. package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
  254. package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
  255. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
  256. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
  257. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
  258. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
  259. package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
  260. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
  261. package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
  262. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
  263. package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
  264. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
  265. package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
  266. package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
  267. package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
  268. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
  269. package/src/agent/orchestrator/tools/builtin.mjs +500 -0
  270. package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
  271. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  272. package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
  273. package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
  274. package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  275. package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
  276. package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
  277. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
  278. package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
  279. package/src/agent/orchestrator/tools/host-input.mjs +204 -0
  280. package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
  281. package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
  282. package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
  283. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
  284. package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
  285. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
  286. package/src/agent/orchestrator/tools/patch.mjs +2754 -0
  287. package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
  288. package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
  289. package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
  290. package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
  291. package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
  292. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
  293. package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
  294. package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
  295. package/src/agent/orchestrator/workflow-store.mjs +93 -0
  296. package/src/agent/tool-defs.mjs +103 -0
  297. package/src/channels/backends/discord.mjs +784 -0
  298. package/src/channels/data/voice-runtime-manifest.json +138 -0
  299. package/src/channels/index.mjs +3229 -0
  300. package/src/channels/lib/cli-worker-host.mjs +12 -0
  301. package/src/channels/lib/config-lock.mjs +13 -0
  302. package/src/channels/lib/config.mjs +292 -0
  303. package/src/channels/lib/drop-trace.mjs +71 -0
  304. package/src/channels/lib/event-pipeline.mjs +81 -0
  305. package/src/channels/lib/event-queue.mjs +345 -0
  306. package/src/channels/lib/executor.mjs +168 -0
  307. package/src/channels/lib/format.mjs +188 -0
  308. package/src/channels/lib/holidays.mjs +138 -0
  309. package/src/channels/lib/hook-pipe-server.mjs +802 -0
  310. package/src/channels/lib/interaction-workflows.mjs +184 -0
  311. package/src/channels/lib/memory-client.mjs +149 -0
  312. package/src/channels/lib/output-forwarder.mjs +765 -0
  313. package/src/channels/lib/runtime-paths.mjs +479 -0
  314. package/src/channels/lib/scheduler.mjs +723 -0
  315. package/src/channels/lib/session-control.mjs +36 -0
  316. package/src/channels/lib/session-discovery.mjs +103 -0
  317. package/src/channels/lib/settings.mjs +11 -0
  318. package/src/channels/lib/state-file.mjs +68 -0
  319. package/src/channels/lib/status-snapshot.mjs +219 -0
  320. package/src/channels/lib/tool-format.mjs +140 -0
  321. package/src/channels/lib/transcript-discovery.mjs +195 -0
  322. package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
  323. package/src/channels/lib/webhook.mjs +1179 -0
  324. package/src/channels/lib/whisper-server.mjs +477 -0
  325. package/src/channels/tool-defs.mjs +170 -0
  326. package/src/daemon/host.mjs +118 -0
  327. package/src/daemon/mcp-transport.mjs +47 -0
  328. package/src/daemon/session.mjs +100 -0
  329. package/src/daemon/thin-client.mjs +71 -0
  330. package/src/daemon/transport.mjs +163 -0
  331. package/src/memory/data/runtime-manifest.json +40 -0
  332. package/src/memory/index.mjs +3305 -0
  333. package/src/memory/lib/agent-ipc.mjs +93 -0
  334. package/src/memory/lib/bridge-trace-queries.mjs +120 -0
  335. package/src/memory/lib/core-memory-store.mjs +330 -0
  336. package/src/memory/lib/embedding-provider.mjs +269 -0
  337. package/src/memory/lib/embedding-worker.mjs +323 -0
  338. package/src/memory/lib/llm-worker-host.mjs +17 -0
  339. package/src/memory/lib/memory-cycle.mjs +11 -0
  340. package/src/memory/lib/memory-cycle1.mjs +641 -0
  341. package/src/memory/lib/memory-cycle2.mjs +1284 -0
  342. package/src/memory/lib/memory-cycle3.mjs +540 -0
  343. package/src/memory/lib/memory-embed.mjs +299 -0
  344. package/src/memory/lib/memory-extraction.mjs +5 -0
  345. package/src/memory/lib/memory-maintenance-store.mjs +32 -0
  346. package/src/memory/lib/memory-ops-policy.mjs +190 -0
  347. package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
  348. package/src/memory/lib/memory-recall-read-query.mjs +7 -0
  349. package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
  350. package/src/memory/lib/memory-recall-store.mjs +621 -0
  351. package/src/memory/lib/memory-retrievers.mjs +112 -0
  352. package/src/memory/lib/memory-score.mjs +71 -0
  353. package/src/memory/lib/memory-text-utils.mjs +58 -0
  354. package/src/memory/lib/memory.mjs +412 -0
  355. package/src/memory/lib/model-profile.mjs +85 -0
  356. package/src/memory/lib/pg/adapter.mjs +308 -0
  357. package/src/memory/lib/pg/process.mjs +360 -0
  358. package/src/memory/lib/pg/supervisor.mjs +396 -0
  359. package/src/memory/lib/project-id-resolver.mjs +86 -0
  360. package/src/memory/lib/runtime-fetcher.mjs +442 -0
  361. package/src/memory/lib/trace-store.mjs +728 -0
  362. package/src/memory/tool-defs.mjs +79 -0
  363. package/src/search/index.mjs +1173 -0
  364. package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
  365. package/src/search/lib/backends/exa.mjs +50 -0
  366. package/src/search/lib/backends/firecrawl.mjs +61 -0
  367. package/src/search/lib/backends/gemini-api.mjs +83 -0
  368. package/src/search/lib/backends/grok-oauth.mjs +86 -0
  369. package/src/search/lib/backends/index.mjs +150 -0
  370. package/src/search/lib/backends/openai-api.mjs +144 -0
  371. package/src/search/lib/backends/openai-oauth.mjs +98 -0
  372. package/src/search/lib/backends/openai-web-search.mjs +76 -0
  373. package/src/search/lib/backends/tavily.mjs +55 -0
  374. package/src/search/lib/backends/xai-api.mjs +113 -0
  375. package/src/search/lib/cache.mjs +131 -0
  376. package/src/search/lib/config.mjs +192 -0
  377. package/src/search/lib/formatter.mjs +115 -0
  378. package/src/search/lib/provider-usage.mjs +67 -0
  379. package/src/search/lib/providers.mjs +47 -0
  380. package/src/search/lib/search-intent.mjs +109 -0
  381. package/src/search/lib/setup-handler.mjs +261 -0
  382. package/src/search/lib/state.mjs +201 -0
  383. package/src/search/lib/web-tools.mjs +1207 -0
  384. package/src/search/tool-defs.mjs +83 -0
  385. package/src/setup/defender-exclusion.mjs +183 -0
  386. package/src/shared/abort-controller.mjs +15 -0
  387. package/src/shared/atomic-file.mjs +420 -0
  388. package/src/shared/config.mjs +350 -0
  389. package/src/shared/daemon-recycle.mjs +108 -0
  390. package/src/shared/disable-claude-builtins.mjs +88 -0
  391. package/src/shared/err-text.mjs +12 -0
  392. package/src/shared/llm/cost.mjs +66 -0
  393. package/src/shared/llm/http-agent.mjs +123 -0
  394. package/src/shared/llm/index.mjs +41 -0
  395. package/src/shared/llm/pid-cleanup.mjs +27 -0
  396. package/src/shared/llm/usage-log.mjs +47 -0
  397. package/src/shared/plugin-paths.mjs +58 -0
  398. package/src/shared/schedules-store.mjs +70 -0
  399. package/src/shared/seed.mjs +119 -0
  400. package/src/shared/user-cwd.mjs +213 -0
  401. package/src/shared/user-data-guard.mjs +238 -0
  402. package/src/status/aggregator.mjs +584 -0
  403. package/src/status/server.mjs +413 -0
  404. 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
+ };