clawdbot 2026.1.4-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +120 -0
- package/LICENSE +21 -0
- package/README-header.png +0 -0
- package/README.md +297 -0
- package/dist/agents/agent-paths.js +17 -0
- package/dist/agents/bash-process-registry.js +126 -0
- package/dist/agents/bash-tools.js +837 -0
- package/dist/agents/clawdbot-tools.js +30 -0
- package/dist/agents/clawdis-tools.js +27 -0
- package/dist/agents/context.js +34 -0
- package/dist/agents/defaults.js +6 -0
- package/dist/agents/model-auth.js +112 -0
- package/dist/agents/model-catalog.js +55 -0
- package/dist/agents/model-fallback.js +191 -0
- package/dist/agents/model-scan.js +263 -0
- package/dist/agents/model-selection.js +116 -0
- package/dist/agents/models-config.js +49 -0
- package/dist/agents/pi-embedded-helpers.js +74 -0
- package/dist/agents/pi-embedded-runner.js +407 -0
- package/dist/agents/pi-embedded-subscribe.js +568 -0
- package/dist/agents/pi-embedded-utils.js +20 -0
- package/dist/agents/pi-embedded.js +1 -0
- package/dist/agents/pi-oauth.js +88 -0
- package/dist/agents/pi-tools.js +433 -0
- package/dist/agents/sandbox-paths.js +68 -0
- package/dist/agents/sandbox.js +644 -0
- package/dist/agents/shell-utils.js +53 -0
- package/dist/agents/skills-install.js +244 -0
- package/dist/agents/skills-status.js +157 -0
- package/dist/agents/skills.js +470 -0
- package/dist/agents/steerable-agent-loop.js +338 -0
- package/dist/agents/steerable-provider-transport.js +48 -0
- package/dist/agents/system-prompt.js +104 -0
- package/dist/agents/tool-display.js +162 -0
- package/dist/agents/tool-images.js +138 -0
- package/dist/agents/tools/browser-tool.js +339 -0
- package/dist/agents/tools/canvas-tool.js +193 -0
- package/dist/agents/tools/common.js +88 -0
- package/dist/agents/tools/cron-tool.js +124 -0
- package/dist/agents/tools/discord-actions-guild.js +186 -0
- package/dist/agents/tools/discord-actions-messaging.js +285 -0
- package/dist/agents/tools/discord-actions-moderation.js +70 -0
- package/dist/agents/tools/discord-actions.js +56 -0
- package/dist/agents/tools/discord-schema.js +199 -0
- package/dist/agents/tools/discord-tool.js +16 -0
- package/dist/agents/tools/gateway-tool.js +46 -0
- package/dist/agents/tools/gateway.js +27 -0
- package/dist/agents/tools/image-tool.js +132 -0
- package/dist/agents/tools/nodes-tool.js +413 -0
- package/dist/agents/tools/nodes-utils.js +92 -0
- package/dist/agents/tools/sessions-helpers.js +88 -0
- package/dist/agents/tools/sessions-history-tool.js +53 -0
- package/dist/agents/tools/sessions-list-tool.js +143 -0
- package/dist/agents/tools/sessions-send-helpers.js +100 -0
- package/dist/agents/tools/sessions-send-tool.js +347 -0
- package/dist/agents/tools/slack-actions.js +129 -0
- package/dist/agents/tools/slack-schema.js +59 -0
- package/dist/agents/tools/slack-tool.js +16 -0
- package/dist/agents/usage.js +39 -0
- package/dist/agents/workspace.js +241 -0
- package/dist/auto-reply/chunk.js +76 -0
- package/dist/auto-reply/envelope.js +38 -0
- package/dist/auto-reply/group-activation.js +20 -0
- package/dist/auto-reply/heartbeat.js +57 -0
- package/dist/auto-reply/model.js +14 -0
- package/dist/auto-reply/reply/abort.js +14 -0
- package/dist/auto-reply/reply/agent-runner.js +371 -0
- package/dist/auto-reply/reply/block-streaming.js +34 -0
- package/dist/auto-reply/reply/body.js +29 -0
- package/dist/auto-reply/reply/commands.js +207 -0
- package/dist/auto-reply/reply/directive-handling.js +361 -0
- package/dist/auto-reply/reply/directives.js +47 -0
- package/dist/auto-reply/reply/followup-runner.js +149 -0
- package/dist/auto-reply/reply/groups.js +91 -0
- package/dist/auto-reply/reply/mentions.js +38 -0
- package/dist/auto-reply/reply/model-selection.js +114 -0
- package/dist/auto-reply/reply/queue.js +399 -0
- package/dist/auto-reply/reply/reply-tags.js +26 -0
- package/dist/auto-reply/reply/session-updates.js +87 -0
- package/dist/auto-reply/reply/session.js +160 -0
- package/dist/auto-reply/reply/typing.js +75 -0
- package/dist/auto-reply/reply.js +535 -0
- package/dist/auto-reply/send-policy.js +28 -0
- package/dist/auto-reply/status.js +158 -0
- package/dist/auto-reply/templating.js +9 -0
- package/dist/auto-reply/thinking.js +49 -0
- package/dist/auto-reply/tokens.js +2 -0
- package/dist/auto-reply/tool-meta.js +74 -0
- package/dist/auto-reply/transcription.js +57 -0
- package/dist/auto-reply/types.js +1 -0
- package/dist/browser/bridge-server.js +37 -0
- package/dist/browser/cdp.js +382 -0
- package/dist/browser/chrome.js +432 -0
- package/dist/browser/client-actions-core.js +67 -0
- package/dist/browser/client-actions-observe.js +24 -0
- package/dist/browser/client-actions-types.js +1 -0
- package/dist/browser/client-actions.js +3 -0
- package/dist/browser/client-fetch.js +43 -0
- package/dist/browser/client.js +105 -0
- package/dist/browser/config.js +140 -0
- package/dist/browser/constants.js +4 -0
- package/dist/browser/profiles-service.js +122 -0
- package/dist/browser/profiles.js +85 -0
- package/dist/browser/pw-ai.js +2 -0
- package/dist/browser/pw-session.js +144 -0
- package/dist/browser/pw-tools-core.js +363 -0
- package/dist/browser/routes/agent.js +535 -0
- package/dist/browser/routes/basic.js +155 -0
- package/dist/browser/routes/index.js +8 -0
- package/dist/browser/routes/tabs.js +105 -0
- package/dist/browser/routes/utils.js +62 -0
- package/dist/browser/screenshot.js +40 -0
- package/dist/browser/server-context.js +377 -0
- package/dist/browser/server.js +81 -0
- package/dist/browser/target-id.js +18 -0
- package/dist/browser/trash.js +21 -0
- package/dist/canvas-host/a2ui/.bundle.hash +1 -0
- package/dist/canvas-host/a2ui/a2ui.bundle.js +17768 -0
- package/dist/canvas-host/a2ui/index.html +246 -0
- package/dist/canvas-host/a2ui.js +187 -0
- package/dist/canvas-host/server.js +382 -0
- package/dist/cli/browser-cli-actions-input.js +459 -0
- package/dist/cli/browser-cli-actions-observe.js +56 -0
- package/dist/cli/browser-cli-examples.js +31 -0
- package/dist/cli/browser-cli-inspect.js +97 -0
- package/dist/cli/browser-cli-manage.js +286 -0
- package/dist/cli/browser-cli-shared.js +1 -0
- package/dist/cli/browser-cli.js +26 -0
- package/dist/cli/canvas-cli.js +416 -0
- package/dist/cli/cron-cli.js +454 -0
- package/dist/cli/deps.js +17 -0
- package/dist/cli/dns-cli.js +180 -0
- package/dist/cli/gateway-cli.js +489 -0
- package/dist/cli/gateway-rpc.js +20 -0
- package/dist/cli/hooks-cli.js +135 -0
- package/dist/cli/models-cli.js +248 -0
- package/dist/cli/nodes-camera.js +57 -0
- package/dist/cli/nodes-canvas.js +26 -0
- package/dist/cli/nodes-cli.js +946 -0
- package/dist/cli/nodes-screen.js +37 -0
- package/dist/cli/parse-duration.js +20 -0
- package/dist/cli/ports.js +97 -0
- package/dist/cli/program.js +406 -0
- package/dist/cli/prompt.js +19 -0
- package/dist/cli/tui-cli.js +35 -0
- package/dist/cli/wait.js +8 -0
- package/dist/commands/agent.js +645 -0
- package/dist/commands/antigravity-oauth.js +327 -0
- package/dist/commands/configure.js +480 -0
- package/dist/commands/doctor.js +484 -0
- package/dist/commands/health.js +108 -0
- package/dist/commands/models/aliases.js +64 -0
- package/dist/commands/models/fallbacks.js +99 -0
- package/dist/commands/models/image-fallbacks.js +99 -0
- package/dist/commands/models/list.js +323 -0
- package/dist/commands/models/scan.js +266 -0
- package/dist/commands/models/set-image.js +23 -0
- package/dist/commands/models/set.js +23 -0
- package/dist/commands/models/shared.js +72 -0
- package/dist/commands/models.js +7 -0
- package/dist/commands/onboard-auth.js +70 -0
- package/dist/commands/onboard-helpers.js +295 -0
- package/dist/commands/onboard-interactive.js +17 -0
- package/dist/commands/onboard-non-interactive.js +202 -0
- package/dist/commands/onboard-providers.js +634 -0
- package/dist/commands/onboard-remote.js +120 -0
- package/dist/commands/onboard-skills.js +148 -0
- package/dist/commands/onboard-types.js +1 -0
- package/dist/commands/onboard.js +12 -0
- package/dist/commands/send.js +124 -0
- package/dist/commands/sessions.js +212 -0
- package/dist/commands/setup.js +58 -0
- package/dist/commands/signal-install.js +135 -0
- package/dist/commands/status.js +207 -0
- package/dist/commands/update.js +16 -0
- package/dist/config/config.js +6 -0
- package/dist/config/defaults.js +61 -0
- package/dist/config/io.js +147 -0
- package/dist/config/legacy-migrate.js +13 -0
- package/dist/config/legacy.js +159 -0
- package/dist/config/paths.js +71 -0
- package/dist/config/schema.js +150 -0
- package/dist/config/sessions.js +282 -0
- package/dist/config/talk.js +31 -0
- package/dist/config/types.js +1 -0
- package/dist/config/validation.js +29 -0
- package/dist/config/zod-schema.js +831 -0
- package/dist/control-ui/assets/index-BFID3yAA.css +1 -0
- package/dist/control-ui/assets/index-CE_axlTS.js +2235 -0
- package/dist/control-ui/assets/index-CE_axlTS.js.map +1 -0
- package/dist/control-ui/index.html +15 -0
- package/dist/cron/isolated-agent.js +499 -0
- package/dist/cron/run-log.js +72 -0
- package/dist/cron/schedule.js +24 -0
- package/dist/cron/service.js +471 -0
- package/dist/cron/store.js +43 -0
- package/dist/cron/types.js +1 -0
- package/dist/daemon/constants.js +10 -0
- package/dist/daemon/launchd.js +276 -0
- package/dist/daemon/legacy.js +63 -0
- package/dist/daemon/program-args.js +76 -0
- package/dist/daemon/schtasks.js +257 -0
- package/dist/daemon/service.js +60 -0
- package/dist/daemon/systemd.js +266 -0
- package/dist/discord/index.js +2 -0
- package/dist/discord/monitor.js +1188 -0
- package/dist/discord/probe.js +54 -0
- package/dist/discord/send.js +577 -0
- package/dist/discord/token.js +8 -0
- package/dist/gateway/auth.js +121 -0
- package/dist/gateway/call.js +94 -0
- package/dist/gateway/chat-attachments.js +41 -0
- package/dist/gateway/client.js +180 -0
- package/dist/gateway/config-reload.js +274 -0
- package/dist/gateway/control-ui.js +184 -0
- package/dist/gateway/hooks-mapping.js +282 -0
- package/dist/gateway/hooks.js +168 -0
- package/dist/gateway/net.js +29 -0
- package/dist/gateway/protocol/index.js +61 -0
- package/dist/gateway/protocol/schema.js +560 -0
- package/dist/gateway/server-bridge-subscriptions.js +93 -0
- package/dist/gateway/server-bridge.js +1013 -0
- package/dist/gateway/server-browser.js +12 -0
- package/dist/gateway/server-chat.js +159 -0
- package/dist/gateway/server-constants.js +8 -0
- package/dist/gateway/server-discovery.js +62 -0
- package/dist/gateway/server-http.js +165 -0
- package/dist/gateway/server-methods/agent-job.js +125 -0
- package/dist/gateway/server-methods/agent.js +250 -0
- package/dist/gateway/server-methods/chat.js +200 -0
- package/dist/gateway/server-methods/config.js +50 -0
- package/dist/gateway/server-methods/connect.js +6 -0
- package/dist/gateway/server-methods/cron.js +83 -0
- package/dist/gateway/server-methods/health.js +28 -0
- package/dist/gateway/server-methods/models.js +16 -0
- package/dist/gateway/server-methods/nodes.js +294 -0
- package/dist/gateway/server-methods/providers.js +217 -0
- package/dist/gateway/server-methods/send.js +166 -0
- package/dist/gateway/server-methods/sessions.js +305 -0
- package/dist/gateway/server-methods/skills.js +83 -0
- package/dist/gateway/server-methods/system.js +118 -0
- package/dist/gateway/server-methods/talk.js +22 -0
- package/dist/gateway/server-methods/types.js +1 -0
- package/dist/gateway/server-methods/voicewake.js +30 -0
- package/dist/gateway/server-methods/web.js +58 -0
- package/dist/gateway/server-methods/wizard.js +100 -0
- package/dist/gateway/server-methods.js +53 -0
- package/dist/gateway/server-providers.js +644 -0
- package/dist/gateway/server-shared.js +1 -0
- package/dist/gateway/server-utils.js +35 -0
- package/dist/gateway/server.js +1437 -0
- package/dist/gateway/session-utils.js +216 -0
- package/dist/gateway/ws-log.js +349 -0
- package/dist/gateway/ws-logging.js +8 -0
- package/dist/globals.js +41 -0
- package/dist/hooks/gmail-ops.js +236 -0
- package/dist/hooks/gmail-setup-utils.js +278 -0
- package/dist/hooks/gmail-watcher.js +175 -0
- package/dist/hooks/gmail.js +177 -0
- package/dist/imessage/client.js +165 -0
- package/dist/imessage/index.js +3 -0
- package/dist/imessage/monitor.js +272 -0
- package/dist/imessage/probe.js +26 -0
- package/dist/imessage/send.js +83 -0
- package/dist/imessage/targets.js +176 -0
- package/dist/index.js +50 -0
- package/dist/infra/agent-events.js +46 -0
- package/dist/infra/binaries.js +9 -0
- package/dist/infra/bonjour-discovery.js +163 -0
- package/dist/infra/bonjour.js +200 -0
- package/dist/infra/bridge/server.js +562 -0
- package/dist/infra/canvas-host-url.js +54 -0
- package/dist/infra/env.js +8 -0
- package/dist/infra/errors.js +28 -0
- package/dist/infra/gateway-lock.js +8 -0
- package/dist/infra/heartbeat-events.js +21 -0
- package/dist/infra/heartbeat-runner.js +453 -0
- package/dist/infra/heartbeat-wake.js +61 -0
- package/dist/infra/is-main.js +37 -0
- package/dist/infra/machine-name.js +40 -0
- package/dist/infra/node-pairing.js +211 -0
- package/dist/infra/pam.js +42 -0
- package/dist/infra/path-env.js +92 -0
- package/dist/infra/ports.js +87 -0
- package/dist/infra/provider-summary.js +80 -0
- package/dist/infra/restart.js +29 -0
- package/dist/infra/retry.js +16 -0
- package/dist/infra/runtime-guard.js +59 -0
- package/dist/infra/system-events.js +44 -0
- package/dist/infra/system-presence.js +216 -0
- package/dist/infra/tailnet.js +46 -0
- package/dist/infra/tailscale.js +149 -0
- package/dist/infra/voicewake.js +77 -0
- package/dist/infra/widearea-dns.js +123 -0
- package/dist/infra/ws.js +13 -0
- package/dist/logger.js +52 -0
- package/dist/logging.js +490 -0
- package/dist/macos/gateway-daemon.js +141 -0
- package/dist/macos/relay.js +46 -0
- package/dist/media/constants.js +33 -0
- package/dist/media/host.js +42 -0
- package/dist/media/image-ops.js +121 -0
- package/dist/media/mime.js +115 -0
- package/dist/media/parse.js +81 -0
- package/dist/media/server.js +64 -0
- package/dist/media/store.js +139 -0
- package/dist/process/command-queue.js +97 -0
- package/dist/process/exec.js +75 -0
- package/dist/protocol.schema.json +2918 -0
- package/dist/provider-web.js +8 -0
- package/dist/providers/web/index.js +2 -0
- package/dist/runtime.js +8 -0
- package/dist/sessions/send-policy.js +68 -0
- package/dist/signal/client.js +134 -0
- package/dist/signal/daemon.js +69 -0
- package/dist/signal/index.js +3 -0
- package/dist/signal/monitor.js +336 -0
- package/dist/signal/probe.js +46 -0
- package/dist/signal/send.js +91 -0
- package/dist/slack/actions.js +97 -0
- package/dist/slack/index.js +5 -0
- package/dist/slack/monitor.js +1029 -0
- package/dist/slack/probe.js +47 -0
- package/dist/slack/send.js +131 -0
- package/dist/slack/token.js +10 -0
- package/dist/telegram/bot.js +394 -0
- package/dist/telegram/download.js +34 -0
- package/dist/telegram/index.js +4 -0
- package/dist/telegram/monitor.js +47 -0
- package/dist/telegram/probe.js +63 -0
- package/dist/telegram/proxy.js +9 -0
- package/dist/telegram/send.js +138 -0
- package/dist/telegram/token.js +30 -0
- package/dist/telegram/webhook-set.js +12 -0
- package/dist/telegram/webhook.js +56 -0
- package/dist/tui/commands.js +74 -0
- package/dist/tui/components/assistant-message.js +16 -0
- package/dist/tui/components/chat-log.js +92 -0
- package/dist/tui/components/custom-editor.js +53 -0
- package/dist/tui/components/selectors.js +8 -0
- package/dist/tui/components/tool-execution.js +111 -0
- package/dist/tui/components/user-message.js +17 -0
- package/dist/tui/gateway-chat.js +140 -0
- package/dist/tui/layout.js +41 -0
- package/dist/tui/message-list.js +57 -0
- package/dist/tui/theme/theme.js +80 -0
- package/dist/tui/theme.js +25 -0
- package/dist/tui/tui.js +708 -0
- package/dist/utils.js +133 -0
- package/dist/version.js +18 -0
- package/dist/web/active-listener.js +7 -0
- package/dist/web/auto-reply.js +1203 -0
- package/dist/web/inbound.js +481 -0
- package/dist/web/login-qr.js +204 -0
- package/dist/web/login.js +59 -0
- package/dist/web/media.js +148 -0
- package/dist/web/outbound.js +67 -0
- package/dist/web/qr-image.js +97 -0
- package/dist/web/reconnect.js +60 -0
- package/dist/web/reply-heartbeat-wake.js +61 -0
- package/dist/web/session.js +346 -0
- package/dist/wizard/clack-prompter.js +56 -0
- package/dist/wizard/onboarding.js +452 -0
- package/dist/wizard/prompts.js +6 -0
- package/dist/wizard/session.js +203 -0
- package/docs/AGENTS.default.md +116 -0
- package/docs/CNAME +1 -0
- package/docs/RELEASING.md +64 -0
- package/docs/_config.yml +51 -0
- package/docs/_layouts/default.html +145 -0
- package/docs/agent-send.md +21 -0
- package/docs/agent.md +104 -0
- package/docs/android/connect.md +131 -0
- package/docs/architecture.md +89 -0
- package/docs/assets/markdown.css +130 -0
- package/docs/assets/pixel-lobster.svg +60 -0
- package/docs/assets/terminal.css +497 -0
- package/docs/assets/theme.js +55 -0
- package/docs/audio.md +50 -0
- package/docs/background-process.md +74 -0
- package/docs/bash.md +32 -0
- package/docs/bonjour.md +159 -0
- package/docs/browser.md +289 -0
- package/docs/camera.md +152 -0
- package/docs/clawd.md +199 -0
- package/docs/clawdbot-mac.md +104 -0
- package/docs/configuration.md +1177 -0
- package/docs/control-api.md +49 -0
- package/docs/control-ui.md +83 -0
- package/docs/cron.md +374 -0
- package/docs/dashboard.md +17 -0
- package/docs/device-models.md +46 -0
- package/docs/discord.md +293 -0
- package/docs/discovery.md +112 -0
- package/docs/docker.md +251 -0
- package/docs/docs.json +86 -0
- package/docs/doctor.md +47 -0
- package/docs/elevated.md +31 -0
- package/docs/faq.md +640 -0
- package/docs/gateway/pairing.md +109 -0
- package/docs/gateway-lock.md +28 -0
- package/docs/gateway.md +174 -0
- package/docs/gmail-pubsub.md +191 -0
- package/docs/grammy.md +27 -0
- package/docs/group-messages.md +71 -0
- package/docs/groups.md +78 -0
- package/docs/health.md +28 -0
- package/docs/heartbeat.md +64 -0
- package/docs/images.md +52 -0
- package/docs/imessage.md +63 -0
- package/docs/index.md +182 -0
- package/docs/ios/connect.md +177 -0
- package/docs/ios/spec.md +236 -0
- package/docs/location-command.md +95 -0
- package/docs/logging.md +99 -0
- package/docs/lore.md +131 -0
- package/docs/mac/bun.md +133 -0
- package/docs/mac/canvas.md +161 -0
- package/docs/mac/child-process.md +72 -0
- package/docs/mac/dev-setup.md +81 -0
- package/docs/mac/health.md +28 -0
- package/docs/mac/icon.md +26 -0
- package/docs/mac/logging.md +51 -0
- package/docs/mac/menu-bar.md +69 -0
- package/docs/mac/peekaboo.md +170 -0
- package/docs/mac/permissions.md +40 -0
- package/docs/mac/release.md +76 -0
- package/docs/mac/remote.md +57 -0
- package/docs/mac/signing.md +41 -0
- package/docs/mac/skills.md +27 -0
- package/docs/mac/voice-overlay.md +52 -0
- package/docs/mac/voicewake.md +56 -0
- package/docs/mac/webchat.md +27 -0
- package/docs/mac/xpc.md +40 -0
- package/docs/models.md +90 -0
- package/docs/nix.md +49 -0
- package/docs/nodes.md +157 -0
- package/docs/onboarding-config-protocol.md +29 -0
- package/docs/onboarding.md +185 -0
- package/docs/presence.md +133 -0
- package/docs/queue.md +78 -0
- package/docs/refactor/browser-control-simplification.md +58 -0
- package/docs/refactor/canvas-a2ui.md +93 -0
- package/docs/refactor/cli-unification.md +64 -0
- package/docs/refactor/gateway-client.md +31 -0
- package/docs/refactor/gateway.md +99 -0
- package/docs/refactor/new-arch.md +171 -0
- package/docs/refactor/tui.md +26 -0
- package/docs/refactor/web-gateway-troubleshooting.md +37 -0
- package/docs/refactor/webagent-session.md +46 -0
- package/docs/remote-gateway-readme.md +148 -0
- package/docs/remote.md +66 -0
- package/docs/research/memory.md +227 -0
- package/docs/rpc.md +35 -0
- package/docs/security.md +168 -0
- package/docs/session-tool.md +119 -0
- package/docs/session.md +84 -0
- package/docs/sessions.md +8 -0
- package/docs/setup.md +118 -0
- package/docs/signal.md +113 -0
- package/docs/skills-config.md +58 -0
- package/docs/skills.md +149 -0
- package/docs/slack.md +158 -0
- package/docs/surface.md +20 -0
- package/docs/tailscale.md +71 -0
- package/docs/talk.md +79 -0
- package/docs/telegram.md +90 -0
- package/docs/templates/AGENTS.md +126 -0
- package/docs/templates/BOOTSTRAP.md +53 -0
- package/docs/templates/IDENTITY.md +17 -0
- package/docs/templates/SOUL.md +41 -0
- package/docs/templates/TOOLS.md +41 -0
- package/docs/templates/USER.md +22 -0
- package/docs/test.md +35 -0
- package/docs/thinking.md +46 -0
- package/docs/tools.md +248 -0
- package/docs/troubleshooting.md +227 -0
- package/docs/tui.md +69 -0
- package/docs/typebox.md +42 -0
- package/docs/voicewake.md +61 -0
- package/docs/web.md +115 -0
- package/docs/webchat.md +34 -0
- package/docs/webhook.md +132 -0
- package/docs/whatsapp-clawd.jpg +0 -0
- package/docs/whatsapp.md +142 -0
- package/docs/wizard.md +158 -0
- package/package.json +186 -0
- package/skills/apple-notes/SKILL.md +50 -0
- package/skills/apple-reminders/SKILL.md +67 -0
- package/skills/bear-notes/SKILL.md +79 -0
- package/skills/bird/SKILL.md +25 -0
- package/skills/blogwatcher/SKILL.md +46 -0
- package/skills/blucli/SKILL.md +27 -0
- package/skills/brave-search/SKILL.md +30 -0
- package/skills/brave-search/scripts/content.mjs +53 -0
- package/skills/brave-search/scripts/search.mjs +79 -0
- package/skills/camsnap/SKILL.md +25 -0
- package/skills/clawdhub/SKILL.md +53 -0
- package/skills/coding-agent/SKILL.md +275 -0
- package/skills/discord/SKILL.md +369 -0
- package/skills/eightctl/SKILL.md +29 -0
- package/skills/food-order/SKILL.md +41 -0
- package/skills/gemini/SKILL.md +23 -0
- package/skills/gifgrep/SKILL.md +47 -0
- package/skills/github/SKILL.md +47 -0
- package/skills/gog/SKILL.md +36 -0
- package/skills/goplaces/SKILL.md +30 -0
- package/skills/imsg/SKILL.md +25 -0
- package/skills/local-places/SERVER_README.md +101 -0
- package/skills/local-places/SKILL.md +91 -0
- package/skills/local-places/pyproject.toml +27 -0
- package/skills/local-places/src/local_places/__init__.py +2 -0
- package/skills/local-places/src/local_places/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/local-places/src/local_places/__pycache__/google_places.cpython-314.pyc +0 -0
- package/skills/local-places/src/local_places/__pycache__/main.cpython-314.pyc +0 -0
- package/skills/local-places/src/local_places/__pycache__/schemas.cpython-314.pyc +0 -0
- package/skills/local-places/src/local_places/google_places.py +314 -0
- package/skills/local-places/src/local_places/main.py +65 -0
- package/skills/local-places/src/local_places/schemas.py +107 -0
- package/skills/mcporter/SKILL.md +38 -0
- package/skills/nano-banana-pro/SKILL.md +29 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +167 -0
- package/skills/nano-pdf/SKILL.md +20 -0
- package/skills/notion/SKILL.md +156 -0
- package/skills/obsidian/SKILL.md +55 -0
- package/skills/openai-image-gen/SKILL.md +31 -0
- package/skills/openai-image-gen/scripts/gen.py +173 -0
- package/skills/openai-whisper/SKILL.md +19 -0
- package/skills/openai-whisper-api/SKILL.md +43 -0
- package/skills/openai-whisper-api/scripts/transcribe.sh +85 -0
- package/skills/openhue/SKILL.md +30 -0
- package/skills/oracle/SKILL.md +105 -0
- package/skills/ordercli/SKILL.md +47 -0
- package/skills/peekaboo/SKILL.md +153 -0
- package/skills/qmd/SKILL.md +26 -0
- package/skills/sag/SKILL.md +62 -0
- package/skills/slack/SKILL.md +143 -0
- package/skills/songsee/SKILL.md +29 -0
- package/skills/sonoscli/SKILL.md +26 -0
- package/skills/spotify-player/SKILL.md +34 -0
- package/skills/summarize/SKILL.md +49 -0
- package/skills/things-mac/SKILL.md +61 -0
- package/skills/tmux/SKILL.md +121 -0
- package/skills/tmux/scripts/find-sessions.sh +112 -0
- package/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/skills/trello/SKILL.md +84 -0
- package/skills/video-frames/SKILL.md +29 -0
- package/skills/video-frames/scripts/frame.sh +81 -0
- package/skills/wacli/SKILL.md +42 -0
- package/skills/weather/SKILL.md +49 -0
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
import { ApplicationCommandOptionType, ChannelType, Client, Events, GatewayIntentBits, MessageType, Partials, } from "discord.js";
|
|
2
|
+
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
|
3
|
+
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
|
4
|
+
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|
5
|
+
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
|
6
|
+
import { loadConfig } from "../config/config.js";
|
|
7
|
+
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
|
8
|
+
import { danger, logVerbose, shouldLogVerbose, warn } from "../globals.js";
|
|
9
|
+
import { enqueueSystemEvent } from "../infra/system-events.js";
|
|
10
|
+
import { getChildLogger } from "../logging.js";
|
|
11
|
+
import { detectMime } from "../media/mime.js";
|
|
12
|
+
import { saveMediaBuffer } from "../media/store.js";
|
|
13
|
+
import { sendMessageDiscord } from "./send.js";
|
|
14
|
+
import { normalizeDiscordToken } from "./token.js";
|
|
15
|
+
export function resolveDiscordReplyTarget(opts) {
|
|
16
|
+
if (opts.replyToMode === "off")
|
|
17
|
+
return undefined;
|
|
18
|
+
const replyToId = opts.replyToId?.trim();
|
|
19
|
+
if (!replyToId)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (opts.replyToMode === "all")
|
|
22
|
+
return replyToId;
|
|
23
|
+
return opts.hasReplied ? undefined : replyToId;
|
|
24
|
+
}
|
|
25
|
+
function summarizeAllowList(list) {
|
|
26
|
+
if (!list || list.length === 0)
|
|
27
|
+
return "any";
|
|
28
|
+
const sample = list.slice(0, 4).map((entry) => String(entry));
|
|
29
|
+
const suffix = list.length > sample.length ? ` (+${list.length - sample.length})` : "";
|
|
30
|
+
return `${sample.join(", ")}${suffix}`;
|
|
31
|
+
}
|
|
32
|
+
function summarizeGuilds(entries) {
|
|
33
|
+
if (!entries || Object.keys(entries).length === 0)
|
|
34
|
+
return "any";
|
|
35
|
+
const keys = Object.keys(entries);
|
|
36
|
+
const sample = keys.slice(0, 4);
|
|
37
|
+
const suffix = keys.length > sample.length ? ` (+${keys.length - sample.length})` : "";
|
|
38
|
+
return `${sample.join(", ")}${suffix}`;
|
|
39
|
+
}
|
|
40
|
+
export async function monitorDiscordProvider(opts = {}) {
|
|
41
|
+
const cfg = loadConfig();
|
|
42
|
+
const token = normalizeDiscordToken(opts.token ??
|
|
43
|
+
process.env.DISCORD_BOT_TOKEN ??
|
|
44
|
+
cfg.discord?.token ??
|
|
45
|
+
undefined);
|
|
46
|
+
if (!token) {
|
|
47
|
+
throw new Error("DISCORD_BOT_TOKEN or discord.token is required for Discord gateway");
|
|
48
|
+
}
|
|
49
|
+
const runtime = opts.runtime ?? {
|
|
50
|
+
log: console.log,
|
|
51
|
+
error: console.error,
|
|
52
|
+
exit: (code) => {
|
|
53
|
+
throw new Error(`exit ${code}`);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const dmConfig = cfg.discord?.dm;
|
|
57
|
+
const guildEntries = cfg.discord?.guilds;
|
|
58
|
+
const allowFrom = dmConfig?.allowFrom;
|
|
59
|
+
const slashCommand = resolveSlashCommandConfig(opts.slashCommand ?? cfg.discord?.slashCommand);
|
|
60
|
+
const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024;
|
|
61
|
+
const textLimit = resolveTextChunkLimit(cfg, "discord");
|
|
62
|
+
const historyLimit = Math.max(0, opts.historyLimit ?? cfg.discord?.historyLimit ?? 20);
|
|
63
|
+
const replyToMode = opts.replyToMode ?? cfg.discord?.replyToMode ?? "off";
|
|
64
|
+
const dmEnabled = dmConfig?.enabled ?? true;
|
|
65
|
+
const groupDmEnabled = dmConfig?.groupEnabled ?? false;
|
|
66
|
+
const groupDmChannels = dmConfig?.groupChannels;
|
|
67
|
+
if (shouldLogVerbose()) {
|
|
68
|
+
logVerbose(`discord: config dm=${dmEnabled ? "on" : "off"} allowFrom=${summarizeAllowList(allowFrom)} groupDm=${groupDmEnabled ? "on" : "off"} groupDmChannels=${summarizeAllowList(groupDmChannels)} guilds=${summarizeGuilds(guildEntries)} historyLimit=${historyLimit} mediaMaxMb=${Math.round(mediaMaxBytes / (1024 * 1024))}`);
|
|
69
|
+
}
|
|
70
|
+
const client = new Client({
|
|
71
|
+
intents: [
|
|
72
|
+
GatewayIntentBits.Guilds,
|
|
73
|
+
GatewayIntentBits.GuildMessages,
|
|
74
|
+
GatewayIntentBits.GuildMessageReactions,
|
|
75
|
+
GatewayIntentBits.MessageContent,
|
|
76
|
+
GatewayIntentBits.DirectMessages,
|
|
77
|
+
GatewayIntentBits.DirectMessageReactions,
|
|
78
|
+
],
|
|
79
|
+
partials: [
|
|
80
|
+
Partials.Channel,
|
|
81
|
+
Partials.Message,
|
|
82
|
+
Partials.Reaction,
|
|
83
|
+
Partials.User,
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
const logger = getChildLogger({ module: "discord-auto-reply" });
|
|
87
|
+
const guildHistories = new Map();
|
|
88
|
+
client.once(Events.ClientReady, () => {
|
|
89
|
+
runtime.log?.(`logged in as ${client.user?.tag ?? "unknown"}`);
|
|
90
|
+
if (slashCommand.enabled) {
|
|
91
|
+
void ensureSlashCommand(client, slashCommand, runtime);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
client.on(Events.Error, (err) => {
|
|
95
|
+
runtime.error?.(danger(`client error: ${String(err)}`));
|
|
96
|
+
});
|
|
97
|
+
client.on(Events.MessageCreate, async (message) => {
|
|
98
|
+
try {
|
|
99
|
+
if (message.author?.bot)
|
|
100
|
+
return;
|
|
101
|
+
if (!message.author)
|
|
102
|
+
return;
|
|
103
|
+
// Discord.js typing excludes GroupDM for message.channel.type; widen for runtime check.
|
|
104
|
+
const channelType = message.channel.type;
|
|
105
|
+
const isGroupDm = channelType === ChannelType.GroupDM;
|
|
106
|
+
const isDirectMessage = channelType === ChannelType.DM;
|
|
107
|
+
const isGuildMessage = Boolean(message.guild);
|
|
108
|
+
if (isGroupDm && !groupDmEnabled) {
|
|
109
|
+
logVerbose("discord: drop group dm (group dms disabled)");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (isDirectMessage && !dmEnabled) {
|
|
113
|
+
logVerbose("discord: drop dm (dms disabled)");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const botId = client.user?.id;
|
|
117
|
+
const wasMentioned = !isDirectMessage && Boolean(botId && message.mentions.has(botId));
|
|
118
|
+
const forwardedSnapshot = resolveForwardedSnapshot(message);
|
|
119
|
+
const forwardedText = forwardedSnapshot
|
|
120
|
+
? resolveDiscordSnapshotText(forwardedSnapshot.snapshot)
|
|
121
|
+
: "";
|
|
122
|
+
const baseText = resolveDiscordMessageText(message, forwardedText);
|
|
123
|
+
if (shouldLogVerbose()) {
|
|
124
|
+
logVerbose(`discord: inbound id=${message.id} guild=${message.guild?.id ?? "dm"} channel=${message.channelId} mention=${wasMentioned ? "yes" : "no"} type=${isDirectMessage ? "dm" : isGroupDm ? "group-dm" : "guild"} content=${baseText ? "yes" : "no"}`);
|
|
125
|
+
}
|
|
126
|
+
if (isGuildMessage &&
|
|
127
|
+
(message.type === MessageType.ChatInputCommand ||
|
|
128
|
+
message.type === MessageType.ContextMenuCommand)) {
|
|
129
|
+
logVerbose("discord: drop channel command message");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const guildInfo = isGuildMessage
|
|
133
|
+
? resolveDiscordGuildEntry({
|
|
134
|
+
guild: message.guild,
|
|
135
|
+
guildEntries,
|
|
136
|
+
})
|
|
137
|
+
: null;
|
|
138
|
+
if (isGuildMessage &&
|
|
139
|
+
guildEntries &&
|
|
140
|
+
Object.keys(guildEntries).length > 0 &&
|
|
141
|
+
!guildInfo) {
|
|
142
|
+
logVerbose(`Blocked discord guild ${message.guild?.id ?? "unknown"} (not in discord.guilds)`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const channelName = (isGuildMessage || isGroupDm) && "name" in message.channel
|
|
146
|
+
? message.channel.name
|
|
147
|
+
: undefined;
|
|
148
|
+
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
|
149
|
+
const guildSlug = guildInfo?.slug ||
|
|
150
|
+
(message.guild?.name ? normalizeDiscordSlug(message.guild.name) : "");
|
|
151
|
+
const channelConfig = isGuildMessage
|
|
152
|
+
? resolveDiscordChannelConfig({
|
|
153
|
+
guildInfo,
|
|
154
|
+
channelId: message.channelId,
|
|
155
|
+
channelName,
|
|
156
|
+
channelSlug,
|
|
157
|
+
})
|
|
158
|
+
: null;
|
|
159
|
+
const groupDmAllowed = isGroupDm &&
|
|
160
|
+
resolveGroupDmAllow({
|
|
161
|
+
channels: groupDmChannels,
|
|
162
|
+
channelId: message.channelId,
|
|
163
|
+
channelName,
|
|
164
|
+
channelSlug,
|
|
165
|
+
});
|
|
166
|
+
if (isGroupDm && !groupDmAllowed)
|
|
167
|
+
return;
|
|
168
|
+
if (isGuildMessage && channelConfig?.allowed === false) {
|
|
169
|
+
logVerbose(`Blocked discord channel ${message.channelId} not in guild channel allowlist`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (isGuildMessage && historyLimit > 0 && baseText) {
|
|
173
|
+
const history = guildHistories.get(message.channelId) ?? [];
|
|
174
|
+
history.push({
|
|
175
|
+
sender: message.member?.displayName ?? message.author.tag,
|
|
176
|
+
body: baseText,
|
|
177
|
+
timestamp: message.createdTimestamp,
|
|
178
|
+
messageId: message.id,
|
|
179
|
+
});
|
|
180
|
+
while (history.length > historyLimit)
|
|
181
|
+
history.shift();
|
|
182
|
+
guildHistories.set(message.channelId, history);
|
|
183
|
+
}
|
|
184
|
+
const resolvedRequireMention = channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
|
|
185
|
+
if (isGuildMessage && resolvedRequireMention) {
|
|
186
|
+
if (botId && !wasMentioned) {
|
|
187
|
+
logVerbose(`discord: drop guild message (mention required, botId=${botId})`);
|
|
188
|
+
logger.info({
|
|
189
|
+
channelId: message.channelId,
|
|
190
|
+
reason: "no-mention",
|
|
191
|
+
}, "discord: skipping guild message");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (isGuildMessage) {
|
|
196
|
+
const userAllow = guildInfo?.users;
|
|
197
|
+
if (Array.isArray(userAllow) && userAllow.length > 0) {
|
|
198
|
+
const users = normalizeDiscordAllowList(userAllow, [
|
|
199
|
+
"discord:",
|
|
200
|
+
"user:",
|
|
201
|
+
]);
|
|
202
|
+
const userOk = !users ||
|
|
203
|
+
allowListMatches(users, {
|
|
204
|
+
id: message.author.id,
|
|
205
|
+
name: message.author.username,
|
|
206
|
+
tag: message.author.tag,
|
|
207
|
+
});
|
|
208
|
+
if (!userOk) {
|
|
209
|
+
logVerbose(`Blocked discord guild sender ${message.author.id} (not in guild users allowlist)`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (isDirectMessage && Array.isArray(allowFrom) && allowFrom.length > 0) {
|
|
215
|
+
const allowList = normalizeDiscordAllowList(allowFrom, [
|
|
216
|
+
"discord:",
|
|
217
|
+
"user:",
|
|
218
|
+
]);
|
|
219
|
+
const permitted = allowList &&
|
|
220
|
+
allowListMatches(allowList, {
|
|
221
|
+
id: message.author.id,
|
|
222
|
+
name: message.author.username,
|
|
223
|
+
tag: message.author.tag,
|
|
224
|
+
});
|
|
225
|
+
if (!permitted) {
|
|
226
|
+
logVerbose(`Blocked unauthorized discord sender ${message.author.id} (not in allowFrom)`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const systemText = resolveDiscordSystemEvent(message);
|
|
231
|
+
if (systemText) {
|
|
232
|
+
enqueueSystemEvent(systemText, {
|
|
233
|
+
contextKey: `discord:system:${message.channelId}:${message.id}`,
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const media = await resolveMedia(message, mediaMaxBytes);
|
|
238
|
+
const text = message.content?.trim() ??
|
|
239
|
+
media?.placeholder ??
|
|
240
|
+
message.embeds[0]?.description ??
|
|
241
|
+
(forwardedSnapshot ? "<forwarded message>" : "");
|
|
242
|
+
if (!text) {
|
|
243
|
+
logVerbose(`discord: drop message ${message.id} (empty content)`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const fromLabel = isDirectMessage
|
|
247
|
+
? buildDirectLabel(message)
|
|
248
|
+
: buildGuildLabel(message);
|
|
249
|
+
const groupRoom = isGuildMessage && channelSlug ? `#${channelSlug}` : undefined;
|
|
250
|
+
const groupSubject = isDirectMessage ? undefined : groupRoom;
|
|
251
|
+
const messageText = text;
|
|
252
|
+
let combinedBody = formatAgentEnvelope({
|
|
253
|
+
surface: "Discord",
|
|
254
|
+
from: fromLabel,
|
|
255
|
+
timestamp: message.createdTimestamp,
|
|
256
|
+
body: messageText,
|
|
257
|
+
});
|
|
258
|
+
let shouldClearHistory = false;
|
|
259
|
+
if (!isDirectMessage) {
|
|
260
|
+
const history = historyLimit > 0 ? (guildHistories.get(message.channelId) ?? []) : [];
|
|
261
|
+
const historyWithoutCurrent = history.length > 0 ? history.slice(0, -1) : [];
|
|
262
|
+
if (historyWithoutCurrent.length > 0) {
|
|
263
|
+
const historyText = historyWithoutCurrent
|
|
264
|
+
.map((entry) => formatAgentEnvelope({
|
|
265
|
+
surface: "Discord",
|
|
266
|
+
from: fromLabel,
|
|
267
|
+
timestamp: entry.timestamp,
|
|
268
|
+
body: `${entry.sender}: ${entry.body} [id:${entry.messageId ?? "unknown"} channel:${message.channelId}]`,
|
|
269
|
+
}))
|
|
270
|
+
.join("\n");
|
|
271
|
+
combinedBody = `[Chat messages since your last reply - for context]\n${historyText}\n\n[Current message - respond to this]\n${combinedBody}`;
|
|
272
|
+
}
|
|
273
|
+
const name = message.author.tag;
|
|
274
|
+
const id = message.author.id;
|
|
275
|
+
combinedBody = `${combinedBody}\n[from: ${name} user id:${id}]`;
|
|
276
|
+
shouldClearHistory = true;
|
|
277
|
+
}
|
|
278
|
+
const replyContext = await resolveReplyContext(message);
|
|
279
|
+
if (replyContext) {
|
|
280
|
+
combinedBody = `[Replied message - for context]\n${replyContext}\n\n${combinedBody}`;
|
|
281
|
+
}
|
|
282
|
+
if (forwardedSnapshot) {
|
|
283
|
+
const forwarderName = message.author.tag ?? message.author.username;
|
|
284
|
+
const forwarder = forwarderName
|
|
285
|
+
? `${forwarderName} id:${message.author.id}`
|
|
286
|
+
: message.author.id;
|
|
287
|
+
const snapshotText = resolveDiscordSnapshotText(forwardedSnapshot.snapshot) ||
|
|
288
|
+
"<forwarded message>";
|
|
289
|
+
const forwardMetaParts = [
|
|
290
|
+
forwardedSnapshot.messageId
|
|
291
|
+
? `forwarded message id: ${forwardedSnapshot.messageId}`
|
|
292
|
+
: null,
|
|
293
|
+
forwardedSnapshot.channelId
|
|
294
|
+
? `channel: ${forwardedSnapshot.channelId}`
|
|
295
|
+
: null,
|
|
296
|
+
forwardedSnapshot.guildId
|
|
297
|
+
? `guild: ${forwardedSnapshot.guildId}`
|
|
298
|
+
: null,
|
|
299
|
+
typeof forwardedSnapshot.snapshot.type === "number"
|
|
300
|
+
? `snapshot type: ${forwardedSnapshot.snapshot.type}`
|
|
301
|
+
: null,
|
|
302
|
+
].filter((entry) => Boolean(entry));
|
|
303
|
+
const forwardedBody = forwardMetaParts.length
|
|
304
|
+
? `${snapshotText}\n[${forwardMetaParts.join(" ")}]`
|
|
305
|
+
: snapshotText;
|
|
306
|
+
const forwardedEnvelope = formatAgentEnvelope({
|
|
307
|
+
surface: "Discord",
|
|
308
|
+
from: `Forwarded by ${forwarder}`,
|
|
309
|
+
timestamp: forwardedSnapshot.snapshot.createdTimestamp ??
|
|
310
|
+
message.createdTimestamp ??
|
|
311
|
+
undefined,
|
|
312
|
+
body: forwardedBody,
|
|
313
|
+
});
|
|
314
|
+
combinedBody = `[Forwarded message]\n${forwardedEnvelope}\n\n${combinedBody}`;
|
|
315
|
+
}
|
|
316
|
+
const ctxPayload = {
|
|
317
|
+
Body: combinedBody,
|
|
318
|
+
From: isDirectMessage
|
|
319
|
+
? `discord:${message.author.id}`
|
|
320
|
+
: `group:${message.channelId}`,
|
|
321
|
+
To: isDirectMessage
|
|
322
|
+
? `user:${message.author.id}`
|
|
323
|
+
: `channel:${message.channelId}`,
|
|
324
|
+
ChatType: isDirectMessage ? "direct" : "group",
|
|
325
|
+
SenderName: message.member?.displayName ?? message.author.tag,
|
|
326
|
+
SenderUsername: message.author.username,
|
|
327
|
+
SenderTag: message.author.tag,
|
|
328
|
+
GroupSubject: groupSubject,
|
|
329
|
+
GroupRoom: groupRoom,
|
|
330
|
+
GroupSpace: isGuildMessage ? guildSlug || undefined : undefined,
|
|
331
|
+
Surface: "discord",
|
|
332
|
+
WasMentioned: wasMentioned,
|
|
333
|
+
MessageSid: message.id,
|
|
334
|
+
Timestamp: message.createdTimestamp,
|
|
335
|
+
MediaPath: media?.path,
|
|
336
|
+
MediaType: media?.contentType,
|
|
337
|
+
MediaUrl: media?.path,
|
|
338
|
+
};
|
|
339
|
+
const replyTarget = ctxPayload.To ?? undefined;
|
|
340
|
+
if (!replyTarget) {
|
|
341
|
+
runtime.error?.(danger("discord: missing reply target"));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (isDirectMessage) {
|
|
345
|
+
const sessionCfg = cfg.session;
|
|
346
|
+
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
|
|
347
|
+
const storePath = resolveStorePath(sessionCfg?.store);
|
|
348
|
+
await updateLastRoute({
|
|
349
|
+
storePath,
|
|
350
|
+
sessionKey: mainKey,
|
|
351
|
+
channel: "discord",
|
|
352
|
+
to: `user:${message.author.id}`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (shouldLogVerbose()) {
|
|
356
|
+
const preview = combinedBody.slice(0, 200).replace(/\n/g, "\\n");
|
|
357
|
+
logVerbose(`discord inbound: channel=${message.channelId} from=${ctxPayload.From} preview="${preview}"`);
|
|
358
|
+
}
|
|
359
|
+
let didSendReply = false;
|
|
360
|
+
let blockSendChain = Promise.resolve();
|
|
361
|
+
const sendBlockReply = (payload) => {
|
|
362
|
+
if (!payload?.text &&
|
|
363
|
+
!payload?.mediaUrl &&
|
|
364
|
+
!(payload?.mediaUrls?.length ?? 0)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
blockSendChain = blockSendChain
|
|
368
|
+
.then(async () => {
|
|
369
|
+
await deliverReplies({
|
|
370
|
+
replies: [payload],
|
|
371
|
+
target: replyTarget,
|
|
372
|
+
token,
|
|
373
|
+
runtime,
|
|
374
|
+
replyToMode,
|
|
375
|
+
textLimit,
|
|
376
|
+
});
|
|
377
|
+
didSendReply = true;
|
|
378
|
+
})
|
|
379
|
+
.catch((err) => {
|
|
380
|
+
runtime.error?.(danger(`discord block reply failed: ${String(err)}`));
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
const replyResult = await getReplyFromConfig(ctxPayload, {
|
|
384
|
+
onReplyStart: () => sendTyping(message),
|
|
385
|
+
onBlockReply: sendBlockReply,
|
|
386
|
+
}, cfg);
|
|
387
|
+
const replies = replyResult
|
|
388
|
+
? Array.isArray(replyResult)
|
|
389
|
+
? replyResult
|
|
390
|
+
: [replyResult]
|
|
391
|
+
: [];
|
|
392
|
+
await blockSendChain;
|
|
393
|
+
if (replies.length === 0) {
|
|
394
|
+
if (isGuildMessage &&
|
|
395
|
+
shouldClearHistory &&
|
|
396
|
+
historyLimit > 0 &&
|
|
397
|
+
didSendReply) {
|
|
398
|
+
guildHistories.set(message.channelId, []);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
await deliverReplies({
|
|
403
|
+
replies,
|
|
404
|
+
target: replyTarget,
|
|
405
|
+
token,
|
|
406
|
+
runtime,
|
|
407
|
+
replyToMode,
|
|
408
|
+
textLimit,
|
|
409
|
+
});
|
|
410
|
+
didSendReply = true;
|
|
411
|
+
if (shouldLogVerbose()) {
|
|
412
|
+
logVerbose(`discord: delivered ${replies.length} reply${replies.length === 1 ? "" : "ies"} to ${replyTarget}`);
|
|
413
|
+
}
|
|
414
|
+
if (isGuildMessage &&
|
|
415
|
+
shouldClearHistory &&
|
|
416
|
+
historyLimit > 0 &&
|
|
417
|
+
didSendReply) {
|
|
418
|
+
guildHistories.set(message.channelId, []);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
runtime.error?.(danger(`handler failed: ${String(err)}`));
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
const handleReactionEvent = async (reaction, user, action) => {
|
|
426
|
+
try {
|
|
427
|
+
if (!user || user.bot)
|
|
428
|
+
return;
|
|
429
|
+
const resolvedReaction = reaction.partial
|
|
430
|
+
? await reaction.fetch()
|
|
431
|
+
: reaction;
|
|
432
|
+
const message = resolvedReaction.message
|
|
433
|
+
.partial
|
|
434
|
+
? await resolvedReaction.message.fetch()
|
|
435
|
+
: resolvedReaction.message;
|
|
436
|
+
const guild = message.guild;
|
|
437
|
+
if (!guild)
|
|
438
|
+
return;
|
|
439
|
+
const guildInfo = resolveDiscordGuildEntry({
|
|
440
|
+
guild,
|
|
441
|
+
guildEntries,
|
|
442
|
+
});
|
|
443
|
+
if (guildEntries && Object.keys(guildEntries).length > 0 && !guildInfo) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const channelName = "name" in message.channel
|
|
447
|
+
? (message.channel.name ?? undefined)
|
|
448
|
+
: undefined;
|
|
449
|
+
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
|
450
|
+
const channelConfig = resolveDiscordChannelConfig({
|
|
451
|
+
guildInfo,
|
|
452
|
+
channelId: message.channelId,
|
|
453
|
+
channelName,
|
|
454
|
+
channelSlug,
|
|
455
|
+
});
|
|
456
|
+
if (channelConfig?.allowed === false)
|
|
457
|
+
return;
|
|
458
|
+
const botId = client.user?.id;
|
|
459
|
+
if (botId && user.id === botId)
|
|
460
|
+
return;
|
|
461
|
+
const reactionMode = guildInfo?.reactionNotifications ?? "own";
|
|
462
|
+
const shouldNotify = shouldEmitDiscordReactionNotification({
|
|
463
|
+
mode: reactionMode,
|
|
464
|
+
botId,
|
|
465
|
+
messageAuthorId: message.author?.id,
|
|
466
|
+
userId: user.id,
|
|
467
|
+
userName: user.username,
|
|
468
|
+
userTag: user.tag,
|
|
469
|
+
allowlist: guildInfo?.users,
|
|
470
|
+
});
|
|
471
|
+
if (!shouldNotify)
|
|
472
|
+
return;
|
|
473
|
+
const emojiLabel = formatDiscordReactionEmoji(resolvedReaction);
|
|
474
|
+
const actorLabel = user.tag ?? user.username ?? user.id;
|
|
475
|
+
const guildSlug = guildInfo?.slug ||
|
|
476
|
+
(guild.name ? normalizeDiscordSlug(guild.name) : guild.id);
|
|
477
|
+
const channelLabel = channelSlug
|
|
478
|
+
? `#${channelSlug}`
|
|
479
|
+
: channelName
|
|
480
|
+
? `#${normalizeDiscordSlug(channelName)}`
|
|
481
|
+
: `#${message.channelId}`;
|
|
482
|
+
const authorLabel = message.author?.tag ?? message.author?.username;
|
|
483
|
+
const baseText = `Discord reaction ${action}: ${emojiLabel} by ${actorLabel} on ${guildSlug} ${channelLabel} msg ${message.id}`;
|
|
484
|
+
const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
|
|
485
|
+
enqueueSystemEvent(text, {
|
|
486
|
+
contextKey: `discord:reaction:${action}:${message.id}:${user.id}:${emojiLabel}`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
runtime.error?.(danger(`discord reaction handler failed: ${String(err)}`));
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
client.on(Events.MessageReactionAdd, async (reaction, user) => {
|
|
494
|
+
await handleReactionEvent(reaction, user, "added");
|
|
495
|
+
});
|
|
496
|
+
client.on(Events.MessageReactionRemove, async (reaction, user) => {
|
|
497
|
+
await handleReactionEvent(reaction, user, "removed");
|
|
498
|
+
});
|
|
499
|
+
client.on(Events.InteractionCreate, async (interaction) => {
|
|
500
|
+
try {
|
|
501
|
+
if (!slashCommand.enabled)
|
|
502
|
+
return;
|
|
503
|
+
if (!interaction.isChatInputCommand())
|
|
504
|
+
return;
|
|
505
|
+
if (interaction.commandName !== slashCommand.name)
|
|
506
|
+
return;
|
|
507
|
+
if (interaction.user?.bot)
|
|
508
|
+
return;
|
|
509
|
+
const channelType = interaction.channel?.type;
|
|
510
|
+
const isGroupDm = channelType === ChannelType.GroupDM;
|
|
511
|
+
const isDirectMessage = !interaction.inGuild() && channelType === ChannelType.DM;
|
|
512
|
+
const isGuildMessage = interaction.inGuild();
|
|
513
|
+
if (isGroupDm && !groupDmEnabled) {
|
|
514
|
+
logVerbose("discord: drop slash (group dms disabled)");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (isDirectMessage && !dmEnabled) {
|
|
518
|
+
logVerbose("discord: drop slash (dms disabled)");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (shouldLogVerbose()) {
|
|
522
|
+
logVerbose(`discord: slash inbound guild=${interaction.guildId ?? "dm"} channel=${interaction.channelId} type=${isDirectMessage ? "dm" : isGroupDm ? "group-dm" : "guild"}`);
|
|
523
|
+
}
|
|
524
|
+
if (isGuildMessage) {
|
|
525
|
+
const guildInfo = resolveDiscordGuildEntry({
|
|
526
|
+
guild: interaction.guild ?? null,
|
|
527
|
+
guildEntries,
|
|
528
|
+
});
|
|
529
|
+
if (guildEntries &&
|
|
530
|
+
Object.keys(guildEntries).length > 0 &&
|
|
531
|
+
!guildInfo) {
|
|
532
|
+
logVerbose(`Blocked discord guild ${interaction.guildId ?? "unknown"} (not in discord.guilds)`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const channelName = interaction.channel &&
|
|
536
|
+
"name" in interaction.channel &&
|
|
537
|
+
typeof interaction.channel.name === "string"
|
|
538
|
+
? interaction.channel.name
|
|
539
|
+
: undefined;
|
|
540
|
+
const channelSlug = channelName
|
|
541
|
+
? normalizeDiscordSlug(channelName)
|
|
542
|
+
: "";
|
|
543
|
+
const channelConfig = resolveDiscordChannelConfig({
|
|
544
|
+
guildInfo,
|
|
545
|
+
channelId: interaction.channelId,
|
|
546
|
+
channelName,
|
|
547
|
+
channelSlug,
|
|
548
|
+
});
|
|
549
|
+
if (channelConfig?.allowed === false) {
|
|
550
|
+
logVerbose(`Blocked discord channel ${interaction.channelId} not in guild channel allowlist`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const userAllow = guildInfo?.users;
|
|
554
|
+
if (Array.isArray(userAllow) && userAllow.length > 0) {
|
|
555
|
+
const users = normalizeDiscordAllowList(userAllow, [
|
|
556
|
+
"discord:",
|
|
557
|
+
"user:",
|
|
558
|
+
]);
|
|
559
|
+
const userOk = !users ||
|
|
560
|
+
allowListMatches(users, {
|
|
561
|
+
id: interaction.user.id,
|
|
562
|
+
name: interaction.user.username,
|
|
563
|
+
tag: interaction.user.tag,
|
|
564
|
+
});
|
|
565
|
+
if (!userOk) {
|
|
566
|
+
logVerbose(`Blocked discord guild sender ${interaction.user.id} (not in guild users allowlist)`);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else if (isGroupDm) {
|
|
572
|
+
const channelName = interaction.channel &&
|
|
573
|
+
"name" in interaction.channel &&
|
|
574
|
+
typeof interaction.channel.name === "string"
|
|
575
|
+
? interaction.channel.name
|
|
576
|
+
: undefined;
|
|
577
|
+
const channelSlug = channelName
|
|
578
|
+
? normalizeDiscordSlug(channelName)
|
|
579
|
+
: "";
|
|
580
|
+
const groupDmAllowed = resolveGroupDmAllow({
|
|
581
|
+
channels: groupDmChannels,
|
|
582
|
+
channelId: interaction.channelId,
|
|
583
|
+
channelName,
|
|
584
|
+
channelSlug,
|
|
585
|
+
});
|
|
586
|
+
if (!groupDmAllowed)
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
else if (isDirectMessage) {
|
|
590
|
+
if (Array.isArray(allowFrom) && allowFrom.length > 0) {
|
|
591
|
+
const allowList = normalizeDiscordAllowList(allowFrom, [
|
|
592
|
+
"discord:",
|
|
593
|
+
"user:",
|
|
594
|
+
]);
|
|
595
|
+
const permitted = allowList &&
|
|
596
|
+
allowListMatches(allowList, {
|
|
597
|
+
id: interaction.user.id,
|
|
598
|
+
name: interaction.user.username,
|
|
599
|
+
tag: interaction.user.tag,
|
|
600
|
+
});
|
|
601
|
+
if (!permitted) {
|
|
602
|
+
logVerbose(`Blocked unauthorized discord sender ${interaction.user.id} (not in allowFrom)`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const prompt = resolveSlashPrompt(interaction.options.data);
|
|
608
|
+
if (!prompt) {
|
|
609
|
+
await interaction.reply({
|
|
610
|
+
content: "Message required.",
|
|
611
|
+
ephemeral: true,
|
|
612
|
+
});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
await interaction.deferReply({ ephemeral: slashCommand.ephemeral });
|
|
616
|
+
const userId = interaction.user.id;
|
|
617
|
+
const ctxPayload = {
|
|
618
|
+
Body: prompt,
|
|
619
|
+
From: `discord:${userId}`,
|
|
620
|
+
To: `slash:${userId}`,
|
|
621
|
+
ChatType: "direct",
|
|
622
|
+
SenderName: interaction.user.username,
|
|
623
|
+
Surface: "discord",
|
|
624
|
+
WasMentioned: true,
|
|
625
|
+
MessageSid: interaction.id,
|
|
626
|
+
Timestamp: interaction.createdTimestamp,
|
|
627
|
+
SessionKey: `${slashCommand.sessionPrefix}:${userId}`,
|
|
628
|
+
};
|
|
629
|
+
const replyResult = await getReplyFromConfig(ctxPayload, undefined, cfg);
|
|
630
|
+
const replies = replyResult
|
|
631
|
+
? Array.isArray(replyResult)
|
|
632
|
+
? replyResult
|
|
633
|
+
: [replyResult]
|
|
634
|
+
: [];
|
|
635
|
+
await deliverSlashReplies({
|
|
636
|
+
replies,
|
|
637
|
+
interaction,
|
|
638
|
+
ephemeral: slashCommand.ephemeral,
|
|
639
|
+
textLimit,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
runtime.error?.(danger(`slash handler failed: ${String(err)}`));
|
|
644
|
+
if (interaction.isRepliable()) {
|
|
645
|
+
const content = "Sorry, something went wrong handling that command.";
|
|
646
|
+
if (interaction.deferred || interaction.replied) {
|
|
647
|
+
await interaction.followUp({ content, ephemeral: true });
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
await interaction.reply({ content, ephemeral: true });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
await client.login(token);
|
|
656
|
+
await new Promise((resolve, reject) => {
|
|
657
|
+
const onAbort = () => {
|
|
658
|
+
cleanup();
|
|
659
|
+
void client.destroy();
|
|
660
|
+
resolve();
|
|
661
|
+
};
|
|
662
|
+
const onError = (err) => {
|
|
663
|
+
cleanup();
|
|
664
|
+
reject(err);
|
|
665
|
+
};
|
|
666
|
+
const cleanup = () => {
|
|
667
|
+
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
668
|
+
client.off(Events.Error, onError);
|
|
669
|
+
};
|
|
670
|
+
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
671
|
+
client.on(Events.Error, onError);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
async function resolveMedia(message, maxBytes) {
|
|
675
|
+
const attachment = message.attachments.first();
|
|
676
|
+
if (!attachment)
|
|
677
|
+
return null;
|
|
678
|
+
const res = await fetch(attachment.url);
|
|
679
|
+
if (!res.ok) {
|
|
680
|
+
throw new Error(`Failed to download discord attachment: HTTP ${res.status}`);
|
|
681
|
+
}
|
|
682
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
683
|
+
const mime = await detectMime({
|
|
684
|
+
buffer,
|
|
685
|
+
headerMime: attachment.contentType ?? res.headers.get("content-type"),
|
|
686
|
+
filePath: attachment.name ?? attachment.url,
|
|
687
|
+
});
|
|
688
|
+
const saved = await saveMediaBuffer(buffer, mime, "inbound", maxBytes);
|
|
689
|
+
return {
|
|
690
|
+
path: saved.path,
|
|
691
|
+
contentType: saved.contentType,
|
|
692
|
+
placeholder: inferPlaceholder(attachment),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function inferPlaceholder(attachment) {
|
|
696
|
+
const mime = attachment.contentType ?? "";
|
|
697
|
+
if (mime.startsWith("image/"))
|
|
698
|
+
return "<media:image>";
|
|
699
|
+
if (mime.startsWith("video/"))
|
|
700
|
+
return "<media:video>";
|
|
701
|
+
if (mime.startsWith("audio/"))
|
|
702
|
+
return "<media:audio>";
|
|
703
|
+
return "<media:document>";
|
|
704
|
+
}
|
|
705
|
+
function resolveDiscordMessageText(message, fallbackText) {
|
|
706
|
+
const attachment = message.attachments.first();
|
|
707
|
+
return (message.content?.trim() ||
|
|
708
|
+
(attachment ? inferPlaceholder(attachment) : "") ||
|
|
709
|
+
message.embeds[0]?.description ||
|
|
710
|
+
fallbackText?.trim() ||
|
|
711
|
+
"");
|
|
712
|
+
}
|
|
713
|
+
function resolveDiscordSnapshotText(snapshot) {
|
|
714
|
+
return snapshot.content?.trim() || snapshot.embeds[0]?.description || "";
|
|
715
|
+
}
|
|
716
|
+
async function resolveReplyContext(message) {
|
|
717
|
+
if (!message.reference?.messageId)
|
|
718
|
+
return null;
|
|
719
|
+
try {
|
|
720
|
+
const referenced = await message.fetchReference();
|
|
721
|
+
if (!referenced?.author)
|
|
722
|
+
return null;
|
|
723
|
+
const referencedText = resolveDiscordMessageText(referenced);
|
|
724
|
+
if (!referencedText)
|
|
725
|
+
return null;
|
|
726
|
+
const channelType = referenced.channel.type;
|
|
727
|
+
const isDirectMessage = channelType === ChannelType.DM;
|
|
728
|
+
const fromLabel = isDirectMessage
|
|
729
|
+
? buildDirectLabel(referenced)
|
|
730
|
+
: (referenced.member?.displayName ?? referenced.author.tag);
|
|
731
|
+
const body = `${referencedText}\n[discord message id: ${referenced.id} channel: ${referenced.channelId} from: ${referenced.author.tag} user id:${referenced.author.id}]`;
|
|
732
|
+
return formatAgentEnvelope({
|
|
733
|
+
surface: "Discord",
|
|
734
|
+
from: fromLabel,
|
|
735
|
+
timestamp: referenced.createdTimestamp,
|
|
736
|
+
body,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
logVerbose(`discord: failed to fetch reply context for ${message.id}: ${String(err)}`);
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function buildDirectLabel(message) {
|
|
745
|
+
const username = message.author.tag;
|
|
746
|
+
return `${username} user id:${message.author.id}`;
|
|
747
|
+
}
|
|
748
|
+
function buildGuildLabel(message) {
|
|
749
|
+
const channelName = "name" in message.channel ? message.channel.name : message.channelId;
|
|
750
|
+
return `${message.guild?.name ?? "Guild"} #${channelName} channel id:${message.channelId}`;
|
|
751
|
+
}
|
|
752
|
+
function resolveDiscordSystemEvent(message) {
|
|
753
|
+
switch (message.type) {
|
|
754
|
+
case MessageType.ChannelPinnedMessage:
|
|
755
|
+
return buildDiscordSystemEvent(message, "pinned a message");
|
|
756
|
+
case MessageType.RecipientAdd:
|
|
757
|
+
return buildDiscordSystemEvent(message, "added a recipient");
|
|
758
|
+
case MessageType.RecipientRemove:
|
|
759
|
+
return buildDiscordSystemEvent(message, "removed a recipient");
|
|
760
|
+
case MessageType.UserJoin:
|
|
761
|
+
return buildDiscordSystemEvent(message, "user joined");
|
|
762
|
+
case MessageType.GuildBoost:
|
|
763
|
+
return buildDiscordSystemEvent(message, "boosted the server");
|
|
764
|
+
case MessageType.GuildBoostTier1:
|
|
765
|
+
return buildDiscordSystemEvent(message, "boosted the server (Tier 1 reached)");
|
|
766
|
+
case MessageType.GuildBoostTier2:
|
|
767
|
+
return buildDiscordSystemEvent(message, "boosted the server (Tier 2 reached)");
|
|
768
|
+
case MessageType.GuildBoostTier3:
|
|
769
|
+
return buildDiscordSystemEvent(message, "boosted the server (Tier 3 reached)");
|
|
770
|
+
case MessageType.ThreadCreated:
|
|
771
|
+
return buildDiscordSystemEvent(message, "created a thread");
|
|
772
|
+
case MessageType.AutoModerationAction:
|
|
773
|
+
return buildDiscordSystemEvent(message, "auto moderation action");
|
|
774
|
+
case MessageType.GuildIncidentAlertModeEnabled:
|
|
775
|
+
return buildDiscordSystemEvent(message, "raid protection enabled");
|
|
776
|
+
case MessageType.GuildIncidentAlertModeDisabled:
|
|
777
|
+
return buildDiscordSystemEvent(message, "raid protection disabled");
|
|
778
|
+
case MessageType.GuildIncidentReportRaid:
|
|
779
|
+
return buildDiscordSystemEvent(message, "raid reported");
|
|
780
|
+
case MessageType.GuildIncidentReportFalseAlarm:
|
|
781
|
+
return buildDiscordSystemEvent(message, "raid report marked false alarm");
|
|
782
|
+
case MessageType.StageStart:
|
|
783
|
+
return buildDiscordSystemEvent(message, "stage started");
|
|
784
|
+
case MessageType.StageEnd:
|
|
785
|
+
return buildDiscordSystemEvent(message, "stage ended");
|
|
786
|
+
case MessageType.StageSpeaker:
|
|
787
|
+
return buildDiscordSystemEvent(message, "stage speaker updated");
|
|
788
|
+
case MessageType.StageTopic:
|
|
789
|
+
return buildDiscordSystemEvent(message, "stage topic updated");
|
|
790
|
+
case MessageType.PollResult:
|
|
791
|
+
return buildDiscordSystemEvent(message, "poll results posted");
|
|
792
|
+
case MessageType.PurchaseNotification:
|
|
793
|
+
return buildDiscordSystemEvent(message, "purchase notification");
|
|
794
|
+
default:
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function resolveForwardedSnapshot(message) {
|
|
799
|
+
const snapshots = message.messageSnapshots;
|
|
800
|
+
if (!snapshots || snapshots.size === 0)
|
|
801
|
+
return null;
|
|
802
|
+
const snapshot = snapshots.first();
|
|
803
|
+
if (!snapshot)
|
|
804
|
+
return null;
|
|
805
|
+
const reference = message.reference;
|
|
806
|
+
return {
|
|
807
|
+
snapshot,
|
|
808
|
+
messageId: reference?.messageId ?? undefined,
|
|
809
|
+
channelId: reference?.channelId ?? undefined,
|
|
810
|
+
guildId: reference?.guildId ?? undefined,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function buildDiscordSystemEvent(message, action) {
|
|
814
|
+
const channelName = "name" in message.channel ? message.channel.name : message.channelId;
|
|
815
|
+
const channelType = message.channel.type;
|
|
816
|
+
const location = message.guild?.name
|
|
817
|
+
? `${message.guild.name} #${channelName}`
|
|
818
|
+
: channelType === ChannelType.GroupDM
|
|
819
|
+
? `Group DM #${channelName}`
|
|
820
|
+
: "DM";
|
|
821
|
+
const authorLabel = message.author?.tag ?? message.author?.username;
|
|
822
|
+
const actor = authorLabel ? `${authorLabel} ` : "";
|
|
823
|
+
return `Discord system: ${actor}${action} in ${location}`;
|
|
824
|
+
}
|
|
825
|
+
function formatDiscordReactionEmoji(reaction) {
|
|
826
|
+
if (typeof reaction.emoji.toString === "function") {
|
|
827
|
+
const rendered = reaction.emoji.toString();
|
|
828
|
+
if (rendered && rendered !== "[object Object]")
|
|
829
|
+
return rendered;
|
|
830
|
+
}
|
|
831
|
+
if (reaction.emoji.id && reaction.emoji.name) {
|
|
832
|
+
return `${reaction.emoji.name}:${reaction.emoji.id}`;
|
|
833
|
+
}
|
|
834
|
+
return reaction.emoji.name ?? "emoji";
|
|
835
|
+
}
|
|
836
|
+
export function normalizeDiscordAllowList(raw, prefixes) {
|
|
837
|
+
if (!raw || raw.length === 0)
|
|
838
|
+
return null;
|
|
839
|
+
const ids = new Set();
|
|
840
|
+
const names = new Set();
|
|
841
|
+
let allowAll = false;
|
|
842
|
+
for (const rawEntry of raw) {
|
|
843
|
+
let entry = String(rawEntry).trim();
|
|
844
|
+
if (!entry)
|
|
845
|
+
continue;
|
|
846
|
+
if (entry === "*") {
|
|
847
|
+
allowAll = true;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
for (const prefix of prefixes) {
|
|
851
|
+
if (entry.toLowerCase().startsWith(prefix)) {
|
|
852
|
+
entry = entry.slice(prefix.length);
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const mentionMatch = entry.match(/^<[@#][!]?(\d+)>$/);
|
|
857
|
+
if (mentionMatch?.[1]) {
|
|
858
|
+
ids.add(mentionMatch[1]);
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
entry = entry.trim();
|
|
862
|
+
if (entry.startsWith("@") || entry.startsWith("#")) {
|
|
863
|
+
entry = entry.slice(1);
|
|
864
|
+
}
|
|
865
|
+
if (/^\d+$/.test(entry)) {
|
|
866
|
+
ids.add(entry);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const normalized = normalizeDiscordName(entry);
|
|
870
|
+
if (normalized)
|
|
871
|
+
names.add(normalized);
|
|
872
|
+
const slugged = normalizeDiscordSlug(entry);
|
|
873
|
+
if (slugged)
|
|
874
|
+
names.add(slugged);
|
|
875
|
+
}
|
|
876
|
+
if (!allowAll && ids.size === 0 && names.size === 0)
|
|
877
|
+
return null;
|
|
878
|
+
return { allowAll, ids, names };
|
|
879
|
+
}
|
|
880
|
+
function normalizeDiscordName(value) {
|
|
881
|
+
if (!value)
|
|
882
|
+
return "";
|
|
883
|
+
return value.trim().toLowerCase();
|
|
884
|
+
}
|
|
885
|
+
export function normalizeDiscordSlug(value) {
|
|
886
|
+
if (!value)
|
|
887
|
+
return "";
|
|
888
|
+
let text = value.trim().toLowerCase();
|
|
889
|
+
if (!text)
|
|
890
|
+
return "";
|
|
891
|
+
text = text.replace(/^[@#]+/, "");
|
|
892
|
+
text = text.replace(/[\s_]+/g, "-");
|
|
893
|
+
text = text.replace(/[^a-z0-9-]+/g, "-");
|
|
894
|
+
text = text.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
895
|
+
return text;
|
|
896
|
+
}
|
|
897
|
+
export function allowListMatches(allowList, candidates) {
|
|
898
|
+
if (allowList.allowAll)
|
|
899
|
+
return true;
|
|
900
|
+
const { id, name, tag } = candidates;
|
|
901
|
+
if (id && allowList.ids.has(id))
|
|
902
|
+
return true;
|
|
903
|
+
const normalizedName = normalizeDiscordName(name);
|
|
904
|
+
if (normalizedName && allowList.names.has(normalizedName))
|
|
905
|
+
return true;
|
|
906
|
+
const normalizedTag = normalizeDiscordName(tag);
|
|
907
|
+
if (normalizedTag && allowList.names.has(normalizedTag))
|
|
908
|
+
return true;
|
|
909
|
+
const slugName = normalizeDiscordSlug(name);
|
|
910
|
+
if (slugName && allowList.names.has(slugName))
|
|
911
|
+
return true;
|
|
912
|
+
const slugTag = normalizeDiscordSlug(tag);
|
|
913
|
+
if (slugTag && allowList.names.has(slugTag))
|
|
914
|
+
return true;
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
export function shouldEmitDiscordReactionNotification(params) {
|
|
918
|
+
const { mode, botId, messageAuthorId, userId, userName, userTag, allowlist } = params;
|
|
919
|
+
const effectiveMode = mode ?? "own";
|
|
920
|
+
if (effectiveMode === "off")
|
|
921
|
+
return false;
|
|
922
|
+
if (effectiveMode === "own") {
|
|
923
|
+
if (!botId || !messageAuthorId)
|
|
924
|
+
return false;
|
|
925
|
+
return messageAuthorId === botId;
|
|
926
|
+
}
|
|
927
|
+
if (effectiveMode === "allowlist") {
|
|
928
|
+
if (!Array.isArray(allowlist) || allowlist.length === 0)
|
|
929
|
+
return false;
|
|
930
|
+
const users = normalizeDiscordAllowList(allowlist, ["discord:", "user:"]);
|
|
931
|
+
if (!users)
|
|
932
|
+
return false;
|
|
933
|
+
return allowListMatches(users, {
|
|
934
|
+
id: userId,
|
|
935
|
+
name: userName ?? undefined,
|
|
936
|
+
tag: userTag ?? undefined,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
export function resolveDiscordGuildEntry(params) {
|
|
942
|
+
const { guild, guildEntries } = params;
|
|
943
|
+
if (!guild || !guildEntries || Object.keys(guildEntries).length === 0) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
const guildId = guild.id;
|
|
947
|
+
const guildSlug = normalizeDiscordSlug(guild.name);
|
|
948
|
+
const direct = guildEntries[guildId];
|
|
949
|
+
if (direct) {
|
|
950
|
+
return {
|
|
951
|
+
id: guildId,
|
|
952
|
+
slug: direct.slug ?? guildSlug,
|
|
953
|
+
requireMention: direct.requireMention,
|
|
954
|
+
reactionNotifications: direct.reactionNotifications,
|
|
955
|
+
users: direct.users,
|
|
956
|
+
channels: direct.channels,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (guildSlug && guildEntries[guildSlug]) {
|
|
960
|
+
const entry = guildEntries[guildSlug];
|
|
961
|
+
return {
|
|
962
|
+
id: guildId,
|
|
963
|
+
slug: entry.slug ?? guildSlug,
|
|
964
|
+
requireMention: entry.requireMention,
|
|
965
|
+
reactionNotifications: entry.reactionNotifications,
|
|
966
|
+
users: entry.users,
|
|
967
|
+
channels: entry.channels,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const matchBySlug = Object.entries(guildEntries).find(([, entry]) => {
|
|
971
|
+
const entrySlug = normalizeDiscordSlug(entry.slug);
|
|
972
|
+
return entrySlug && entrySlug === guildSlug;
|
|
973
|
+
});
|
|
974
|
+
if (matchBySlug) {
|
|
975
|
+
const entry = matchBySlug[1];
|
|
976
|
+
return {
|
|
977
|
+
id: guildId,
|
|
978
|
+
slug: entry.slug ?? guildSlug,
|
|
979
|
+
requireMention: entry.requireMention,
|
|
980
|
+
reactionNotifications: entry.reactionNotifications,
|
|
981
|
+
users: entry.users,
|
|
982
|
+
channels: entry.channels,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const wildcard = guildEntries["*"];
|
|
986
|
+
if (wildcard) {
|
|
987
|
+
return {
|
|
988
|
+
id: guildId,
|
|
989
|
+
slug: wildcard.slug ?? guildSlug,
|
|
990
|
+
requireMention: wildcard.requireMention,
|
|
991
|
+
reactionNotifications: wildcard.reactionNotifications,
|
|
992
|
+
users: wildcard.users,
|
|
993
|
+
channels: wildcard.channels,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
export function resolveDiscordChannelConfig(params) {
|
|
999
|
+
const { guildInfo, channelId, channelName, channelSlug } = params;
|
|
1000
|
+
const channelEntries = guildInfo?.channels;
|
|
1001
|
+
if (channelEntries && Object.keys(channelEntries).length > 0) {
|
|
1002
|
+
const entry = channelEntries[channelId] ??
|
|
1003
|
+
(channelSlug
|
|
1004
|
+
? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`])
|
|
1005
|
+
: undefined) ??
|
|
1006
|
+
(channelName
|
|
1007
|
+
? channelEntries[normalizeDiscordSlug(channelName)]
|
|
1008
|
+
: undefined);
|
|
1009
|
+
if (!entry)
|
|
1010
|
+
return { allowed: false };
|
|
1011
|
+
return {
|
|
1012
|
+
allowed: entry.allow !== false,
|
|
1013
|
+
requireMention: entry.requireMention,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
return { allowed: true };
|
|
1017
|
+
}
|
|
1018
|
+
export function resolveGroupDmAllow(params) {
|
|
1019
|
+
const { channels, channelId, channelName, channelSlug } = params;
|
|
1020
|
+
if (!channels || channels.length === 0)
|
|
1021
|
+
return true;
|
|
1022
|
+
const allowList = normalizeDiscordAllowList(channels, ["channel:"]);
|
|
1023
|
+
if (!allowList)
|
|
1024
|
+
return true;
|
|
1025
|
+
return allowListMatches(allowList, {
|
|
1026
|
+
id: channelId,
|
|
1027
|
+
name: channelSlug || channelName,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
async function ensureSlashCommand(client, slashCommand, runtime) {
|
|
1031
|
+
try {
|
|
1032
|
+
const appCommands = client.application?.commands;
|
|
1033
|
+
if (!appCommands) {
|
|
1034
|
+
runtime.error?.(danger("discord slash commands unavailable"));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const existing = await appCommands.fetch();
|
|
1038
|
+
const hasCommand = Array.from(existing.values()).some((entry) => entry.name === slashCommand.name);
|
|
1039
|
+
if (hasCommand)
|
|
1040
|
+
return;
|
|
1041
|
+
await appCommands.create({
|
|
1042
|
+
name: slashCommand.name,
|
|
1043
|
+
description: "Ask Clawdbot a question",
|
|
1044
|
+
options: [
|
|
1045
|
+
{
|
|
1046
|
+
name: "prompt",
|
|
1047
|
+
description: "What should Clawdbot help with?",
|
|
1048
|
+
type: ApplicationCommandOptionType.String,
|
|
1049
|
+
required: true,
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
});
|
|
1053
|
+
runtime.log?.(`registered discord slash command /${slashCommand.name}`);
|
|
1054
|
+
}
|
|
1055
|
+
catch (err) {
|
|
1056
|
+
const status = err?.status;
|
|
1057
|
+
const code = err?.code;
|
|
1058
|
+
const message = String(err);
|
|
1059
|
+
const isRateLimit = status === 429 || code === 429 || /rate ?limit/i.test(message);
|
|
1060
|
+
const text = `discord slash command setup failed: ${message}`;
|
|
1061
|
+
if (isRateLimit) {
|
|
1062
|
+
logVerbose(text);
|
|
1063
|
+
runtime.error?.(warn(text));
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
runtime.error?.(danger(text));
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function resolveSlashCommandConfig(raw) {
|
|
1071
|
+
return {
|
|
1072
|
+
enabled: raw ? raw.enabled !== false : false,
|
|
1073
|
+
name: raw?.name?.trim() || "clawd",
|
|
1074
|
+
sessionPrefix: raw?.sessionPrefix?.trim() || "discord:slash",
|
|
1075
|
+
ephemeral: raw?.ephemeral !== false,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function resolveSlashPrompt(options) {
|
|
1079
|
+
const direct = findFirstStringOption(options);
|
|
1080
|
+
if (direct)
|
|
1081
|
+
return direct;
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1084
|
+
function findFirstStringOption(options) {
|
|
1085
|
+
for (const option of options) {
|
|
1086
|
+
if (typeof option.value === "string") {
|
|
1087
|
+
const trimmed = option.value.trim();
|
|
1088
|
+
if (trimmed)
|
|
1089
|
+
return trimmed;
|
|
1090
|
+
}
|
|
1091
|
+
if (option.options && option.options.length > 0) {
|
|
1092
|
+
const nested = findFirstStringOption(option.options);
|
|
1093
|
+
if (nested)
|
|
1094
|
+
return nested;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return undefined;
|
|
1098
|
+
}
|
|
1099
|
+
async function sendTyping(message) {
|
|
1100
|
+
try {
|
|
1101
|
+
const channel = message.channel;
|
|
1102
|
+
if (channel.isSendable()) {
|
|
1103
|
+
await channel.sendTyping();
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
catch {
|
|
1107
|
+
/* ignore */
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async function deliverReplies({ replies, target, token, runtime, replyToMode, textLimit, }) {
|
|
1111
|
+
let hasReplied = false;
|
|
1112
|
+
const chunkLimit = Math.min(textLimit, 2000);
|
|
1113
|
+
for (const payload of replies) {
|
|
1114
|
+
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
1115
|
+
const text = payload.text ?? "";
|
|
1116
|
+
const replyToId = payload.replyToId;
|
|
1117
|
+
if (!text && mediaList.length === 0)
|
|
1118
|
+
continue;
|
|
1119
|
+
if (mediaList.length === 0) {
|
|
1120
|
+
for (const chunk of chunkText(text, chunkLimit)) {
|
|
1121
|
+
const replyTo = resolveDiscordReplyTarget({
|
|
1122
|
+
replyToMode,
|
|
1123
|
+
replyToId,
|
|
1124
|
+
hasReplied,
|
|
1125
|
+
});
|
|
1126
|
+
await sendMessageDiscord(target, chunk, {
|
|
1127
|
+
token,
|
|
1128
|
+
replyTo,
|
|
1129
|
+
});
|
|
1130
|
+
if (replyTo && !hasReplied) {
|
|
1131
|
+
hasReplied = true;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
let first = true;
|
|
1137
|
+
for (const mediaUrl of mediaList) {
|
|
1138
|
+
const caption = first ? text : "";
|
|
1139
|
+
first = false;
|
|
1140
|
+
const replyTo = resolveDiscordReplyTarget({
|
|
1141
|
+
replyToMode,
|
|
1142
|
+
replyToId,
|
|
1143
|
+
hasReplied,
|
|
1144
|
+
});
|
|
1145
|
+
await sendMessageDiscord(target, caption, {
|
|
1146
|
+
token,
|
|
1147
|
+
mediaUrl,
|
|
1148
|
+
replyTo,
|
|
1149
|
+
});
|
|
1150
|
+
if (replyTo && !hasReplied) {
|
|
1151
|
+
hasReplied = true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
runtime.log?.(`delivered reply to ${target}`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
async function deliverSlashReplies({ replies, interaction, ephemeral, textLimit, }) {
|
|
1159
|
+
const messages = [];
|
|
1160
|
+
const chunkLimit = Math.min(textLimit, 2000);
|
|
1161
|
+
for (const payload of replies) {
|
|
1162
|
+
const textRaw = payload.text?.trim() ?? "";
|
|
1163
|
+
const text = textRaw && textRaw !== SILENT_REPLY_TOKEN ? textRaw : undefined;
|
|
1164
|
+
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
1165
|
+
const combined = [
|
|
1166
|
+
text ?? "",
|
|
1167
|
+
...mediaList.map((url) => url.trim()).filter(Boolean),
|
|
1168
|
+
]
|
|
1169
|
+
.filter(Boolean)
|
|
1170
|
+
.join("\n");
|
|
1171
|
+
if (!combined)
|
|
1172
|
+
continue;
|
|
1173
|
+
for (const chunk of chunkText(combined, chunkLimit)) {
|
|
1174
|
+
messages.push(chunk);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (messages.length === 0) {
|
|
1178
|
+
await interaction.editReply({
|
|
1179
|
+
content: "No response was generated for that command.",
|
|
1180
|
+
});
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const [first, ...rest] = messages;
|
|
1184
|
+
await interaction.editReply({ content: first });
|
|
1185
|
+
for (const message of rest) {
|
|
1186
|
+
await interaction.followUp({ content: message, ephemeral });
|
|
1187
|
+
}
|
|
1188
|
+
}
|