mixdog 0.9.3 → 0.9.4
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/package.json +7 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +23 -63
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +84 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +945 -8094
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +23 -101
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +19652 -18630
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +67 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Gateway connect / login / event-listener wiring extracted from discord.mjs.
|
|
2
|
+
// Implemented as instance-method installers that operate on a DiscordBackend
|
|
3
|
+
// `self`. Behavior is byte-identical to the originals; only the host object is
|
|
4
|
+
// passed explicitly instead of via `this`.
|
|
5
|
+
|
|
6
|
+
async function connect(self) {
|
|
7
|
+
// Re-entry guard: if a connect() is already in-flight or completed, return
|
|
8
|
+
// the same promise / no-op so concurrent ownership-timer fires cannot
|
|
9
|
+
// overwrite self.client, duplicate listeners, or trigger duplicate logins.
|
|
10
|
+
if (self._connectPromise) return self._connectPromise;
|
|
11
|
+
self._connectPromise = connectInner(self).catch((err) => {
|
|
12
|
+
self._connectPromise = null;
|
|
13
|
+
throw err;
|
|
14
|
+
});
|
|
15
|
+
return self._connectPromise;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function connectInner(self) {
|
|
19
|
+
await buildClient(self);
|
|
20
|
+
applyStaticAccessOverride(self);
|
|
21
|
+
registerEventListeners(self);
|
|
22
|
+
registerSlashCommands(self);
|
|
23
|
+
registerShardListeners(self);
|
|
24
|
+
try {
|
|
25
|
+
await awaitLogin(self);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// Destroy the partial Client to free the listeners/handles it already
|
|
28
|
+
// attached. Without this, a ready-timeout retry leaks every listener
|
|
29
|
+
// set by registerEventListeners/registerSlashCommands/registerShardListeners.
|
|
30
|
+
try { self.client?.destroy?.(); } catch {}
|
|
31
|
+
self.client = null;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
self.persistAccessFromChannelsConfig();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function buildClient(self) {
|
|
38
|
+
const { Client, GatewayIntentBits, Partials } = await self._ensureDiscord();
|
|
39
|
+
self.client = new Client({
|
|
40
|
+
intents: [
|
|
41
|
+
GatewayIntentBits.DirectMessages,
|
|
42
|
+
GatewayIntentBits.Guilds,
|
|
43
|
+
GatewayIntentBits.GuildMessages,
|
|
44
|
+
GatewayIntentBits.MessageContent
|
|
45
|
+
],
|
|
46
|
+
partials: [Partials.Channel]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function applyStaticAccessOverride(self) {
|
|
51
|
+
if (self.isStatic) {
|
|
52
|
+
self.bootAccess = self.loadAccess();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function registerEventListeners(self) {
|
|
57
|
+
self.client.on("error", (err) => {
|
|
58
|
+
process.stderr.write(`mixdog discord: client error: ${err}
|
|
59
|
+
`);
|
|
60
|
+
});
|
|
61
|
+
self.client.on("messageCreate", (msg) => {
|
|
62
|
+
if (msg.author.id === self.client.user?.id) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (msg.author.bot) return;
|
|
66
|
+
self.handleInbound(msg, Date.now()).catch(
|
|
67
|
+
(e) => process.stderr.write(`mixdog discord: handleInbound failed: ${e}
|
|
68
|
+
`)
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
self.client.on("interactionCreate", async (interaction) => {
|
|
72
|
+
try {
|
|
73
|
+
// Trust gate for interactions. Buttons / selects / modal submits used to
|
|
74
|
+
// reach onInteraction / onModalRequest without passing through the
|
|
75
|
+
// message gate(), so a configured allowFrom never applied to them
|
|
76
|
+
// (schedule/quiet/profile modals + perm approvals were openable by any
|
|
77
|
+
// user in the channel). Apply the same allowFrom decision here; an empty
|
|
78
|
+
// allowFrom stays open so current configs are unaffected.
|
|
79
|
+
if (!self._interactionAllowed(interaction.channelId ?? "", interaction.user?.id, interaction)) {
|
|
80
|
+
try {
|
|
81
|
+
if (typeof interaction.reply === "function" && !interaction.replied && !interaction.deferred) {
|
|
82
|
+
await interaction.reply({ content: "⛔ Not authorized for this action.", ephemeral: true });
|
|
83
|
+
} else if (typeof interaction.deferUpdate === "function") {
|
|
84
|
+
await interaction.deferUpdate();
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (interaction.isChatInputCommand() && interaction.commandName === "stop") {
|
|
90
|
+
await interaction.reply({ content: "\u23F9 Stopping...", ephemeral: true });
|
|
91
|
+
if (self.onInteraction) {
|
|
92
|
+
self.onInteraction({
|
|
93
|
+
type: "button",
|
|
94
|
+
customId: "stop_task",
|
|
95
|
+
userId: interaction.user.id,
|
|
96
|
+
channelId: interaction.channelId ?? ""
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (interaction.isModalSubmit()) {
|
|
102
|
+
if (self.onInteraction) {
|
|
103
|
+
const fields = {};
|
|
104
|
+
for (const row of interaction.components) {
|
|
105
|
+
for (const comp of row.components ?? []) {
|
|
106
|
+
if (comp.customId && comp.value != null) fields[comp.customId] = String(comp.value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
self.onInteraction({
|
|
110
|
+
type: "modal",
|
|
111
|
+
customId: interaction.customId,
|
|
112
|
+
userId: interaction.user.id,
|
|
113
|
+
channelId: interaction.channelId ?? "",
|
|
114
|
+
fields,
|
|
115
|
+
message: interaction.message ? { id: interaction.message.id } : void 0
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
await interaction.deferUpdate().catch(() => {
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isRoleSelectMenu() || interaction.isUserSelectMenu() || interaction.isChannelSelectMenu()) {
|
|
123
|
+
const needsModal = interaction.isButton() && (interaction.customId === "sched_add_next" || interaction.customId === "sched_edit_next" || interaction.customId === "quiet_set_next" || interaction.customId === "activity_add_next" || interaction.customId === "profile_edit");
|
|
124
|
+
if (needsModal) {
|
|
125
|
+
if (self.onModalRequest) {
|
|
126
|
+
await Promise.resolve(self.onModalRequest(interaction)).catch((err) => {
|
|
127
|
+
process.stderr.write(`mixdog discord: onModalRequest failed: ${err}\n`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await interaction.deferUpdate().catch(() => {
|
|
133
|
+
});
|
|
134
|
+
if (self.onInteraction) {
|
|
135
|
+
self.onInteraction({
|
|
136
|
+
type: interaction.isButton() ? "button" : "select",
|
|
137
|
+
customId: interaction.customId,
|
|
138
|
+
userId: interaction.user.id,
|
|
139
|
+
channelId: interaction.channelId,
|
|
140
|
+
values: interaction.isStringSelectMenu() ? interaction.values : void 0,
|
|
141
|
+
message: interaction.message ? { id: interaction.message.id } : void 0
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
process.stderr.write(`mixdog discord: interaction error: ${err}
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function registerSlashCommands(self) {
|
|
153
|
+
self.client.on(self._discordEvents().ClientReady, async (c) => {
|
|
154
|
+
process.stderr.write(`mixdog discord: gateway connected as ${c.user.tag}
|
|
155
|
+
`);
|
|
156
|
+
try {
|
|
157
|
+
// Plugin registers no global commands; clear the global slot so any
|
|
158
|
+
// pre-existing entry from prior installs is not surfaced to users.
|
|
159
|
+
await c.application?.commands.set([]);
|
|
160
|
+
process.stderr.write(`mixdog discord: global application commands cleared
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
// Replace each guild's command set with just /stop. set() overwrites,
|
|
164
|
+
// so the desired set is the only one that survives.
|
|
165
|
+
const desiredCommands = [
|
|
166
|
+
{ name: "stop", description: "Stop the current Mixdog response" },
|
|
167
|
+
];
|
|
168
|
+
for (const [guildId] of c.guilds.cache) {
|
|
169
|
+
await c.application?.commands.set(desiredCommands, guildId);
|
|
170
|
+
}
|
|
171
|
+
// Register /stop globally so it is available in DM bridge contexts
|
|
172
|
+
// where there is no guild scope.
|
|
173
|
+
try {
|
|
174
|
+
await c.application?.commands.set(desiredCommands);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
process.stderr.write(`mixdog discord: global /stop register failed: ${e?.message}\n`);
|
|
177
|
+
}
|
|
178
|
+
process.stderr.write(`mixdog discord: /stop command registered (${c.guilds.cache.size} guild(s))
|
|
179
|
+
`);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
process.stderr.write(`mixdog discord: slash command registration failed: ${err}
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function registerShardListeners(self) {
|
|
188
|
+
self.client.on("shardDisconnect", (ev, id) => {
|
|
189
|
+
process.stderr.write(`mixdog discord: shard ${id} disconnected (code ${ev.code}). Will auto-reconnect.
|
|
190
|
+
`);
|
|
191
|
+
});
|
|
192
|
+
self.client.on("shardReconnecting", (id) => {
|
|
193
|
+
process.stderr.write(`mixdog discord: shard ${id} reconnecting...
|
|
194
|
+
`);
|
|
195
|
+
});
|
|
196
|
+
self.client.on("shardResume", (id, replayedEvents) => {
|
|
197
|
+
process.stderr.write(`mixdog discord: shard ${id} resumed (replayed ${replayedEvents} events)
|
|
198
|
+
`);
|
|
199
|
+
});
|
|
200
|
+
self.client.on("warn", (msg) => {
|
|
201
|
+
process.stderr.write(`mixdog discord: warn: ${msg}
|
|
202
|
+
`);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function awaitLogin(self) {
|
|
207
|
+
let readyTimeout;
|
|
208
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
209
|
+
readyTimeout = setTimeout(() => reject(new Error("discord ready timeout (30s)")), 3e4);
|
|
210
|
+
self.client.once(self._discordEvents().ClientReady, () => {
|
|
211
|
+
clearTimeout(readyTimeout);
|
|
212
|
+
resolve();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
try {
|
|
216
|
+
await self.client.login(self.token);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
clearTimeout(readyTimeout);
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
await readyPromise;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export {
|
|
225
|
+
connect,
|
|
226
|
+
connectInner,
|
|
227
|
+
buildClient,
|
|
228
|
+
applyStaticAccessOverride,
|
|
229
|
+
registerEventListeners,
|
|
230
|
+
registerSlashCommands,
|
|
231
|
+
registerShardListeners,
|
|
232
|
+
awaitLogin
|
|
233
|
+
};
|
|
@@ -10,7 +10,6 @@ async function ensureDiscord() {
|
|
|
10
10
|
return _discord;
|
|
11
11
|
}
|
|
12
12
|
import {
|
|
13
|
-
writeFileSync,
|
|
14
13
|
mkdirSync,
|
|
15
14
|
statSync,
|
|
16
15
|
realpathSync
|
|
@@ -20,37 +19,11 @@ import { createHash } from "crypto";
|
|
|
20
19
|
import { chunk, formatForDiscord, MAX_DISCORD_MESSAGE } from "../lib/format.mjs";
|
|
21
20
|
import { withConfigLock } from "../lib/config-lock.mjs";
|
|
22
21
|
import { readSection, updateSection } from "../../shared/config.mjs";
|
|
22
|
+
import { normalizeAccess, safeAttName } from "./discord-access.mjs";
|
|
23
|
+
import { MAX_ATTACHMENT_BYTES, downloadSingleAttachment } from "./discord-attachments.mjs";
|
|
24
|
+
import * as gateway from "./discord-gateway.mjs";
|
|
23
25
|
const MAX_CHUNK_LIMIT = MAX_DISCORD_MESSAGE;
|
|
24
|
-
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
25
26
|
const RECENT_SENT_CAP = 200;
|
|
26
|
-
function defaultAccess() {
|
|
27
|
-
return {
|
|
28
|
-
dmPolicy: "allowlist",
|
|
29
|
-
allowFrom: [],
|
|
30
|
-
channels: {}
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function normalizeAccess(parsed) {
|
|
34
|
-
const defaults = defaultAccess();
|
|
35
|
-
return {
|
|
36
|
-
// Legacy "pairing" policy was removed (its approval flow was never
|
|
37
|
-
// completable); normalize it to "allowlist" on load.
|
|
38
|
-
dmPolicy: parsed?.dmPolicy === "pairing" ? "allowlist" : (parsed?.dmPolicy ?? defaults.dmPolicy),
|
|
39
|
-
allowFrom: parsed?.allowFrom ?? defaults.allowFrom,
|
|
40
|
-
channels: parsed?.channels ?? defaults.channels,
|
|
41
|
-
mentionPatterns: parsed?.mentionPatterns,
|
|
42
|
-
// Setup UI historically saved a boolean toggle; runtime needs an emoji
|
|
43
|
-
// string for msg.react(). true → default emoji, non-string → off.
|
|
44
|
-
ackReaction: parsed?.ackReaction === true
|
|
45
|
-
? "✅"
|
|
46
|
-
: (typeof parsed?.ackReaction === "string" && parsed.ackReaction ? parsed.ackReaction : undefined),
|
|
47
|
-
replyToMode: parsed?.replyToMode,
|
|
48
|
-
textChunkLimit: parsed?.textChunkLimit,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function safeAttName(att) {
|
|
52
|
-
return (att.name ?? att.id).replace(/[\[\]\r\n;]/g, "_");
|
|
53
|
-
}
|
|
54
27
|
class DiscordBackend {
|
|
55
28
|
name = "discord";
|
|
56
29
|
MAX_MESSAGE_LENGTH = MAX_DISCORD_MESSAGE;
|
|
@@ -82,216 +55,16 @@ class DiscordBackend {
|
|
|
82
55
|
formatOutgoing(text) {
|
|
83
56
|
return formatForDiscord(text);
|
|
84
57
|
}
|
|
85
|
-
// ──
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// the same promise / no-op so concurrent ownership-timer fires cannot
|
|
89
|
-
// overwrite this.client, duplicate listeners, or trigger duplicate logins.
|
|
90
|
-
if (this._connectPromise) return this._connectPromise;
|
|
91
|
-
this._connectPromise = this._connectInner().catch((err) => {
|
|
92
|
-
this._connectPromise = null;
|
|
93
|
-
throw err;
|
|
94
|
-
});
|
|
95
|
-
return this._connectPromise;
|
|
96
|
-
}
|
|
97
|
-
async _connectInner() {
|
|
98
|
-
await this._buildClient();
|
|
99
|
-
this._applyStaticAccessOverride();
|
|
100
|
-
this._registerEventListeners();
|
|
101
|
-
this._registerSlashCommands();
|
|
102
|
-
this._registerShardListeners();
|
|
103
|
-
try {
|
|
104
|
-
await this._awaitLogin();
|
|
105
|
-
} catch (err) {
|
|
106
|
-
// Destroy the partial Client to free the listeners/handles it already
|
|
107
|
-
// attached. Without this, a ready-timeout retry leaks every listener
|
|
108
|
-
// set by _registerEventListeners/_registerSlashCommands/_registerShardListeners.
|
|
109
|
-
try { this.client?.destroy?.(); } catch {}
|
|
110
|
-
this.client = null;
|
|
111
|
-
throw err;
|
|
112
|
-
}
|
|
113
|
-
this.persistAccessFromChannelsConfig();
|
|
114
|
-
}
|
|
115
|
-
async _buildClient() {
|
|
116
|
-
const { Client, GatewayIntentBits, Partials } = await ensureDiscord();
|
|
117
|
-
this.client = new Client({
|
|
118
|
-
intents: [
|
|
119
|
-
GatewayIntentBits.DirectMessages,
|
|
120
|
-
GatewayIntentBits.Guilds,
|
|
121
|
-
GatewayIntentBits.GuildMessages,
|
|
122
|
-
GatewayIntentBits.MessageContent
|
|
123
|
-
],
|
|
124
|
-
partials: [Partials.Channel]
|
|
125
|
-
});
|
|
58
|
+
// ── discord.js access bridges for extracted gateway module ─────────
|
|
59
|
+
_ensureDiscord() {
|
|
60
|
+
return ensureDiscord();
|
|
126
61
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.bootAccess = this.loadAccess();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
_registerEventListeners() {
|
|
133
|
-
this.client.on("error", (err) => {
|
|
134
|
-
process.stderr.write(`mixdog discord: client error: ${err}
|
|
135
|
-
`);
|
|
136
|
-
});
|
|
137
|
-
this.client.on("messageCreate", (msg) => {
|
|
138
|
-
if (msg.author.id === this.client.user?.id) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (msg.author.bot) return;
|
|
142
|
-
this.handleInbound(msg, Date.now()).catch(
|
|
143
|
-
(e) => process.stderr.write(`mixdog discord: handleInbound failed: ${e}
|
|
144
|
-
`)
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
this.client.on("interactionCreate", async (interaction) => {
|
|
148
|
-
try {
|
|
149
|
-
// Trust gate for interactions. Buttons / selects / modal submits used to
|
|
150
|
-
// reach onInteraction / onModalRequest without passing through the
|
|
151
|
-
// message gate(), so a configured allowFrom never applied to them
|
|
152
|
-
// (schedule/quiet/profile modals + perm approvals were openable by any
|
|
153
|
-
// user in the channel). Apply the same allowFrom decision here; an empty
|
|
154
|
-
// allowFrom stays open so current configs are unaffected.
|
|
155
|
-
if (!this._interactionAllowed(interaction.channelId ?? "", interaction.user?.id, interaction)) {
|
|
156
|
-
try {
|
|
157
|
-
if (typeof interaction.reply === "function" && !interaction.replied && !interaction.deferred) {
|
|
158
|
-
await interaction.reply({ content: "⛔ Not authorized for this action.", ephemeral: true });
|
|
159
|
-
} else if (typeof interaction.deferUpdate === "function") {
|
|
160
|
-
await interaction.deferUpdate();
|
|
161
|
-
}
|
|
162
|
-
} catch {}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (interaction.isChatInputCommand() && interaction.commandName === "stop") {
|
|
166
|
-
await interaction.reply({ content: "\u23F9 Stopping...", ephemeral: true });
|
|
167
|
-
if (this.onInteraction) {
|
|
168
|
-
this.onInteraction({
|
|
169
|
-
type: "button",
|
|
170
|
-
customId: "stop_task",
|
|
171
|
-
userId: interaction.user.id,
|
|
172
|
-
channelId: interaction.channelId ?? ""
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (interaction.isModalSubmit()) {
|
|
178
|
-
if (this.onInteraction) {
|
|
179
|
-
const fields = {};
|
|
180
|
-
for (const row of interaction.components) {
|
|
181
|
-
for (const comp of row.components ?? []) {
|
|
182
|
-
if (comp.customId && comp.value != null) fields[comp.customId] = String(comp.value);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
this.onInteraction({
|
|
186
|
-
type: "modal",
|
|
187
|
-
customId: interaction.customId,
|
|
188
|
-
userId: interaction.user.id,
|
|
189
|
-
channelId: interaction.channelId ?? "",
|
|
190
|
-
fields,
|
|
191
|
-
message: interaction.message ? { id: interaction.message.id } : void 0
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
await interaction.deferUpdate().catch(() => {
|
|
195
|
-
});
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isRoleSelectMenu() || interaction.isUserSelectMenu() || interaction.isChannelSelectMenu()) {
|
|
199
|
-
const needsModal = interaction.isButton() && (interaction.customId === "sched_add_next" || interaction.customId === "sched_edit_next" || interaction.customId === "quiet_set_next" || interaction.customId === "activity_add_next" || interaction.customId === "profile_edit");
|
|
200
|
-
if (needsModal) {
|
|
201
|
-
if (this.onModalRequest) {
|
|
202
|
-
await Promise.resolve(this.onModalRequest(interaction)).catch((err) => {
|
|
203
|
-
process.stderr.write(`mixdog discord: onModalRequest failed: ${err}\n`);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
await interaction.deferUpdate().catch(() => {
|
|
209
|
-
});
|
|
210
|
-
if (this.onInteraction) {
|
|
211
|
-
this.onInteraction({
|
|
212
|
-
type: interaction.isButton() ? "button" : "select",
|
|
213
|
-
customId: interaction.customId,
|
|
214
|
-
userId: interaction.user.id,
|
|
215
|
-
channelId: interaction.channelId,
|
|
216
|
-
values: interaction.isStringSelectMenu() ? interaction.values : void 0,
|
|
217
|
-
message: interaction.message ? { id: interaction.message.id } : void 0
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
} catch (err) {
|
|
222
|
-
process.stderr.write(`mixdog discord: interaction error: ${err}
|
|
223
|
-
`);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
62
|
+
_discordEvents() {
|
|
63
|
+
return _discord.Events;
|
|
226
64
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
`);
|
|
231
|
-
try {
|
|
232
|
-
// Plugin registers no global commands; clear the global slot so any
|
|
233
|
-
// pre-existing entry from prior installs is not surfaced to users.
|
|
234
|
-
await c.application?.commands.set([]);
|
|
235
|
-
process.stderr.write(`mixdog discord: global application commands cleared
|
|
236
|
-
`);
|
|
237
|
-
|
|
238
|
-
// Replace each guild's command set with just /stop. set() overwrites,
|
|
239
|
-
// so the desired set is the only one that survives.
|
|
240
|
-
const desiredCommands = [
|
|
241
|
-
{ name: "stop", description: "Stop the current Mixdog response" },
|
|
242
|
-
];
|
|
243
|
-
for (const [guildId] of c.guilds.cache) {
|
|
244
|
-
await c.application?.commands.set(desiredCommands, guildId);
|
|
245
|
-
}
|
|
246
|
-
// Register /stop globally so it is available in DM bridge contexts
|
|
247
|
-
// where there is no guild scope.
|
|
248
|
-
try {
|
|
249
|
-
await c.application?.commands.set(desiredCommands);
|
|
250
|
-
} catch (e) {
|
|
251
|
-
process.stderr.write(`mixdog discord: global /stop register failed: ${e?.message}\n`);
|
|
252
|
-
}
|
|
253
|
-
process.stderr.write(`mixdog discord: /stop command registered (${c.guilds.cache.size} guild(s))
|
|
254
|
-
`);
|
|
255
|
-
} catch (err) {
|
|
256
|
-
process.stderr.write(`mixdog discord: slash command registration failed: ${err}
|
|
257
|
-
`);
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
_registerShardListeners() {
|
|
262
|
-
this.client.on("shardDisconnect", (ev, id) => {
|
|
263
|
-
process.stderr.write(`mixdog discord: shard ${id} disconnected (code ${ev.code}). Will auto-reconnect.
|
|
264
|
-
`);
|
|
265
|
-
});
|
|
266
|
-
this.client.on("shardReconnecting", (id) => {
|
|
267
|
-
process.stderr.write(`mixdog discord: shard ${id} reconnecting...
|
|
268
|
-
`);
|
|
269
|
-
});
|
|
270
|
-
this.client.on("shardResume", (id, replayedEvents) => {
|
|
271
|
-
process.stderr.write(`mixdog discord: shard ${id} resumed (replayed ${replayedEvents} events)
|
|
272
|
-
`);
|
|
273
|
-
});
|
|
274
|
-
this.client.on("warn", (msg) => {
|
|
275
|
-
process.stderr.write(`mixdog discord: warn: ${msg}
|
|
276
|
-
`);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
async _awaitLogin() {
|
|
280
|
-
let readyTimeout;
|
|
281
|
-
const readyPromise = new Promise((resolve, reject) => {
|
|
282
|
-
readyTimeout = setTimeout(() => reject(new Error("discord ready timeout (30s)")), 3e4);
|
|
283
|
-
this.client.once(_discord.Events.ClientReady, () => {
|
|
284
|
-
clearTimeout(readyTimeout);
|
|
285
|
-
resolve();
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
try {
|
|
289
|
-
await this.client.login(this.token);
|
|
290
|
-
} catch (err) {
|
|
291
|
-
clearTimeout(readyTimeout);
|
|
292
|
-
throw err;
|
|
293
|
-
}
|
|
294
|
-
await readyPromise;
|
|
65
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
66
|
+
async connect() {
|
|
67
|
+
return gateway.connect(this);
|
|
295
68
|
}
|
|
296
69
|
async disconnect() {
|
|
297
70
|
for (const interval of this.typingIntervals.values()) {
|
|
@@ -810,60 +583,7 @@ class DiscordBackend {
|
|
|
810
583
|
}
|
|
811
584
|
}
|
|
812
585
|
async downloadSingleAttachment(att) {
|
|
813
|
-
|
|
814
|
-
throw new Error(
|
|
815
|
-
`attachment too large: ${(att.size / 1024 / 1024).toFixed(1)}MB, max ${MAX_ATTACHMENT_BYTES / 1024 / 1024}MB`
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
const res = await fetch(att.url, { signal: AbortSignal.timeout(180_000) });
|
|
819
|
-
if (!res.ok) {
|
|
820
|
-
throw new Error(`attachment download failed: HTTP ${res.status}`);
|
|
821
|
-
}
|
|
822
|
-
if (!res.body) {
|
|
823
|
-
throw new Error(`attachment download returned empty body: ${att.name ?? att.id}`);
|
|
824
|
-
}
|
|
825
|
-
// Stream the response so an oversized payload is aborted before the
|
|
826
|
-
// full body lands in memory. Buffering via arrayBuffer() first would
|
|
827
|
-
// already exceed MAX_ATTACHMENT_BYTES by the time we checked length.
|
|
828
|
-
const reader = res.body.getReader();
|
|
829
|
-
const chunks = [];
|
|
830
|
-
let received = 0;
|
|
831
|
-
try {
|
|
832
|
-
while (true) {
|
|
833
|
-
const { done, value } = await reader.read();
|
|
834
|
-
if (done) break;
|
|
835
|
-
if (!value) continue;
|
|
836
|
-
received += value.byteLength;
|
|
837
|
-
if (received > MAX_ATTACHMENT_BYTES) {
|
|
838
|
-
try { await reader.cancel(); } catch {}
|
|
839
|
-
throw new Error(
|
|
840
|
-
`attachment payload too large: exceeded ${MAX_ATTACHMENT_BYTES / 1024 / 1024}MB while streaming (${att.name ?? att.id})`
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
chunks.push(value);
|
|
844
|
-
}
|
|
845
|
-
} finally {
|
|
846
|
-
try { reader.releaseLock(); } catch {}
|
|
847
|
-
}
|
|
848
|
-
if (received === 0) {
|
|
849
|
-
throw new Error(`attachment download returned empty buffer: ${att.name ?? att.id}`);
|
|
850
|
-
}
|
|
851
|
-
const buf = Buffer.concat(chunks.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength)), received);
|
|
852
|
-
if (att.size > 0 && buf.length !== att.size) {
|
|
853
|
-
process.stderr.write(`mixdog discord: attachment size mismatch: expected ${att.size} got ${buf.length} (${att.name ?? att.id})\n`);
|
|
854
|
-
}
|
|
855
|
-
const name = att.name ?? `${att.id}`;
|
|
856
|
-
const rawExt = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : "bin";
|
|
857
|
-
const ext = rawExt.replace(/[^a-zA-Z0-9]/g, "") || "bin";
|
|
858
|
-
const safeId = String(att.id).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
859
|
-
mkdirSync(this.inboxDir, { recursive: true });
|
|
860
|
-
const candidate = join(this.inboxDir, `${Date.now()}-${safeId}.${ext}`);
|
|
861
|
-
const resolvedInbox = realpathSync(this.inboxDir);
|
|
862
|
-
if (!candidate.startsWith(resolvedInbox + sep)) {
|
|
863
|
-
throw new Error(`attachment path traversal rejected: ${candidate}`);
|
|
864
|
-
}
|
|
865
|
-
writeFileSync(candidate, buf);
|
|
866
|
-
return candidate;
|
|
586
|
+
return downloadSingleAttachment(att, this.inboxDir);
|
|
867
587
|
}
|
|
868
588
|
}
|
|
869
589
|
export {
|