mixdog 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -38
- package/package.json +10 -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/r5-orchestrated-task.json +7 -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-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -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 +251 -32
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +3 -4
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- 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-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +25 -65
- 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 +9 -8
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +10 -7
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +647 -2056
- package/src/output-styles/default.md +30 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +8 -6
- package/src/output-styles/simple.md +21 -7
- package/src/rules/agent/00-common.md +6 -3
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +15 -0
- package/src/rules/lead/lead-tool.md +6 -15
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- 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 +105 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -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 +740 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -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/providers/provider-catalog-cache.mjs +80 -0
- 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 +275 -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 +278 -402
- 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/stall-policy.mjs +20 -1
- 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 +35 -44
- 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 +27 -318
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +247 -701
- package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
- package/src/runtime/channels/lib/event-queue.mjs +78 -13
- package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- 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/scheduler.mjs +27 -113
- 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 +313 -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 +43 -616
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +210 -1948
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- 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/ko-morph.mjs +195 -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 +69 -12
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +802 -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/config.mjs +14 -4
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.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 +119 -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 +205 -0
- package/src/session-runtime/quick-search-models.mjs +47 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +352 -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/session-runtime/workflow.mjs +1 -1
- 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 +138 -669
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +64 -14
- 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 +1059 -8110
- package/src/tui/app/app-format.mjs +213 -0
- package/src/tui/app/channel-pickers.mjs +508 -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 +356 -0
- package/src/tui/app/model-options.mjs +334 -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 +489 -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 +677 -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 +512 -0
- package/src/tui/app/use-transcript-window.mjs +607 -0
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +64 -15
- package/src/tui/components/PromptInput.jsx +33 -102
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +69 -15
- 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 +13553 -12384
- 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 +170 -574
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +65 -1
- 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 +77 -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 +16 -18
- package/src/workflows/sequential/WORKFLOW.md +16 -18
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +112 -7
- 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/channels/lib/holidays.mjs +0 -138
- 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.persistAccessFromMainChannel();
|
|
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 === "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
|
-
});
|
|
126
|
-
}
|
|
127
|
-
_applyStaticAccessOverride() {
|
|
128
|
-
if (this.isStatic) {
|
|
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
|
-
});
|
|
226
|
-
}
|
|
227
|
-
_registerSlashCommands() {
|
|
228
|
-
this.client.on(_discord.Events.ClientReady, async (c) => {
|
|
229
|
-
process.stderr.write(`mixdog discord: gateway connected as ${c.user.tag}
|
|
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
|
-
});
|
|
58
|
+
// ── discord.js access bridges for extracted gateway module ─────────
|
|
59
|
+
_ensureDiscord() {
|
|
60
|
+
return ensureDiscord();
|
|
260
61
|
}
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
});
|
|
62
|
+
_discordEvents() {
|
|
63
|
+
return _discord.Events;
|
|
278
64
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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()) {
|
|
@@ -553,47 +326,37 @@ class DiscordBackend {
|
|
|
553
326
|
try {
|
|
554
327
|
const parsed = readSection("channels");
|
|
555
328
|
const access = normalizeAccess(parsed.access ?? this.initialAccess);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
329
|
+
// Single main channel: auto-allow the configured channel id so channel
|
|
330
|
+
// adds apply without a restart (mirrors loadAccess()).
|
|
331
|
+
const id = this.mainChannelId;
|
|
332
|
+
if (id && !(id in access.channels)) {
|
|
333
|
+
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
565
334
|
}
|
|
566
335
|
return access;
|
|
567
336
|
} catch {
|
|
568
337
|
return this.initialAccess;
|
|
569
338
|
}
|
|
570
339
|
}
|
|
571
|
-
|
|
340
|
+
persistAccessFromMainChannel() {
|
|
572
341
|
if (this.isStatic || !this.configFile) return;
|
|
573
342
|
try {
|
|
343
|
+
const id = this.mainChannelId;
|
|
344
|
+
if (!id) return;
|
|
574
345
|
const parsed = readSection("channels");
|
|
575
|
-
if (!parsed.channelsConfig) return;
|
|
576
346
|
const access = normalizeAccess(parsed.access);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const id = entry.channelId;
|
|
581
|
-
if (id && !(id in access.channels)) {
|
|
582
|
-
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
583
|
-
changed = true;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
347
|
+
if (!(id in access.channels)) {
|
|
348
|
+
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
349
|
+
this.saveAccess(access);
|
|
586
350
|
}
|
|
587
|
-
if (changed) this.saveAccess(access);
|
|
588
351
|
} catch (err) {
|
|
589
|
-
process.stderr.write(`mixdog discord:
|
|
352
|
+
process.stderr.write(`mixdog discord: persistAccessFromMainChannel failed: ${err}\n`);
|
|
590
353
|
}
|
|
591
354
|
}
|
|
592
355
|
loadAccess() {
|
|
593
356
|
const a = this.bootAccess ?? this.readConfigAccess();
|
|
594
357
|
// Single-source channel setup: auto-allow the configured main channel so
|
|
595
358
|
// both inbound (gate) and outbound (fetchAllowedChannel) accept it from a
|
|
596
|
-
// single
|
|
359
|
+
// single channel.channelId — no separate access.channels entry.
|
|
597
360
|
// An explicit entry for the same channel is preserved (not overwritten).
|
|
598
361
|
if (this.mainChannelId && a && !(this.mainChannelId in (a.channels ?? {}))) {
|
|
599
362
|
return { ...a, channels: { ...(a.channels ?? {}), [this.mainChannelId]: { allowFrom: [], requireMention: false } } };
|
|
@@ -743,9 +506,8 @@ class DiscordBackend {
|
|
|
743
506
|
}
|
|
744
507
|
if (this.onMessage) {
|
|
745
508
|
// Thread messages were gated against the parent channel's policy (see
|
|
746
|
-
// gate() above)
|
|
747
|
-
//
|
|
748
|
-
// can fall back to the parent when the thread id has no entry.
|
|
509
|
+
// gate() above). Surface parentChatId so downstream routing can fall
|
|
510
|
+
// back to the parent when the thread id needs it.
|
|
749
511
|
const isThread = (() => { try { return !!msg.channel?.isThread?.(); } catch { return false; } })();
|
|
750
512
|
const parentChatId = isThread ? (msg.channel.parentId ?? null) : null;
|
|
751
513
|
this.onMessage({
|
|
@@ -810,60 +572,7 @@ class DiscordBackend {
|
|
|
810
572
|
}
|
|
811
573
|
}
|
|
812
574
|
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;
|
|
575
|
+
return downloadSingleAttachment(att, this.inboxDir);
|
|
867
576
|
}
|
|
868
577
|
}
|
|
869
578
|
export {
|
|
@@ -112,22 +112,18 @@ class TelegramBackend {
|
|
|
112
112
|
formatOutgoing(text) {
|
|
113
113
|
return text;
|
|
114
114
|
}
|
|
115
|
-
// Fold
|
|
116
|
-
// DiscordBackend.readConfigAccess(). Read live so
|
|
117
|
-
//
|
|
115
|
+
// Fold the single configured main channel id into access.channels, mirroring
|
|
116
|
+
// DiscordBackend.readConfigAccess(). Read live so a channel change applies
|
|
117
|
+
// without a restart. Falls back to initialAccess if the read throws.
|
|
118
118
|
readConfigAccess() {
|
|
119
119
|
try {
|
|
120
120
|
const parsed = readSection("channels");
|
|
121
121
|
const access = normalizeAccess(parsed.access ?? this.initialAccess);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
122
|
+
// Single main channel: auto-allow the configured channel id so a channel
|
|
123
|
+
// change applies without a restart (mirrors loadAccess()).
|
|
124
|
+
const id = this.mainChannelId;
|
|
125
|
+
if (id && !(id in access.channels)) {
|
|
126
|
+
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
131
127
|
}
|
|
132
128
|
return access;
|
|
133
129
|
} catch {
|