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,1203 @@
|
|
|
1
|
+
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
|
2
|
+
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
|
3
|
+
import { normalizeGroupActivation, parseActivationCommand, } from "../auto-reply/group-activation.js";
|
|
4
|
+
import { HEARTBEAT_PROMPT, stripHeartbeatToken, } from "../auto-reply/heartbeat.js";
|
|
5
|
+
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|
6
|
+
import { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
|
7
|
+
import { waitForever } from "../cli/wait.js";
|
|
8
|
+
import { loadConfig } from "../config/config.js";
|
|
9
|
+
import { DEFAULT_IDLE_MINUTES, loadSessionStore, resolveGroupSessionKey, resolveSessionKey, resolveStorePath, saveSessionStore, updateLastRoute, } from "../config/sessions.js";
|
|
10
|
+
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
|
11
|
+
import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
|
|
12
|
+
import { enqueueSystemEvent } from "../infra/system-events.js";
|
|
13
|
+
import { createSubsystemLogger, getChildLogger } from "../logging.js";
|
|
14
|
+
import { defaultRuntime } from "../runtime.js";
|
|
15
|
+
import { isSelfChatMode, jidToE164, normalizeE164 } from "../utils.js";
|
|
16
|
+
import { setActiveWebListener } from "./active-listener.js";
|
|
17
|
+
import { monitorWebInbox } from "./inbound.js";
|
|
18
|
+
import { loadWebMedia } from "./media.js";
|
|
19
|
+
import { sendMessageWhatsApp } from "./outbound.js";
|
|
20
|
+
import { computeBackoff, newConnectionId, resolveHeartbeatSeconds, resolveReconnectPolicy, sleepWithAbort, } from "./reconnect.js";
|
|
21
|
+
import { formatError, getWebAuthAgeMs, readWebSelfId } from "./session.js";
|
|
22
|
+
const DEFAULT_GROUP_HISTORY_LIMIT = 50;
|
|
23
|
+
const whatsappLog = createSubsystemLogger("gateway/providers/whatsapp");
|
|
24
|
+
const whatsappInboundLog = whatsappLog.child("inbound");
|
|
25
|
+
const whatsappOutboundLog = whatsappLog.child("outbound");
|
|
26
|
+
const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
|
|
27
|
+
// Send via the active gateway-backed listener. The monitor already owns the single
|
|
28
|
+
// Baileys session, so use its send API directly.
|
|
29
|
+
async function sendWithIpcFallback(to, message, opts) {
|
|
30
|
+
return sendMessageWhatsApp(to, message, opts);
|
|
31
|
+
}
|
|
32
|
+
const DEFAULT_WEB_MEDIA_BYTES = 5 * 1024 * 1024;
|
|
33
|
+
const formatDuration = (ms) => ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`;
|
|
34
|
+
export { HEARTBEAT_PROMPT, HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN };
|
|
35
|
+
function elide(text, limit = 400) {
|
|
36
|
+
if (!text)
|
|
37
|
+
return text;
|
|
38
|
+
if (text.length <= limit)
|
|
39
|
+
return text;
|
|
40
|
+
return `${text.slice(0, limit)}… (truncated ${text.length - limit} chars)`;
|
|
41
|
+
}
|
|
42
|
+
function buildMentionConfig(cfg) {
|
|
43
|
+
const gc = cfg.routing?.groupChat;
|
|
44
|
+
const mentionRegexes = gc?.mentionPatterns
|
|
45
|
+
?.map((p) => {
|
|
46
|
+
try {
|
|
47
|
+
return new RegExp(p, "i");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.filter((r) => Boolean(r)) ?? [];
|
|
54
|
+
return { mentionRegexes, allowFrom: cfg.whatsapp?.allowFrom };
|
|
55
|
+
}
|
|
56
|
+
function isBotMentioned(msg, mentionCfg) {
|
|
57
|
+
const clean = (text) => text
|
|
58
|
+
// Remove zero-width and directionality markers WhatsApp injects around display names
|
|
59
|
+
.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
const isSelfChat = isSelfChatMode(msg.selfE164, mentionCfg.allowFrom);
|
|
62
|
+
if (msg.mentionedJids?.length && !isSelfChat) {
|
|
63
|
+
const normalizedMentions = msg.mentionedJids
|
|
64
|
+
.map((jid) => jidToE164(jid) ?? jid)
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
if (msg.selfE164 && normalizedMentions.includes(msg.selfE164))
|
|
67
|
+
return true;
|
|
68
|
+
if (msg.selfJid && msg.selfE164) {
|
|
69
|
+
// Some mentions use the bare JID; match on E.164 to be safe.
|
|
70
|
+
const bareSelf = msg.selfJid.replace(/:\\d+/, "");
|
|
71
|
+
if (normalizedMentions.includes(bareSelf))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (msg.mentionedJids?.length && isSelfChat) {
|
|
76
|
+
// Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in group chats triggers the bot.
|
|
77
|
+
}
|
|
78
|
+
const bodyClean = clean(msg.body);
|
|
79
|
+
if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean)))
|
|
80
|
+
return true;
|
|
81
|
+
// Fallback: detect body containing our own number (with or without +, spacing)
|
|
82
|
+
if (msg.selfE164) {
|
|
83
|
+
const selfDigits = msg.selfE164.replace(/\D/g, "");
|
|
84
|
+
if (selfDigits) {
|
|
85
|
+
const bodyDigits = bodyClean.replace(/[^\d]/g, "");
|
|
86
|
+
if (bodyDigits.includes(selfDigits))
|
|
87
|
+
return true;
|
|
88
|
+
const bodyNoSpace = msg.body.replace(/[\s-]/g, "");
|
|
89
|
+
const pattern = new RegExp(`\\+?${selfDigits}`, "i");
|
|
90
|
+
if (pattern.test(bodyNoSpace))
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function debugMention(msg, mentionCfg) {
|
|
97
|
+
const result = isBotMentioned(msg, mentionCfg);
|
|
98
|
+
const details = {
|
|
99
|
+
from: msg.from,
|
|
100
|
+
body: msg.body,
|
|
101
|
+
bodyClean: msg.body
|
|
102
|
+
.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
|
|
103
|
+
.toLowerCase(),
|
|
104
|
+
mentionedJids: msg.mentionedJids ?? null,
|
|
105
|
+
selfJid: msg.selfJid ?? null,
|
|
106
|
+
selfE164: msg.selfE164 ?? null,
|
|
107
|
+
};
|
|
108
|
+
return { wasMentioned: result, details };
|
|
109
|
+
}
|
|
110
|
+
export { stripHeartbeatToken };
|
|
111
|
+
function isSilentReply(payload) {
|
|
112
|
+
if (!payload)
|
|
113
|
+
return false;
|
|
114
|
+
const text = payload.text?.trim();
|
|
115
|
+
if (!text || text !== SILENT_REPLY_TOKEN)
|
|
116
|
+
return false;
|
|
117
|
+
if (payload.mediaUrl || payload.mediaUrls?.length)
|
|
118
|
+
return false;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
function resolveHeartbeatReplyPayload(replyResult) {
|
|
122
|
+
if (!replyResult)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (!Array.isArray(replyResult))
|
|
125
|
+
return replyResult;
|
|
126
|
+
for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
|
|
127
|
+
const payload = replyResult[idx];
|
|
128
|
+
if (!payload)
|
|
129
|
+
continue;
|
|
130
|
+
if (payload.text ||
|
|
131
|
+
payload.mediaUrl ||
|
|
132
|
+
(payload.mediaUrls && payload.mediaUrls.length > 0)) {
|
|
133
|
+
return payload;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
export async function runWebHeartbeatOnce(opts) {
|
|
139
|
+
const { cfg: cfgOverride, to, verbose = false, sessionId, overrideBody, dryRun = false, } = opts;
|
|
140
|
+
const replyResolver = opts.replyResolver ?? getReplyFromConfig;
|
|
141
|
+
const sender = opts.sender ?? sendWithIpcFallback;
|
|
142
|
+
const runId = newConnectionId();
|
|
143
|
+
const heartbeatLogger = getChildLogger({
|
|
144
|
+
module: "web-heartbeat",
|
|
145
|
+
runId,
|
|
146
|
+
to,
|
|
147
|
+
});
|
|
148
|
+
const cfg = cfgOverride ?? loadConfig();
|
|
149
|
+
const sessionCfg = cfg.session;
|
|
150
|
+
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
|
151
|
+
const mainKey = sessionCfg?.mainKey;
|
|
152
|
+
const sessionKey = resolveSessionKey(sessionScope, { From: to }, mainKey);
|
|
153
|
+
if (sessionId) {
|
|
154
|
+
const storePath = resolveStorePath(cfg.session?.store);
|
|
155
|
+
const store = loadSessionStore(storePath);
|
|
156
|
+
const current = store[sessionKey] ?? {};
|
|
157
|
+
store[sessionKey] = {
|
|
158
|
+
...current,
|
|
159
|
+
sessionId,
|
|
160
|
+
updatedAt: Date.now(),
|
|
161
|
+
};
|
|
162
|
+
await saveSessionStore(storePath, store);
|
|
163
|
+
}
|
|
164
|
+
const sessionSnapshot = getSessionSnapshot(cfg, to, true);
|
|
165
|
+
if (verbose) {
|
|
166
|
+
heartbeatLogger.info({
|
|
167
|
+
to,
|
|
168
|
+
sessionKey: sessionSnapshot.key,
|
|
169
|
+
sessionId: sessionId ?? sessionSnapshot.entry?.sessionId ?? null,
|
|
170
|
+
sessionFresh: sessionSnapshot.fresh,
|
|
171
|
+
idleMinutes: sessionSnapshot.idleMinutes,
|
|
172
|
+
}, "heartbeat session snapshot");
|
|
173
|
+
}
|
|
174
|
+
if (overrideBody && overrideBody.trim().length === 0) {
|
|
175
|
+
throw new Error("Override body must be non-empty when provided.");
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
if (overrideBody) {
|
|
179
|
+
if (dryRun) {
|
|
180
|
+
whatsappHeartbeatLog.info(`[dry-run] web send -> ${to}: ${elide(overrideBody.trim(), 200)} (manual message)`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const sendResult = await sender(to, overrideBody, { verbose });
|
|
184
|
+
emitHeartbeatEvent({
|
|
185
|
+
status: "sent",
|
|
186
|
+
to,
|
|
187
|
+
preview: overrideBody.slice(0, 160),
|
|
188
|
+
hasMedia: false,
|
|
189
|
+
});
|
|
190
|
+
heartbeatLogger.info({
|
|
191
|
+
to,
|
|
192
|
+
messageId: sendResult.messageId,
|
|
193
|
+
chars: overrideBody.length,
|
|
194
|
+
reason: "manual-message",
|
|
195
|
+
}, "manual heartbeat message sent");
|
|
196
|
+
whatsappHeartbeatLog.info(`manual heartbeat sent to ${to} (id ${sendResult.messageId})`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const replyResult = await replyResolver({
|
|
200
|
+
Body: HEARTBEAT_PROMPT,
|
|
201
|
+
From: to,
|
|
202
|
+
To: to,
|
|
203
|
+
MessageSid: sessionId ?? sessionSnapshot.entry?.sessionId,
|
|
204
|
+
}, { isHeartbeat: true }, cfg);
|
|
205
|
+
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
|
|
206
|
+
if (!replyPayload ||
|
|
207
|
+
(!replyPayload.text &&
|
|
208
|
+
!replyPayload.mediaUrl &&
|
|
209
|
+
!replyPayload.mediaUrls?.length)) {
|
|
210
|
+
heartbeatLogger.info({
|
|
211
|
+
to,
|
|
212
|
+
reason: "empty-reply",
|
|
213
|
+
sessionId: sessionSnapshot.entry?.sessionId ?? null,
|
|
214
|
+
}, "heartbeat skipped");
|
|
215
|
+
if (shouldLogVerbose()) {
|
|
216
|
+
whatsappHeartbeatLog.debug("heartbeat ok (empty reply)");
|
|
217
|
+
}
|
|
218
|
+
emitHeartbeatEvent({ status: "ok-empty", to });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const hasMedia = Boolean(replyPayload.mediaUrl || (replyPayload.mediaUrls?.length ?? 0) > 0);
|
|
222
|
+
const stripped = stripHeartbeatToken(replyPayload.text, {
|
|
223
|
+
mode: "heartbeat",
|
|
224
|
+
maxAckChars: 30,
|
|
225
|
+
});
|
|
226
|
+
if (stripped.shouldSkip && !hasMedia) {
|
|
227
|
+
// Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works.
|
|
228
|
+
const storePath = resolveStorePath(cfg.session?.store);
|
|
229
|
+
const store = loadSessionStore(storePath);
|
|
230
|
+
if (sessionSnapshot.entry && store[sessionSnapshot.key]) {
|
|
231
|
+
store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
|
|
232
|
+
await saveSessionStore(storePath, store);
|
|
233
|
+
}
|
|
234
|
+
heartbeatLogger.info({ to, reason: "heartbeat-token", rawLength: replyPayload.text?.length }, "heartbeat skipped");
|
|
235
|
+
if (shouldLogVerbose()) {
|
|
236
|
+
whatsappHeartbeatLog.debug("heartbeat ok (HEARTBEAT_OK)");
|
|
237
|
+
}
|
|
238
|
+
emitHeartbeatEvent({ status: "ok-token", to });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (hasMedia) {
|
|
242
|
+
heartbeatLogger.warn({ to }, "heartbeat reply contained media; sending text only");
|
|
243
|
+
}
|
|
244
|
+
const finalText = stripped.text || replyPayload.text || "";
|
|
245
|
+
if (dryRun) {
|
|
246
|
+
heartbeatLogger.info({ to, reason: "dry-run", chars: finalText.length }, "heartbeat dry-run");
|
|
247
|
+
whatsappHeartbeatLog.info(`[dry-run] heartbeat -> ${to}: ${elide(finalText, 200)}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const sendResult = await sender(to, finalText, { verbose });
|
|
251
|
+
emitHeartbeatEvent({
|
|
252
|
+
status: "sent",
|
|
253
|
+
to,
|
|
254
|
+
preview: finalText.slice(0, 160),
|
|
255
|
+
hasMedia,
|
|
256
|
+
});
|
|
257
|
+
heartbeatLogger.info({
|
|
258
|
+
to,
|
|
259
|
+
messageId: sendResult.messageId,
|
|
260
|
+
chars: finalText.length,
|
|
261
|
+
preview: elide(finalText, 140),
|
|
262
|
+
}, "heartbeat sent");
|
|
263
|
+
whatsappHeartbeatLog.info(`heartbeat alert sent to ${to}`);
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
const reason = formatError(err);
|
|
267
|
+
heartbeatLogger.warn({ to, error: reason }, "heartbeat failed");
|
|
268
|
+
whatsappHeartbeatLog.warn(`heartbeat failed (${reason})`);
|
|
269
|
+
emitHeartbeatEvent({ status: "failed", to, reason });
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function getSessionRecipients(cfg) {
|
|
274
|
+
const sessionCfg = cfg.session;
|
|
275
|
+
const scope = sessionCfg?.scope ?? "per-sender";
|
|
276
|
+
if (scope === "global")
|
|
277
|
+
return [];
|
|
278
|
+
const storePath = resolveStorePath(cfg.session?.store);
|
|
279
|
+
const store = loadSessionStore(storePath);
|
|
280
|
+
const isGroupKey = (key) => key.startsWith("group:") ||
|
|
281
|
+
key.includes(":group:") ||
|
|
282
|
+
key.includes(":channel:") ||
|
|
283
|
+
key.includes("@g.us");
|
|
284
|
+
const isCronKey = (key) => key.startsWith("cron:");
|
|
285
|
+
const recipients = Object.entries(store)
|
|
286
|
+
.filter(([key]) => key !== "global" && key !== "unknown")
|
|
287
|
+
.filter(([key]) => !isGroupKey(key) && !isCronKey(key))
|
|
288
|
+
.map(([_, entry]) => ({
|
|
289
|
+
to: entry?.lastChannel === "whatsapp" && entry?.lastTo
|
|
290
|
+
? normalizeE164(entry.lastTo)
|
|
291
|
+
: "",
|
|
292
|
+
updatedAt: entry?.updatedAt ?? 0,
|
|
293
|
+
}))
|
|
294
|
+
.filter(({ to }) => to.length > 1)
|
|
295
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
296
|
+
// Dedupe while preserving recency ordering.
|
|
297
|
+
const seen = new Set();
|
|
298
|
+
return recipients.filter((r) => {
|
|
299
|
+
if (seen.has(r.to))
|
|
300
|
+
return false;
|
|
301
|
+
seen.add(r.to);
|
|
302
|
+
return true;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
export function resolveHeartbeatRecipients(cfg, opts = {}) {
|
|
306
|
+
if (opts.to)
|
|
307
|
+
return { recipients: [normalizeE164(opts.to)], source: "flag" };
|
|
308
|
+
const sessionRecipients = getSessionRecipients(cfg);
|
|
309
|
+
const allowFrom = Array.isArray(cfg.whatsapp?.allowFrom) && cfg.whatsapp.allowFrom.length > 0
|
|
310
|
+
? cfg.whatsapp.allowFrom.filter((v) => v !== "*").map(normalizeE164)
|
|
311
|
+
: [];
|
|
312
|
+
const unique = (list) => [...new Set(list.filter(Boolean))];
|
|
313
|
+
if (opts.all) {
|
|
314
|
+
const all = unique([...sessionRecipients.map((s) => s.to), ...allowFrom]);
|
|
315
|
+
return { recipients: all, source: "all" };
|
|
316
|
+
}
|
|
317
|
+
if (sessionRecipients.length === 1) {
|
|
318
|
+
return { recipients: [sessionRecipients[0].to], source: "session-single" };
|
|
319
|
+
}
|
|
320
|
+
if (sessionRecipients.length > 1) {
|
|
321
|
+
return {
|
|
322
|
+
recipients: sessionRecipients.map((s) => s.to),
|
|
323
|
+
source: "session-ambiguous",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return { recipients: allowFrom, source: "allowFrom" };
|
|
327
|
+
}
|
|
328
|
+
function getSessionSnapshot(cfg, from, isHeartbeat = false) {
|
|
329
|
+
const sessionCfg = cfg.session;
|
|
330
|
+
const scope = sessionCfg?.scope ?? "per-sender";
|
|
331
|
+
const key = resolveSessionKey(scope, { From: from, To: "", Body: "" }, sessionCfg?.mainKey);
|
|
332
|
+
const store = loadSessionStore(resolveStorePath(sessionCfg?.store));
|
|
333
|
+
const entry = store[key];
|
|
334
|
+
const idleMinutes = Math.max((isHeartbeat
|
|
335
|
+
? (sessionCfg?.heartbeatIdleMinutes ?? sessionCfg?.idleMinutes)
|
|
336
|
+
: sessionCfg?.idleMinutes) ?? DEFAULT_IDLE_MINUTES, 1);
|
|
337
|
+
const fresh = !!(entry && Date.now() - entry.updatedAt <= idleMinutes * 60_000);
|
|
338
|
+
return { key, entry, fresh, idleMinutes };
|
|
339
|
+
}
|
|
340
|
+
async function deliverWebReply(params) {
|
|
341
|
+
const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog, } = params;
|
|
342
|
+
const replyStarted = Date.now();
|
|
343
|
+
const textChunks = chunkText(replyResult.text || "", textLimit);
|
|
344
|
+
const mediaList = replyResult.mediaUrls?.length
|
|
345
|
+
? replyResult.mediaUrls
|
|
346
|
+
: replyResult.mediaUrl
|
|
347
|
+
? [replyResult.mediaUrl]
|
|
348
|
+
: [];
|
|
349
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
350
|
+
const sendWithRetry = async (fn, label, maxAttempts = 3) => {
|
|
351
|
+
let lastErr;
|
|
352
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
353
|
+
try {
|
|
354
|
+
return await fn();
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
lastErr = err;
|
|
358
|
+
const errText = formatError(err);
|
|
359
|
+
const isLast = attempt === maxAttempts;
|
|
360
|
+
const shouldRetry = /closed|reset|timed\s*out|disconnect/i.test(errText);
|
|
361
|
+
if (!shouldRetry || isLast) {
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
const backoffMs = 500 * attempt;
|
|
365
|
+
logVerbose(`Retrying ${label} to ${msg.from} after failure (${attempt}/${maxAttempts - 1}) in ${backoffMs}ms: ${errText}`);
|
|
366
|
+
await sleep(backoffMs);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
throw lastErr;
|
|
370
|
+
};
|
|
371
|
+
// Text-only replies
|
|
372
|
+
if (mediaList.length === 0 && textChunks.length) {
|
|
373
|
+
const totalChunks = textChunks.length;
|
|
374
|
+
for (const [index, chunk] of textChunks.entries()) {
|
|
375
|
+
const chunkStarted = Date.now();
|
|
376
|
+
await sendWithRetry(() => msg.reply(chunk), "text");
|
|
377
|
+
if (!skipLog) {
|
|
378
|
+
const durationMs = Date.now() - chunkStarted;
|
|
379
|
+
whatsappOutboundLog.debug(`Sent chunk ${index + 1}/${totalChunks} to ${msg.from} (${durationMs.toFixed(0)}ms)`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
replyLogger.info({
|
|
383
|
+
correlationId: msg.id ?? newConnectionId(),
|
|
384
|
+
connectionId: connectionId ?? null,
|
|
385
|
+
to: msg.from,
|
|
386
|
+
from: msg.to,
|
|
387
|
+
text: elide(replyResult.text, 240),
|
|
388
|
+
mediaUrl: null,
|
|
389
|
+
mediaSizeBytes: null,
|
|
390
|
+
mediaKind: null,
|
|
391
|
+
durationMs: Date.now() - replyStarted,
|
|
392
|
+
}, "auto-reply sent (text)");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const remainingText = [...textChunks];
|
|
396
|
+
// Media (with optional caption on first item)
|
|
397
|
+
for (const [index, mediaUrl] of mediaList.entries()) {
|
|
398
|
+
const caption = index === 0 ? remainingText.shift() || undefined : undefined;
|
|
399
|
+
try {
|
|
400
|
+
const media = await loadWebMedia(mediaUrl, maxMediaBytes);
|
|
401
|
+
if (shouldLogVerbose()) {
|
|
402
|
+
logVerbose(`Web auto-reply media size: ${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB`);
|
|
403
|
+
logVerbose(`Web auto-reply media source: ${mediaUrl} (kind ${media.kind})`);
|
|
404
|
+
}
|
|
405
|
+
if (media.kind === "image") {
|
|
406
|
+
await sendWithRetry(() => msg.sendMedia({
|
|
407
|
+
image: media.buffer,
|
|
408
|
+
caption,
|
|
409
|
+
mimetype: media.contentType,
|
|
410
|
+
}), "media:image");
|
|
411
|
+
}
|
|
412
|
+
else if (media.kind === "audio") {
|
|
413
|
+
await sendWithRetry(() => msg.sendMedia({
|
|
414
|
+
audio: media.buffer,
|
|
415
|
+
ptt: true,
|
|
416
|
+
mimetype: media.contentType,
|
|
417
|
+
caption,
|
|
418
|
+
}), "media:audio");
|
|
419
|
+
}
|
|
420
|
+
else if (media.kind === "video") {
|
|
421
|
+
await sendWithRetry(() => msg.sendMedia({
|
|
422
|
+
video: media.buffer,
|
|
423
|
+
caption,
|
|
424
|
+
mimetype: media.contentType,
|
|
425
|
+
}), "media:video");
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const fileName = media.fileName ?? mediaUrl.split("/").pop() ?? "file";
|
|
429
|
+
const mimetype = media.contentType ?? "application/octet-stream";
|
|
430
|
+
await sendWithRetry(() => msg.sendMedia({
|
|
431
|
+
document: media.buffer,
|
|
432
|
+
fileName,
|
|
433
|
+
caption,
|
|
434
|
+
mimetype,
|
|
435
|
+
}), "media:document");
|
|
436
|
+
}
|
|
437
|
+
whatsappOutboundLog.info(`Sent media reply to ${msg.from} (${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB)`);
|
|
438
|
+
replyLogger.info({
|
|
439
|
+
correlationId: msg.id ?? newConnectionId(),
|
|
440
|
+
connectionId: connectionId ?? null,
|
|
441
|
+
to: msg.from,
|
|
442
|
+
from: msg.to,
|
|
443
|
+
text: caption ?? null,
|
|
444
|
+
mediaUrl,
|
|
445
|
+
mediaSizeBytes: media.buffer.length,
|
|
446
|
+
mediaKind: media.kind,
|
|
447
|
+
durationMs: Date.now() - replyStarted,
|
|
448
|
+
}, "auto-reply sent (media)");
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
whatsappOutboundLog.error(`Failed sending web media to ${msg.from}: ${formatError(err)}`);
|
|
452
|
+
replyLogger.warn({ err, mediaUrl }, "failed to send web media reply");
|
|
453
|
+
if (index === 0) {
|
|
454
|
+
const warning = err instanceof Error
|
|
455
|
+
? `⚠️ Media failed: ${err.message}`
|
|
456
|
+
: "⚠️ Media failed.";
|
|
457
|
+
const fallbackTextParts = [
|
|
458
|
+
remainingText.shift() ?? caption ?? "",
|
|
459
|
+
warning,
|
|
460
|
+
].filter(Boolean);
|
|
461
|
+
const fallbackText = fallbackTextParts.join("\n");
|
|
462
|
+
if (fallbackText) {
|
|
463
|
+
whatsappOutboundLog.warn(`Media skipped; sent text-only to ${msg.from}`);
|
|
464
|
+
await msg.reply(fallbackText);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Remaining text chunks after media
|
|
470
|
+
for (const chunk of remainingText) {
|
|
471
|
+
await msg.reply(chunk);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
export async function monitorWebProvider(verbose, listenerFactory = monitorWebInbox, keepAlive = true, replyResolver = getReplyFromConfig, runtime = defaultRuntime, abortSignal, tuning = {}) {
|
|
475
|
+
const runId = newConnectionId();
|
|
476
|
+
const replyLogger = getChildLogger({ module: "web-auto-reply", runId });
|
|
477
|
+
const heartbeatLogger = getChildLogger({ module: "web-heartbeat", runId });
|
|
478
|
+
const reconnectLogger = getChildLogger({ module: "web-reconnect", runId });
|
|
479
|
+
const status = {
|
|
480
|
+
running: true,
|
|
481
|
+
connected: false,
|
|
482
|
+
reconnectAttempts: 0,
|
|
483
|
+
lastConnectedAt: null,
|
|
484
|
+
lastDisconnect: null,
|
|
485
|
+
lastMessageAt: null,
|
|
486
|
+
lastEventAt: null,
|
|
487
|
+
lastError: null,
|
|
488
|
+
};
|
|
489
|
+
const emitStatus = () => {
|
|
490
|
+
tuning.statusSink?.({
|
|
491
|
+
...status,
|
|
492
|
+
lastDisconnect: status.lastDisconnect
|
|
493
|
+
? { ...status.lastDisconnect }
|
|
494
|
+
: null,
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
emitStatus();
|
|
498
|
+
const cfg = loadConfig();
|
|
499
|
+
const configuredMaxMb = cfg.agent?.mediaMaxMb;
|
|
500
|
+
const maxMediaBytes = typeof configuredMaxMb === "number" && configuredMaxMb > 0
|
|
501
|
+
? configuredMaxMb * 1024 * 1024
|
|
502
|
+
: DEFAULT_WEB_MEDIA_BYTES;
|
|
503
|
+
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
|
|
504
|
+
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
|
|
505
|
+
const mentionConfig = buildMentionConfig(cfg);
|
|
506
|
+
const sessionStorePath = resolveStorePath(cfg.session?.store);
|
|
507
|
+
const groupHistoryLimit = cfg.routing?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
|
|
508
|
+
const groupHistories = new Map();
|
|
509
|
+
const groupMemberNames = new Map();
|
|
510
|
+
const sleep = tuning.sleep ??
|
|
511
|
+
((ms, signal) => sleepWithAbort(ms, signal ?? abortSignal));
|
|
512
|
+
const stopRequested = () => abortSignal?.aborted === true;
|
|
513
|
+
const abortPromise = abortSignal &&
|
|
514
|
+
new Promise((resolve) => abortSignal.addEventListener("abort", () => resolve("aborted"), {
|
|
515
|
+
once: true,
|
|
516
|
+
}));
|
|
517
|
+
const noteGroupMember = (conversationId, e164, name) => {
|
|
518
|
+
if (!e164 || !name)
|
|
519
|
+
return;
|
|
520
|
+
const normalized = normalizeE164(e164);
|
|
521
|
+
const key = normalized ?? e164;
|
|
522
|
+
if (!key)
|
|
523
|
+
return;
|
|
524
|
+
let roster = groupMemberNames.get(conversationId);
|
|
525
|
+
if (!roster) {
|
|
526
|
+
roster = new Map();
|
|
527
|
+
groupMemberNames.set(conversationId, roster);
|
|
528
|
+
}
|
|
529
|
+
roster.set(key, name);
|
|
530
|
+
};
|
|
531
|
+
const formatGroupMembers = (participants, roster, fallbackE164) => {
|
|
532
|
+
const seen = new Set();
|
|
533
|
+
const ordered = [];
|
|
534
|
+
if (participants?.length) {
|
|
535
|
+
for (const entry of participants) {
|
|
536
|
+
if (!entry)
|
|
537
|
+
continue;
|
|
538
|
+
const normalized = normalizeE164(entry) ?? entry;
|
|
539
|
+
if (!normalized || seen.has(normalized))
|
|
540
|
+
continue;
|
|
541
|
+
seen.add(normalized);
|
|
542
|
+
ordered.push(normalized);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (roster) {
|
|
546
|
+
for (const entry of roster.keys()) {
|
|
547
|
+
const normalized = normalizeE164(entry) ?? entry;
|
|
548
|
+
if (!normalized || seen.has(normalized))
|
|
549
|
+
continue;
|
|
550
|
+
seen.add(normalized);
|
|
551
|
+
ordered.push(normalized);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (ordered.length === 0 && fallbackE164) {
|
|
555
|
+
const normalized = normalizeE164(fallbackE164) ?? fallbackE164;
|
|
556
|
+
if (normalized)
|
|
557
|
+
ordered.push(normalized);
|
|
558
|
+
}
|
|
559
|
+
if (ordered.length === 0)
|
|
560
|
+
return undefined;
|
|
561
|
+
return ordered
|
|
562
|
+
.map((entry) => {
|
|
563
|
+
const name = roster?.get(entry);
|
|
564
|
+
return name ? `${name} (${entry})` : entry;
|
|
565
|
+
})
|
|
566
|
+
.join(", ");
|
|
567
|
+
};
|
|
568
|
+
const resolveGroupResolution = (conversationId) => resolveGroupSessionKey({
|
|
569
|
+
From: conversationId,
|
|
570
|
+
ChatType: "group",
|
|
571
|
+
Surface: "whatsapp",
|
|
572
|
+
});
|
|
573
|
+
const resolveGroupRequireMentionFor = (conversationId) => {
|
|
574
|
+
const groupId = resolveGroupResolution(conversationId)?.id ?? conversationId;
|
|
575
|
+
const groupConfig = cfg.whatsapp?.groups?.[groupId];
|
|
576
|
+
if (typeof groupConfig?.requireMention === "boolean") {
|
|
577
|
+
return groupConfig.requireMention;
|
|
578
|
+
}
|
|
579
|
+
const groupDefault = cfg.whatsapp?.groups?.["*"]?.requireMention;
|
|
580
|
+
if (typeof groupDefault === "boolean")
|
|
581
|
+
return groupDefault;
|
|
582
|
+
return true;
|
|
583
|
+
};
|
|
584
|
+
const resolveGroupActivationFor = (conversationId) => {
|
|
585
|
+
const key = resolveGroupResolution(conversationId)?.key ??
|
|
586
|
+
(conversationId.startsWith("group:")
|
|
587
|
+
? conversationId
|
|
588
|
+
: `whatsapp:group:${conversationId}`);
|
|
589
|
+
const store = loadSessionStore(sessionStorePath);
|
|
590
|
+
const entry = store[key];
|
|
591
|
+
const requireMention = resolveGroupRequireMentionFor(conversationId);
|
|
592
|
+
const defaultActivation = requireMention === false ? "always" : "mention";
|
|
593
|
+
return (normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation);
|
|
594
|
+
};
|
|
595
|
+
const resolveOwnerList = (selfE164) => {
|
|
596
|
+
const allowFrom = mentionConfig.allowFrom;
|
|
597
|
+
const raw = Array.isArray(allowFrom) && allowFrom.length > 0
|
|
598
|
+
? allowFrom
|
|
599
|
+
: selfE164
|
|
600
|
+
? [selfE164]
|
|
601
|
+
: [];
|
|
602
|
+
return raw
|
|
603
|
+
.filter((entry) => Boolean(entry && entry !== "*"))
|
|
604
|
+
.map((entry) => normalizeE164(entry))
|
|
605
|
+
.filter((entry) => Boolean(entry));
|
|
606
|
+
};
|
|
607
|
+
const isOwnerSender = (msg) => {
|
|
608
|
+
const sender = normalizeE164(msg.senderE164 ?? "");
|
|
609
|
+
if (!sender)
|
|
610
|
+
return false;
|
|
611
|
+
const owners = resolveOwnerList(msg.selfE164 ?? undefined);
|
|
612
|
+
return owners.includes(sender);
|
|
613
|
+
};
|
|
614
|
+
const isStatusCommand = (body) => {
|
|
615
|
+
const trimmed = body.trim().toLowerCase();
|
|
616
|
+
if (!trimmed)
|
|
617
|
+
return false;
|
|
618
|
+
return (trimmed === "/status" ||
|
|
619
|
+
trimmed === "status" ||
|
|
620
|
+
trimmed.startsWith("/status "));
|
|
621
|
+
};
|
|
622
|
+
const stripMentionsForCommand = (text, selfE164) => {
|
|
623
|
+
let result = text;
|
|
624
|
+
for (const re of mentionConfig.mentionRegexes) {
|
|
625
|
+
result = result.replace(re, " ");
|
|
626
|
+
}
|
|
627
|
+
if (selfE164) {
|
|
628
|
+
const digits = selfE164.replace(/\D/g, "");
|
|
629
|
+
if (digits) {
|
|
630
|
+
const pattern = new RegExp(`\\+?${digits}`, "g");
|
|
631
|
+
result = result.replace(pattern, " ");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return result.replace(/\s+/g, " ").trim();
|
|
635
|
+
};
|
|
636
|
+
// Avoid noisy MaxListenersExceeded warnings in test environments where
|
|
637
|
+
// multiple gateway instances may be constructed.
|
|
638
|
+
const currentMaxListeners = process.getMaxListeners?.() ?? 10;
|
|
639
|
+
if (process.setMaxListeners && currentMaxListeners < 50) {
|
|
640
|
+
process.setMaxListeners(50);
|
|
641
|
+
}
|
|
642
|
+
let sigintStop = false;
|
|
643
|
+
const handleSigint = () => {
|
|
644
|
+
sigintStop = true;
|
|
645
|
+
};
|
|
646
|
+
process.once("SIGINT", handleSigint);
|
|
647
|
+
let reconnectAttempts = 0;
|
|
648
|
+
// Track recently sent messages to prevent echo loops
|
|
649
|
+
const recentlySent = new Set();
|
|
650
|
+
const MAX_RECENT_MESSAGES = 100;
|
|
651
|
+
while (true) {
|
|
652
|
+
if (stopRequested())
|
|
653
|
+
break;
|
|
654
|
+
const connectionId = newConnectionId();
|
|
655
|
+
const startedAt = Date.now();
|
|
656
|
+
let heartbeat = null;
|
|
657
|
+
let watchdogTimer = null;
|
|
658
|
+
let lastMessageAt = null;
|
|
659
|
+
let handledMessages = 0;
|
|
660
|
+
let _lastInboundMsg = null;
|
|
661
|
+
// Watchdog to detect stuck message processing (e.g., event emitter died)
|
|
662
|
+
// Should be significantly longer than the reply heartbeat interval to avoid false positives
|
|
663
|
+
const MESSAGE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes without any messages
|
|
664
|
+
const WATCHDOG_CHECK_MS = 60 * 1000; // Check every minute
|
|
665
|
+
const backgroundTasks = new Set();
|
|
666
|
+
const formatReplyContext = (msg) => {
|
|
667
|
+
if (!msg.replyToBody)
|
|
668
|
+
return null;
|
|
669
|
+
const sender = msg.replyToSender ?? "unknown sender";
|
|
670
|
+
const idPart = msg.replyToId ? ` id:${msg.replyToId}` : "";
|
|
671
|
+
return `[Replying to ${sender}${idPart}]\n${msg.replyToBody}\n[/Replying]`;
|
|
672
|
+
};
|
|
673
|
+
const buildLine = (msg) => {
|
|
674
|
+
// Build message prefix: explicit config > default based on allowFrom
|
|
675
|
+
let messagePrefix = cfg.messages?.messagePrefix;
|
|
676
|
+
if (messagePrefix === undefined) {
|
|
677
|
+
const hasAllowFrom = (cfg.whatsapp?.allowFrom?.length ?? 0) > 0;
|
|
678
|
+
messagePrefix = hasAllowFrom ? "" : "[clawdbot]";
|
|
679
|
+
}
|
|
680
|
+
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
|
|
681
|
+
const senderLabel = msg.chatType === "group"
|
|
682
|
+
? `${msg.senderName ?? msg.senderE164 ?? "Someone"}: `
|
|
683
|
+
: "";
|
|
684
|
+
const replyContext = formatReplyContext(msg);
|
|
685
|
+
const baseLine = `${prefixStr}${senderLabel}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`;
|
|
686
|
+
// Wrap with standardized envelope for the agent.
|
|
687
|
+
return formatAgentEnvelope({
|
|
688
|
+
surface: "WhatsApp",
|
|
689
|
+
from: msg.chatType === "group"
|
|
690
|
+
? msg.from
|
|
691
|
+
: msg.from?.replace(/^whatsapp:/, ""),
|
|
692
|
+
timestamp: msg.timestamp,
|
|
693
|
+
body: baseLine,
|
|
694
|
+
});
|
|
695
|
+
};
|
|
696
|
+
const processMessage = async (msg) => {
|
|
697
|
+
status.lastMessageAt = Date.now();
|
|
698
|
+
status.lastEventAt = status.lastMessageAt;
|
|
699
|
+
emitStatus();
|
|
700
|
+
const conversationId = msg.conversationId ?? msg.from;
|
|
701
|
+
let combinedBody = buildLine(msg);
|
|
702
|
+
let shouldClearGroupHistory = false;
|
|
703
|
+
if (msg.chatType === "group") {
|
|
704
|
+
const history = groupHistories.get(conversationId) ?? [];
|
|
705
|
+
const historyWithoutCurrent = history.length > 0 ? history.slice(0, -1) : [];
|
|
706
|
+
if (historyWithoutCurrent.length > 0) {
|
|
707
|
+
const historyText = historyWithoutCurrent
|
|
708
|
+
.map((m) => formatAgentEnvelope({
|
|
709
|
+
surface: "WhatsApp",
|
|
710
|
+
from: conversationId,
|
|
711
|
+
timestamp: m.timestamp,
|
|
712
|
+
body: `${m.sender}: ${m.body}`,
|
|
713
|
+
}))
|
|
714
|
+
.join("\\n");
|
|
715
|
+
combinedBody = `[Chat messages since your last reply - for context]\\n${historyText}\\n\\n[Current message - respond to this]\\n${buildLine(msg)}`;
|
|
716
|
+
}
|
|
717
|
+
// Always surface who sent the triggering message so the agent can address them.
|
|
718
|
+
const senderLabel = msg.senderName && msg.senderE164
|
|
719
|
+
? `${msg.senderName} (${msg.senderE164})`
|
|
720
|
+
: (msg.senderName ?? msg.senderE164 ?? "Unknown");
|
|
721
|
+
combinedBody = `${combinedBody}\\n[from: ${senderLabel}]`;
|
|
722
|
+
shouldClearGroupHistory = true;
|
|
723
|
+
}
|
|
724
|
+
// Echo detection uses combined body so we don't respond twice.
|
|
725
|
+
if (recentlySent.has(combinedBody)) {
|
|
726
|
+
logVerbose(`Skipping auto-reply: detected echo for combined message`);
|
|
727
|
+
recentlySent.delete(combinedBody);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const correlationId = msg.id ?? newConnectionId();
|
|
731
|
+
replyLogger.info({
|
|
732
|
+
connectionId,
|
|
733
|
+
correlationId,
|
|
734
|
+
from: msg.chatType === "group" ? conversationId : msg.from,
|
|
735
|
+
to: msg.to,
|
|
736
|
+
body: elide(combinedBody, 240),
|
|
737
|
+
mediaType: msg.mediaType ?? null,
|
|
738
|
+
mediaPath: msg.mediaPath ?? null,
|
|
739
|
+
}, "inbound web message");
|
|
740
|
+
const fromDisplay = msg.chatType === "group" ? conversationId : msg.from;
|
|
741
|
+
const kindLabel = msg.mediaType ? `, ${msg.mediaType}` : "";
|
|
742
|
+
whatsappInboundLog.info(`Inbound message ${fromDisplay} -> ${msg.to} (${msg.chatType}${kindLabel}, ${combinedBody.length} chars)`);
|
|
743
|
+
if (shouldLogVerbose()) {
|
|
744
|
+
whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
|
|
745
|
+
}
|
|
746
|
+
if (msg.chatType !== "group") {
|
|
747
|
+
const sessionCfg = cfg.session;
|
|
748
|
+
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
|
|
749
|
+
const storePath = resolveStorePath(sessionCfg?.store);
|
|
750
|
+
const to = (() => {
|
|
751
|
+
if (msg.senderE164)
|
|
752
|
+
return normalizeE164(msg.senderE164);
|
|
753
|
+
// In direct chats, `msg.from` is already the canonical conversation id,
|
|
754
|
+
// which is an E.164 string (e.g. "+1555"). Only fall back to JID parsing
|
|
755
|
+
// when we were handed a JID-like string.
|
|
756
|
+
if (msg.from.includes("@"))
|
|
757
|
+
return jidToE164(msg.from);
|
|
758
|
+
return normalizeE164(msg.from);
|
|
759
|
+
})();
|
|
760
|
+
if (to) {
|
|
761
|
+
const task = updateLastRoute({
|
|
762
|
+
storePath,
|
|
763
|
+
sessionKey: mainKey,
|
|
764
|
+
channel: "whatsapp",
|
|
765
|
+
to,
|
|
766
|
+
}).catch((err) => {
|
|
767
|
+
replyLogger.warn({ error: formatError(err), storePath, sessionKey: mainKey, to }, "failed updating last route");
|
|
768
|
+
});
|
|
769
|
+
backgroundTasks.add(task);
|
|
770
|
+
void task.finally(() => {
|
|
771
|
+
backgroundTasks.delete(task);
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const responsePrefix = cfg.messages?.responsePrefix;
|
|
776
|
+
const textLimit = resolveTextChunkLimit(cfg, "whatsapp");
|
|
777
|
+
let didLogHeartbeatStrip = false;
|
|
778
|
+
let didSendReply = false;
|
|
779
|
+
let toolSendChain = Promise.resolve();
|
|
780
|
+
const sendToolResult = (payload) => {
|
|
781
|
+
if (!payload?.text &&
|
|
782
|
+
!payload?.mediaUrl &&
|
|
783
|
+
!(payload?.mediaUrls?.length ?? 0)) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (isSilentReply(payload))
|
|
787
|
+
return;
|
|
788
|
+
const toolPayload = { ...payload };
|
|
789
|
+
if (toolPayload.text?.includes(HEARTBEAT_TOKEN)) {
|
|
790
|
+
const stripped = stripHeartbeatToken(toolPayload.text, {
|
|
791
|
+
mode: "message",
|
|
792
|
+
});
|
|
793
|
+
if (stripped.didStrip && !didLogHeartbeatStrip) {
|
|
794
|
+
didLogHeartbeatStrip = true;
|
|
795
|
+
logVerbose("Stripped stray HEARTBEAT_OK token from web reply");
|
|
796
|
+
}
|
|
797
|
+
const hasMedia = Boolean(toolPayload.mediaUrl || (toolPayload.mediaUrls?.length ?? 0) > 0);
|
|
798
|
+
if (stripped.shouldSkip && !hasMedia)
|
|
799
|
+
return;
|
|
800
|
+
toolPayload.text = stripped.text;
|
|
801
|
+
}
|
|
802
|
+
if (responsePrefix &&
|
|
803
|
+
toolPayload.text &&
|
|
804
|
+
toolPayload.text.trim() !== HEARTBEAT_TOKEN &&
|
|
805
|
+
!toolPayload.text.startsWith(responsePrefix)) {
|
|
806
|
+
toolPayload.text = `${responsePrefix} ${toolPayload.text}`;
|
|
807
|
+
}
|
|
808
|
+
toolSendChain = toolSendChain
|
|
809
|
+
.then(async () => {
|
|
810
|
+
await deliverWebReply({
|
|
811
|
+
replyResult: toolPayload,
|
|
812
|
+
msg,
|
|
813
|
+
maxMediaBytes,
|
|
814
|
+
textLimit,
|
|
815
|
+
replyLogger,
|
|
816
|
+
connectionId,
|
|
817
|
+
skipLog: true,
|
|
818
|
+
});
|
|
819
|
+
didSendReply = true;
|
|
820
|
+
if (toolPayload.text) {
|
|
821
|
+
recentlySent.add(toolPayload.text);
|
|
822
|
+
if (recentlySent.size > MAX_RECENT_MESSAGES) {
|
|
823
|
+
const firstKey = recentlySent.values().next().value;
|
|
824
|
+
if (firstKey)
|
|
825
|
+
recentlySent.delete(firstKey);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
})
|
|
829
|
+
.catch((err) => {
|
|
830
|
+
whatsappOutboundLog.error(`Failed sending web tool update to ${msg.from ?? conversationId}: ${formatError(err)}`);
|
|
831
|
+
});
|
|
832
|
+
};
|
|
833
|
+
const sendBlockReply = (payload) => {
|
|
834
|
+
if (!payload?.text &&
|
|
835
|
+
!payload?.mediaUrl &&
|
|
836
|
+
!(payload?.mediaUrls?.length ?? 0)) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (isSilentReply(payload))
|
|
840
|
+
return;
|
|
841
|
+
const blockPayload = { ...payload };
|
|
842
|
+
if (responsePrefix &&
|
|
843
|
+
blockPayload.text &&
|
|
844
|
+
blockPayload.text.trim() !== HEARTBEAT_TOKEN &&
|
|
845
|
+
!blockPayload.text.startsWith(responsePrefix)) {
|
|
846
|
+
blockPayload.text = `${responsePrefix} ${blockPayload.text}`;
|
|
847
|
+
}
|
|
848
|
+
toolSendChain = toolSendChain
|
|
849
|
+
.then(async () => {
|
|
850
|
+
await deliverWebReply({
|
|
851
|
+
replyResult: blockPayload,
|
|
852
|
+
msg,
|
|
853
|
+
maxMediaBytes,
|
|
854
|
+
textLimit,
|
|
855
|
+
replyLogger,
|
|
856
|
+
connectionId,
|
|
857
|
+
skipLog: true,
|
|
858
|
+
});
|
|
859
|
+
didSendReply = true;
|
|
860
|
+
if (blockPayload.text) {
|
|
861
|
+
recentlySent.add(blockPayload.text);
|
|
862
|
+
recentlySent.add(combinedBody);
|
|
863
|
+
if (recentlySent.size > MAX_RECENT_MESSAGES) {
|
|
864
|
+
const firstKey = recentlySent.values().next().value;
|
|
865
|
+
if (firstKey)
|
|
866
|
+
recentlySent.delete(firstKey);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
})
|
|
870
|
+
.catch((err) => {
|
|
871
|
+
whatsappOutboundLog.error(`Failed sending web block update to ${msg.from ?? conversationId}: ${formatError(err)}`);
|
|
872
|
+
});
|
|
873
|
+
};
|
|
874
|
+
const replyResult = await (replyResolver ?? getReplyFromConfig)({
|
|
875
|
+
Body: combinedBody,
|
|
876
|
+
From: msg.from,
|
|
877
|
+
To: msg.to,
|
|
878
|
+
MessageSid: msg.id,
|
|
879
|
+
ReplyToId: msg.replyToId,
|
|
880
|
+
ReplyToBody: msg.replyToBody,
|
|
881
|
+
ReplyToSender: msg.replyToSender,
|
|
882
|
+
MediaPath: msg.mediaPath,
|
|
883
|
+
MediaUrl: msg.mediaUrl,
|
|
884
|
+
MediaType: msg.mediaType,
|
|
885
|
+
ChatType: msg.chatType,
|
|
886
|
+
GroupSubject: msg.groupSubject,
|
|
887
|
+
GroupMembers: formatGroupMembers(msg.groupParticipants, groupMemberNames.get(conversationId), msg.senderE164),
|
|
888
|
+
SenderName: msg.senderName,
|
|
889
|
+
SenderE164: msg.senderE164,
|
|
890
|
+
WasMentioned: msg.wasMentioned,
|
|
891
|
+
Surface: "whatsapp",
|
|
892
|
+
}, {
|
|
893
|
+
onReplyStart: msg.sendComposing,
|
|
894
|
+
onToolResult: sendToolResult,
|
|
895
|
+
onBlockReply: sendBlockReply,
|
|
896
|
+
});
|
|
897
|
+
const replyList = replyResult
|
|
898
|
+
? Array.isArray(replyResult)
|
|
899
|
+
? replyResult
|
|
900
|
+
: [replyResult]
|
|
901
|
+
: [];
|
|
902
|
+
const sendableReplies = replyList.filter((payload) => !isSilentReply(payload));
|
|
903
|
+
if (sendableReplies.length === 0) {
|
|
904
|
+
await toolSendChain;
|
|
905
|
+
if (shouldClearGroupHistory && didSendReply) {
|
|
906
|
+
groupHistories.set(conversationId, []);
|
|
907
|
+
}
|
|
908
|
+
logVerbose("Skipping auto-reply: silent token or no text/media returned from resolver");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
await toolSendChain;
|
|
912
|
+
for (const replyPayload of sendableReplies) {
|
|
913
|
+
if (replyPayload.text?.includes(HEARTBEAT_TOKEN)) {
|
|
914
|
+
const stripped = stripHeartbeatToken(replyPayload.text, {
|
|
915
|
+
mode: "message",
|
|
916
|
+
});
|
|
917
|
+
if (stripped.didStrip && !didLogHeartbeatStrip) {
|
|
918
|
+
didLogHeartbeatStrip = true;
|
|
919
|
+
logVerbose("Stripped stray HEARTBEAT_OK token from web reply");
|
|
920
|
+
}
|
|
921
|
+
const hasMedia = Boolean(replyPayload.mediaUrl || (replyPayload.mediaUrls?.length ?? 0) > 0);
|
|
922
|
+
if (stripped.shouldSkip && !hasMedia)
|
|
923
|
+
continue;
|
|
924
|
+
replyPayload.text = stripped.text;
|
|
925
|
+
}
|
|
926
|
+
if (responsePrefix &&
|
|
927
|
+
replyPayload.text &&
|
|
928
|
+
replyPayload.text.trim() !== HEARTBEAT_TOKEN &&
|
|
929
|
+
!replyPayload.text.startsWith(responsePrefix)) {
|
|
930
|
+
replyPayload.text = `${responsePrefix} ${replyPayload.text}`;
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
await deliverWebReply({
|
|
934
|
+
replyResult: replyPayload,
|
|
935
|
+
msg,
|
|
936
|
+
maxMediaBytes,
|
|
937
|
+
textLimit,
|
|
938
|
+
replyLogger,
|
|
939
|
+
connectionId,
|
|
940
|
+
});
|
|
941
|
+
didSendReply = true;
|
|
942
|
+
if (replyPayload.text) {
|
|
943
|
+
recentlySent.add(replyPayload.text);
|
|
944
|
+
recentlySent.add(combinedBody); // Prevent echo on the combined text itself
|
|
945
|
+
logVerbose(`Added to echo detection set (size now: ${recentlySent.size}): ${replyPayload.text.substring(0, 50)}...`);
|
|
946
|
+
if (recentlySent.size > MAX_RECENT_MESSAGES) {
|
|
947
|
+
const firstKey = recentlySent.values().next().value;
|
|
948
|
+
if (firstKey)
|
|
949
|
+
recentlySent.delete(firstKey);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const fromDisplay = msg.chatType === "group" ? conversationId : (msg.from ?? "unknown");
|
|
953
|
+
const hasMedia = Boolean(replyPayload.mediaUrl || replyPayload.mediaUrls?.length);
|
|
954
|
+
whatsappOutboundLog.info(`Auto-replied to ${fromDisplay}${hasMedia ? " (media)" : ""}`);
|
|
955
|
+
if (shouldLogVerbose()) {
|
|
956
|
+
const preview = replyPayload.text != null
|
|
957
|
+
? elide(replyPayload.text, 400)
|
|
958
|
+
: "<media>";
|
|
959
|
+
whatsappOutboundLog.debug(`Reply body: ${preview}${hasMedia ? " (media)" : ""}`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
whatsappOutboundLog.error(`Failed sending web auto-reply to ${msg.from ?? conversationId}: ${formatError(err)}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (shouldClearGroupHistory && didSendReply) {
|
|
967
|
+
groupHistories.set(conversationId, []);
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
const listener = await (listenerFactory ?? monitorWebInbox)({
|
|
971
|
+
verbose,
|
|
972
|
+
onMessage: async (msg) => {
|
|
973
|
+
handledMessages += 1;
|
|
974
|
+
lastMessageAt = Date.now();
|
|
975
|
+
status.lastMessageAt = lastMessageAt;
|
|
976
|
+
status.lastEventAt = lastMessageAt;
|
|
977
|
+
emitStatus();
|
|
978
|
+
_lastInboundMsg = msg;
|
|
979
|
+
const conversationId = msg.conversationId ?? msg.from;
|
|
980
|
+
// Same-phone mode logging retained
|
|
981
|
+
if (msg.from === msg.to) {
|
|
982
|
+
logVerbose(`📱 Same-phone mode detected (from === to: ${msg.from})`);
|
|
983
|
+
}
|
|
984
|
+
// Skip if this is a message we just sent (echo detection)
|
|
985
|
+
if (recentlySent.has(msg.body)) {
|
|
986
|
+
whatsappInboundLog.debug("Skipping echo: detected recently sent message");
|
|
987
|
+
logVerbose(`Skipping auto-reply: detected echo (message matches recently sent text)`);
|
|
988
|
+
recentlySent.delete(msg.body);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (msg.chatType === "group") {
|
|
992
|
+
noteGroupMember(conversationId, msg.senderE164, msg.senderName);
|
|
993
|
+
const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
|
|
994
|
+
const activationCommand = parseActivationCommand(commandBody);
|
|
995
|
+
const isOwner = isOwnerSender(msg);
|
|
996
|
+
const statusCommand = isStatusCommand(commandBody);
|
|
997
|
+
const shouldBypassMention = isOwner && (activationCommand.hasCommand || statusCommand);
|
|
998
|
+
if (activationCommand.hasCommand && !isOwner) {
|
|
999
|
+
logVerbose(`Ignoring /activation from non-owner in group ${conversationId}`);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (!shouldBypassMention) {
|
|
1003
|
+
const history = groupHistories.get(conversationId) ??
|
|
1004
|
+
[];
|
|
1005
|
+
history.push({
|
|
1006
|
+
sender: msg.senderName ?? msg.senderE164 ?? "Unknown",
|
|
1007
|
+
body: msg.body,
|
|
1008
|
+
timestamp: msg.timestamp,
|
|
1009
|
+
});
|
|
1010
|
+
while (history.length > groupHistoryLimit)
|
|
1011
|
+
history.shift();
|
|
1012
|
+
groupHistories.set(conversationId, history);
|
|
1013
|
+
}
|
|
1014
|
+
const mentionDebug = debugMention(msg, mentionConfig);
|
|
1015
|
+
replyLogger.debug({
|
|
1016
|
+
conversationId,
|
|
1017
|
+
wasMentioned: mentionDebug.wasMentioned,
|
|
1018
|
+
...mentionDebug.details,
|
|
1019
|
+
}, "group mention debug");
|
|
1020
|
+
const wasMentioned = mentionDebug.wasMentioned;
|
|
1021
|
+
msg.wasMentioned = wasMentioned;
|
|
1022
|
+
const activation = resolveGroupActivationFor(conversationId);
|
|
1023
|
+
const requireMention = activation !== "always";
|
|
1024
|
+
if (!shouldBypassMention && requireMention && !wasMentioned) {
|
|
1025
|
+
logVerbose(`Group message stored for context (no mention detected) in ${conversationId}: ${msg.body}`);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return processMessage(msg);
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
status.connected = true;
|
|
1033
|
+
status.lastConnectedAt = Date.now();
|
|
1034
|
+
status.lastEventAt = status.lastConnectedAt;
|
|
1035
|
+
status.lastError = null;
|
|
1036
|
+
emitStatus();
|
|
1037
|
+
// Surface a concise connection event for the next main-session turn/heartbeat.
|
|
1038
|
+
const { e164: selfE164 } = readWebSelfId();
|
|
1039
|
+
enqueueSystemEvent(`WhatsApp gateway connected${selfE164 ? ` as ${selfE164}` : ""}.`);
|
|
1040
|
+
setActiveWebListener(listener);
|
|
1041
|
+
const closeListener = async () => {
|
|
1042
|
+
setActiveWebListener(null);
|
|
1043
|
+
if (heartbeat)
|
|
1044
|
+
clearInterval(heartbeat);
|
|
1045
|
+
if (watchdogTimer)
|
|
1046
|
+
clearInterval(watchdogTimer);
|
|
1047
|
+
if (backgroundTasks.size > 0) {
|
|
1048
|
+
await Promise.allSettled(backgroundTasks);
|
|
1049
|
+
backgroundTasks.clear();
|
|
1050
|
+
}
|
|
1051
|
+
try {
|
|
1052
|
+
await listener.close();
|
|
1053
|
+
}
|
|
1054
|
+
catch (err) {
|
|
1055
|
+
logVerbose(`Socket close failed: ${formatError(err)}`);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
if (keepAlive) {
|
|
1059
|
+
heartbeat = setInterval(() => {
|
|
1060
|
+
const authAgeMs = getWebAuthAgeMs();
|
|
1061
|
+
const minutesSinceLastMessage = lastMessageAt
|
|
1062
|
+
? Math.floor((Date.now() - lastMessageAt) / 60000)
|
|
1063
|
+
: null;
|
|
1064
|
+
const logData = {
|
|
1065
|
+
connectionId,
|
|
1066
|
+
reconnectAttempts,
|
|
1067
|
+
messagesHandled: handledMessages,
|
|
1068
|
+
lastMessageAt,
|
|
1069
|
+
authAgeMs,
|
|
1070
|
+
uptimeMs: Date.now() - startedAt,
|
|
1071
|
+
...(minutesSinceLastMessage !== null && minutesSinceLastMessage > 30
|
|
1072
|
+
? { minutesSinceLastMessage }
|
|
1073
|
+
: {}),
|
|
1074
|
+
};
|
|
1075
|
+
// Warn if no messages in 30+ minutes
|
|
1076
|
+
if (minutesSinceLastMessage && minutesSinceLastMessage > 30) {
|
|
1077
|
+
heartbeatLogger.warn(logData, "⚠️ web gateway heartbeat - no messages in 30+ minutes");
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
heartbeatLogger.info(logData, "web gateway heartbeat");
|
|
1081
|
+
}
|
|
1082
|
+
}, heartbeatSeconds * 1000);
|
|
1083
|
+
// Watchdog: Auto-restart if no messages received for MESSAGE_TIMEOUT_MS
|
|
1084
|
+
watchdogTimer = setInterval(() => {
|
|
1085
|
+
if (lastMessageAt) {
|
|
1086
|
+
const timeSinceLastMessage = Date.now() - lastMessageAt;
|
|
1087
|
+
if (timeSinceLastMessage > MESSAGE_TIMEOUT_MS) {
|
|
1088
|
+
const minutesSinceLastMessage = Math.floor(timeSinceLastMessage / 60000);
|
|
1089
|
+
heartbeatLogger.warn({
|
|
1090
|
+
connectionId,
|
|
1091
|
+
minutesSinceLastMessage,
|
|
1092
|
+
lastMessageAt: new Date(lastMessageAt),
|
|
1093
|
+
messagesHandled: handledMessages,
|
|
1094
|
+
}, "Message timeout detected - forcing reconnect");
|
|
1095
|
+
whatsappHeartbeatLog.warn(`No messages received in ${minutesSinceLastMessage}m - restarting connection`);
|
|
1096
|
+
void closeListener().catch((err) => {
|
|
1097
|
+
logVerbose(`Close listener failed: ${formatError(err)}`);
|
|
1098
|
+
}); // Trigger reconnect
|
|
1099
|
+
listener.signalClose?.({
|
|
1100
|
+
status: 499,
|
|
1101
|
+
isLoggedOut: false,
|
|
1102
|
+
error: "watchdog-timeout",
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}, WATCHDOG_CHECK_MS);
|
|
1107
|
+
}
|
|
1108
|
+
whatsappLog.info("Listening for personal WhatsApp inbound messages.");
|
|
1109
|
+
if (process.stdout.isTTY || process.stderr.isTTY) {
|
|
1110
|
+
whatsappLog.raw("Ctrl+C to stop.");
|
|
1111
|
+
}
|
|
1112
|
+
if (!keepAlive) {
|
|
1113
|
+
await closeListener();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const reason = await Promise.race([
|
|
1117
|
+
listener.onClose?.catch((err) => {
|
|
1118
|
+
reconnectLogger.error({ error: formatError(err) }, "listener.onClose rejected");
|
|
1119
|
+
return { status: 500, isLoggedOut: false, error: err };
|
|
1120
|
+
}) ?? waitForever(),
|
|
1121
|
+
abortPromise ?? waitForever(),
|
|
1122
|
+
]);
|
|
1123
|
+
const uptimeMs = Date.now() - startedAt;
|
|
1124
|
+
if (uptimeMs > heartbeatSeconds * 1000) {
|
|
1125
|
+
reconnectAttempts = 0; // Healthy stretch; reset the backoff.
|
|
1126
|
+
}
|
|
1127
|
+
status.reconnectAttempts = reconnectAttempts;
|
|
1128
|
+
emitStatus();
|
|
1129
|
+
if (stopRequested() || sigintStop || reason === "aborted") {
|
|
1130
|
+
await closeListener();
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
const statusCode = (typeof reason === "object" && reason && "status" in reason
|
|
1134
|
+
? reason.status
|
|
1135
|
+
: undefined) ?? "unknown";
|
|
1136
|
+
const loggedOut = typeof reason === "object" &&
|
|
1137
|
+
reason &&
|
|
1138
|
+
"isLoggedOut" in reason &&
|
|
1139
|
+
reason.isLoggedOut;
|
|
1140
|
+
const errorStr = formatError(reason);
|
|
1141
|
+
status.connected = false;
|
|
1142
|
+
status.lastEventAt = Date.now();
|
|
1143
|
+
status.lastDisconnect = {
|
|
1144
|
+
at: status.lastEventAt,
|
|
1145
|
+
status: typeof statusCode === "number" ? statusCode : undefined,
|
|
1146
|
+
error: errorStr,
|
|
1147
|
+
loggedOut: Boolean(loggedOut),
|
|
1148
|
+
};
|
|
1149
|
+
status.lastError = errorStr;
|
|
1150
|
+
status.reconnectAttempts = reconnectAttempts;
|
|
1151
|
+
emitStatus();
|
|
1152
|
+
reconnectLogger.info({
|
|
1153
|
+
connectionId,
|
|
1154
|
+
status: statusCode,
|
|
1155
|
+
loggedOut,
|
|
1156
|
+
reconnectAttempts,
|
|
1157
|
+
error: errorStr,
|
|
1158
|
+
}, "web reconnect: connection closed");
|
|
1159
|
+
enqueueSystemEvent(`WhatsApp gateway disconnected (status ${statusCode ?? "unknown"})`);
|
|
1160
|
+
if (loggedOut) {
|
|
1161
|
+
runtime.error("WhatsApp session logged out. Run `clawdbot login --provider web` to relink.");
|
|
1162
|
+
await closeListener();
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
reconnectAttempts += 1;
|
|
1166
|
+
status.reconnectAttempts = reconnectAttempts;
|
|
1167
|
+
emitStatus();
|
|
1168
|
+
if (reconnectPolicy.maxAttempts > 0 &&
|
|
1169
|
+
reconnectAttempts >= reconnectPolicy.maxAttempts) {
|
|
1170
|
+
reconnectLogger.warn({
|
|
1171
|
+
connectionId,
|
|
1172
|
+
status: statusCode,
|
|
1173
|
+
reconnectAttempts,
|
|
1174
|
+
maxAttempts: reconnectPolicy.maxAttempts,
|
|
1175
|
+
}, "web reconnect: max attempts reached; continuing in degraded mode");
|
|
1176
|
+
runtime.error(`WhatsApp Web reconnect: max attempts reached (${reconnectAttempts}/${reconnectPolicy.maxAttempts}). Stopping web monitoring.`);
|
|
1177
|
+
await closeListener();
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
const delay = computeBackoff(reconnectPolicy, reconnectAttempts);
|
|
1181
|
+
reconnectLogger.info({
|
|
1182
|
+
connectionId,
|
|
1183
|
+
status: statusCode,
|
|
1184
|
+
reconnectAttempts,
|
|
1185
|
+
maxAttempts: reconnectPolicy.maxAttempts || "unlimited",
|
|
1186
|
+
delayMs: delay,
|
|
1187
|
+
}, "web reconnect: scheduling retry");
|
|
1188
|
+
runtime.error(`WhatsApp Web connection closed (status ${statusCode}). Retry ${reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} in ${formatDuration(delay)}… (${errorStr})`);
|
|
1189
|
+
await closeListener();
|
|
1190
|
+
try {
|
|
1191
|
+
await sleep(delay, abortSignal);
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
status.running = false;
|
|
1198
|
+
status.connected = false;
|
|
1199
|
+
status.lastEventAt = Date.now();
|
|
1200
|
+
emitStatus();
|
|
1201
|
+
process.removeListener("SIGINT", handleSigint);
|
|
1202
|
+
}
|
|
1203
|
+
export { DEFAULT_WEB_MEDIA_BYTES };
|