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,33 @@
|
|
|
1
|
+
export const MAX_IMAGE_BYTES = 6 * 1024 * 1024; // 6MB
|
|
2
|
+
export const MAX_AUDIO_BYTES = 16 * 1024 * 1024; // 16MB
|
|
3
|
+
export const MAX_VIDEO_BYTES = 16 * 1024 * 1024; // 16MB
|
|
4
|
+
export const MAX_DOCUMENT_BYTES = 100 * 1024 * 1024; // 100MB
|
|
5
|
+
export function mediaKindFromMime(mime) {
|
|
6
|
+
if (!mime)
|
|
7
|
+
return "unknown";
|
|
8
|
+
if (mime.startsWith("image/"))
|
|
9
|
+
return "image";
|
|
10
|
+
if (mime.startsWith("audio/"))
|
|
11
|
+
return "audio";
|
|
12
|
+
if (mime.startsWith("video/"))
|
|
13
|
+
return "video";
|
|
14
|
+
if (mime === "application/pdf")
|
|
15
|
+
return "document";
|
|
16
|
+
if (mime.startsWith("application/"))
|
|
17
|
+
return "document";
|
|
18
|
+
return "unknown";
|
|
19
|
+
}
|
|
20
|
+
export function maxBytesForKind(kind) {
|
|
21
|
+
switch (kind) {
|
|
22
|
+
case "image":
|
|
23
|
+
return MAX_IMAGE_BYTES;
|
|
24
|
+
case "audio":
|
|
25
|
+
return MAX_AUDIO_BYTES;
|
|
26
|
+
case "video":
|
|
27
|
+
return MAX_VIDEO_BYTES;
|
|
28
|
+
case "document":
|
|
29
|
+
return MAX_DOCUMENT_BYTES;
|
|
30
|
+
default:
|
|
31
|
+
return MAX_DOCUMENT_BYTES;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { ensurePortAvailable, PortInUseError } from "../infra/ports.js";
|
|
3
|
+
import { getTailnetHostname } from "../infra/tailscale.js";
|
|
4
|
+
import { logInfo } from "../logger.js";
|
|
5
|
+
import { defaultRuntime } from "../runtime.js";
|
|
6
|
+
import { startMediaServer } from "./server.js";
|
|
7
|
+
import { saveMediaSource } from "./store.js";
|
|
8
|
+
const DEFAULT_PORT = 42873;
|
|
9
|
+
const TTL_MS = 2 * 60 * 1000;
|
|
10
|
+
let mediaServer = null;
|
|
11
|
+
export async function ensureMediaHosted(source, opts = {}) {
|
|
12
|
+
const port = opts.port ?? DEFAULT_PORT;
|
|
13
|
+
const runtime = opts.runtime ?? defaultRuntime;
|
|
14
|
+
const saved = await saveMediaSource(source);
|
|
15
|
+
const hostname = await getTailnetHostname();
|
|
16
|
+
// Decide whether we must start a media server.
|
|
17
|
+
const needsServerStart = await isPortFree(port);
|
|
18
|
+
if (needsServerStart && !opts.startServer) {
|
|
19
|
+
await fs.rm(saved.path).catch(() => { });
|
|
20
|
+
throw new Error("Media hosting requires the webhook/Funnel server. Start `clawdbot webhook`/`clawdbot up` or re-run with --serve-media.");
|
|
21
|
+
}
|
|
22
|
+
if (needsServerStart && opts.startServer) {
|
|
23
|
+
if (!mediaServer) {
|
|
24
|
+
mediaServer = await startMediaServer(port, TTL_MS, runtime);
|
|
25
|
+
logInfo(`📡 Started temporary media host on http://localhost:${port}/media/:id (TTL ${TTL_MS / 1000}s)`, runtime);
|
|
26
|
+
mediaServer.unref?.();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const url = `https://${hostname}/media/${saved.id}`;
|
|
30
|
+
return { url, id: saved.id, size: saved.size };
|
|
31
|
+
}
|
|
32
|
+
async function isPortFree(port) {
|
|
33
|
+
try {
|
|
34
|
+
await ensurePortAvailable(port);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err instanceof PortInUseError)
|
|
39
|
+
return false;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { runExec } from "../process/exec.js";
|
|
5
|
+
function isBun() {
|
|
6
|
+
return typeof process.versions.bun === "string";
|
|
7
|
+
}
|
|
8
|
+
function prefersSips() {
|
|
9
|
+
return (process.env.CLAWDBOT_IMAGE_BACKEND === "sips" ||
|
|
10
|
+
(process.env.CLAWDBOT_IMAGE_BACKEND !== "sharp" &&
|
|
11
|
+
isBun() &&
|
|
12
|
+
process.platform === "darwin"));
|
|
13
|
+
}
|
|
14
|
+
async function loadSharp() {
|
|
15
|
+
const mod = (await import("sharp"));
|
|
16
|
+
const sharp = mod.default ?? mod;
|
|
17
|
+
return (buffer) => sharp(buffer, { failOnError: false });
|
|
18
|
+
}
|
|
19
|
+
async function withTempDir(fn) {
|
|
20
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-img-"));
|
|
21
|
+
try {
|
|
22
|
+
return await fn(dir);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function sipsMetadataFromBuffer(buffer) {
|
|
29
|
+
return await withTempDir(async (dir) => {
|
|
30
|
+
const input = path.join(dir, "in.img");
|
|
31
|
+
await fs.writeFile(input, buffer);
|
|
32
|
+
const { stdout } = await runExec("/usr/bin/sips", ["-g", "pixelWidth", "-g", "pixelHeight", input], {
|
|
33
|
+
timeoutMs: 10_000,
|
|
34
|
+
maxBuffer: 512 * 1024,
|
|
35
|
+
});
|
|
36
|
+
const w = stdout.match(/pixelWidth:\s*([0-9]+)/);
|
|
37
|
+
const h = stdout.match(/pixelHeight:\s*([0-9]+)/);
|
|
38
|
+
if (!w?.[1] || !h?.[1])
|
|
39
|
+
return null;
|
|
40
|
+
const width = Number.parseInt(w[1], 10);
|
|
41
|
+
const height = Number.parseInt(h[1], 10);
|
|
42
|
+
if (!Number.isFinite(width) || !Number.isFinite(height))
|
|
43
|
+
return null;
|
|
44
|
+
if (width <= 0 || height <= 0)
|
|
45
|
+
return null;
|
|
46
|
+
return { width, height };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function sipsResizeToJpeg(params) {
|
|
50
|
+
return await withTempDir(async (dir) => {
|
|
51
|
+
const input = path.join(dir, "in.img");
|
|
52
|
+
const output = path.join(dir, "out.jpg");
|
|
53
|
+
await fs.writeFile(input, params.buffer);
|
|
54
|
+
await runExec("/usr/bin/sips", [
|
|
55
|
+
"-Z",
|
|
56
|
+
String(Math.max(1, Math.round(params.maxSide))),
|
|
57
|
+
"-s",
|
|
58
|
+
"format",
|
|
59
|
+
"jpeg",
|
|
60
|
+
"-s",
|
|
61
|
+
"formatOptions",
|
|
62
|
+
String(Math.max(1, Math.min(100, Math.round(params.quality)))),
|
|
63
|
+
input,
|
|
64
|
+
"--out",
|
|
65
|
+
output,
|
|
66
|
+
], { timeoutMs: 20_000, maxBuffer: 1024 * 1024 });
|
|
67
|
+
return await fs.readFile(output);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export async function getImageMetadata(buffer) {
|
|
71
|
+
if (prefersSips()) {
|
|
72
|
+
return await sipsMetadataFromBuffer(buffer).catch(() => null);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const sharp = await loadSharp();
|
|
76
|
+
const meta = await sharp(buffer).metadata();
|
|
77
|
+
const width = Number(meta.width ?? 0);
|
|
78
|
+
const height = Number(meta.height ?? 0);
|
|
79
|
+
if (!Number.isFinite(width) || !Number.isFinite(height))
|
|
80
|
+
return null;
|
|
81
|
+
if (width <= 0 || height <= 0)
|
|
82
|
+
return null;
|
|
83
|
+
return { width, height };
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function resizeToJpeg(params) {
|
|
90
|
+
if (prefersSips()) {
|
|
91
|
+
// Avoid enlarging by checking dimensions first (sips has no withoutEnlargement flag).
|
|
92
|
+
if (params.withoutEnlargement !== false) {
|
|
93
|
+
const meta = await getImageMetadata(params.buffer);
|
|
94
|
+
if (meta) {
|
|
95
|
+
const maxDim = Math.max(meta.width, meta.height);
|
|
96
|
+
if (maxDim > 0 && maxDim <= params.maxSide) {
|
|
97
|
+
return await sipsResizeToJpeg({
|
|
98
|
+
buffer: params.buffer,
|
|
99
|
+
maxSide: maxDim,
|
|
100
|
+
quality: params.quality,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return await sipsResizeToJpeg({
|
|
106
|
+
buffer: params.buffer,
|
|
107
|
+
maxSide: params.maxSide,
|
|
108
|
+
quality: params.quality,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const sharp = await loadSharp();
|
|
112
|
+
return await sharp(params.buffer)
|
|
113
|
+
.resize({
|
|
114
|
+
width: params.maxSide,
|
|
115
|
+
height: params.maxSide,
|
|
116
|
+
fit: "inside",
|
|
117
|
+
withoutEnlargement: params.withoutEnlargement !== false,
|
|
118
|
+
})
|
|
119
|
+
.jpeg({ quality: params.quality, mozjpeg: true })
|
|
120
|
+
.toBuffer();
|
|
121
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
3
|
+
import { mediaKindFromMime } from "./constants.js";
|
|
4
|
+
// Map common mimes to preferred file extensions.
|
|
5
|
+
const EXT_BY_MIME = {
|
|
6
|
+
"image/jpeg": ".jpg",
|
|
7
|
+
"image/png": ".png",
|
|
8
|
+
"image/webp": ".webp",
|
|
9
|
+
"image/gif": ".gif",
|
|
10
|
+
"audio/ogg": ".ogg",
|
|
11
|
+
"audio/mpeg": ".mp3",
|
|
12
|
+
"video/mp4": ".mp4",
|
|
13
|
+
"application/pdf": ".pdf",
|
|
14
|
+
"application/json": ".json",
|
|
15
|
+
"application/zip": ".zip",
|
|
16
|
+
"application/gzip": ".gz",
|
|
17
|
+
"application/x-tar": ".tar",
|
|
18
|
+
"application/x-7z-compressed": ".7z",
|
|
19
|
+
"application/vnd.rar": ".rar",
|
|
20
|
+
"application/msword": ".doc",
|
|
21
|
+
"application/vnd.ms-excel": ".xls",
|
|
22
|
+
"application/vnd.ms-powerpoint": ".ppt",
|
|
23
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
24
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
25
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
|
26
|
+
"text/csv": ".csv",
|
|
27
|
+
"text/plain": ".txt",
|
|
28
|
+
"text/markdown": ".md",
|
|
29
|
+
};
|
|
30
|
+
const MIME_BY_EXT = Object.fromEntries(Object.entries(EXT_BY_MIME).map(([mime, ext]) => [ext, mime]));
|
|
31
|
+
function normalizeHeaderMime(mime) {
|
|
32
|
+
if (!mime)
|
|
33
|
+
return undefined;
|
|
34
|
+
const cleaned = mime.split(";")[0]?.trim().toLowerCase();
|
|
35
|
+
return cleaned || undefined;
|
|
36
|
+
}
|
|
37
|
+
async function sniffMime(buffer) {
|
|
38
|
+
if (!buffer)
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const type = await fileTypeFromBuffer(buffer);
|
|
42
|
+
return type?.mime ?? undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function extFromPath(filePath) {
|
|
49
|
+
if (!filePath)
|
|
50
|
+
return undefined;
|
|
51
|
+
try {
|
|
52
|
+
if (/^https?:\/\//i.test(filePath)) {
|
|
53
|
+
const url = new URL(filePath);
|
|
54
|
+
return path.extname(url.pathname).toLowerCase() || undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// fall back to plain path parsing
|
|
59
|
+
}
|
|
60
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
61
|
+
return ext || undefined;
|
|
62
|
+
}
|
|
63
|
+
export function detectMime(opts) {
|
|
64
|
+
return detectMimeImpl(opts);
|
|
65
|
+
}
|
|
66
|
+
function isGenericMime(mime) {
|
|
67
|
+
if (!mime)
|
|
68
|
+
return true;
|
|
69
|
+
const m = mime.toLowerCase();
|
|
70
|
+
return m === "application/octet-stream" || m === "application/zip";
|
|
71
|
+
}
|
|
72
|
+
async function detectMimeImpl(opts) {
|
|
73
|
+
const ext = extFromPath(opts.filePath);
|
|
74
|
+
const extMime = ext ? MIME_BY_EXT[ext] : undefined;
|
|
75
|
+
const headerMime = normalizeHeaderMime(opts.headerMime);
|
|
76
|
+
const sniffed = await sniffMime(opts.buffer);
|
|
77
|
+
// Prefer sniffed types, but don't let generic container types override a more
|
|
78
|
+
// specific extension mapping (e.g. XLSX vs ZIP).
|
|
79
|
+
if (sniffed && (!isGenericMime(sniffed) || !extMime))
|
|
80
|
+
return sniffed;
|
|
81
|
+
if (extMime)
|
|
82
|
+
return extMime;
|
|
83
|
+
if (headerMime && !isGenericMime(headerMime))
|
|
84
|
+
return headerMime;
|
|
85
|
+
if (sniffed)
|
|
86
|
+
return sniffed;
|
|
87
|
+
if (headerMime)
|
|
88
|
+
return headerMime;
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
export function extensionForMime(mime) {
|
|
92
|
+
if (!mime)
|
|
93
|
+
return undefined;
|
|
94
|
+
return EXT_BY_MIME[mime.toLowerCase()];
|
|
95
|
+
}
|
|
96
|
+
export function imageMimeFromFormat(format) {
|
|
97
|
+
if (!format)
|
|
98
|
+
return undefined;
|
|
99
|
+
switch (format.toLowerCase()) {
|
|
100
|
+
case "jpg":
|
|
101
|
+
case "jpeg":
|
|
102
|
+
return "image/jpeg";
|
|
103
|
+
case "png":
|
|
104
|
+
return "image/png";
|
|
105
|
+
case "webp":
|
|
106
|
+
return "image/webp";
|
|
107
|
+
case "gif":
|
|
108
|
+
return "image/gif";
|
|
109
|
+
default:
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function kindFromMime(mime) {
|
|
114
|
+
return mediaKindFromMime(mime);
|
|
115
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Shared helpers for parsing MEDIA tokens from command/stdout text.
|
|
2
|
+
// Allow optional wrapping backticks and punctuation after the token; capture the core token.
|
|
3
|
+
export const MEDIA_TOKEN_RE = /\bMEDIA:\s*`?([^\n]+)`?/gi;
|
|
4
|
+
export function normalizeMediaSource(src) {
|
|
5
|
+
return src.startsWith("file://") ? src.replace("file://", "") : src;
|
|
6
|
+
}
|
|
7
|
+
function cleanCandidate(raw) {
|
|
8
|
+
return raw.replace(/^[`"'[{(]+/, "").replace(/[`"'\\})\],]+$/, "");
|
|
9
|
+
}
|
|
10
|
+
function isValidMedia(candidate) {
|
|
11
|
+
if (!candidate)
|
|
12
|
+
return false;
|
|
13
|
+
if (candidate.length > 1024)
|
|
14
|
+
return false;
|
|
15
|
+
if (/\s/.test(candidate))
|
|
16
|
+
return false;
|
|
17
|
+
return (/^https?:\/\//i.test(candidate) ||
|
|
18
|
+
candidate.startsWith("/") ||
|
|
19
|
+
candidate.startsWith("./"));
|
|
20
|
+
}
|
|
21
|
+
export function splitMediaFromOutput(raw) {
|
|
22
|
+
const trimmedRaw = raw.trim();
|
|
23
|
+
if (!trimmedRaw)
|
|
24
|
+
return { text: "" };
|
|
25
|
+
const media = [];
|
|
26
|
+
let foundMediaToken = false;
|
|
27
|
+
// Collect tokens line by line so we can strip them cleanly.
|
|
28
|
+
const lines = trimmedRaw.split("\n");
|
|
29
|
+
const keptLines = [];
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const matches = Array.from(line.matchAll(MEDIA_TOKEN_RE));
|
|
32
|
+
if (matches.length === 0) {
|
|
33
|
+
keptLines.push(line);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
foundMediaToken = true;
|
|
37
|
+
const pieces = [];
|
|
38
|
+
let cursor = 0;
|
|
39
|
+
let hasValidMedia = false;
|
|
40
|
+
for (const match of matches) {
|
|
41
|
+
const start = match.index ?? 0;
|
|
42
|
+
pieces.push(line.slice(cursor, start));
|
|
43
|
+
const payload = match[1];
|
|
44
|
+
const parts = payload.split(/\s+/).filter(Boolean);
|
|
45
|
+
const invalidParts = [];
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
const candidate = normalizeMediaSource(cleanCandidate(part));
|
|
48
|
+
if (isValidMedia(candidate)) {
|
|
49
|
+
media.push(candidate);
|
|
50
|
+
hasValidMedia = true;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
invalidParts.push(part);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (hasValidMedia && invalidParts.length > 0) {
|
|
57
|
+
pieces.push(invalidParts.join(" "));
|
|
58
|
+
}
|
|
59
|
+
cursor = start + match[0].length;
|
|
60
|
+
}
|
|
61
|
+
pieces.push(line.slice(cursor));
|
|
62
|
+
const cleanedLine = pieces
|
|
63
|
+
.join("")
|
|
64
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
65
|
+
.trim();
|
|
66
|
+
// If the line becomes empty, drop it.
|
|
67
|
+
if (cleanedLine) {
|
|
68
|
+
keptLines.push(cleanedLine);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const cleanedText = keptLines
|
|
72
|
+
.join("\n")
|
|
73
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
74
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
75
|
+
.replace(/\n{2,}/g, "\n")
|
|
76
|
+
.trim();
|
|
77
|
+
if (media.length === 0) {
|
|
78
|
+
return { text: foundMediaToken ? cleanedText : trimmedRaw };
|
|
79
|
+
}
|
|
80
|
+
return { text: cleanedText, mediaUrls: media, mediaUrl: media[0] };
|
|
81
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import { danger } from "../globals.js";
|
|
5
|
+
import { defaultRuntime } from "../runtime.js";
|
|
6
|
+
import { detectMime } from "./mime.js";
|
|
7
|
+
import { cleanOldMedia, getMediaDir } from "./store.js";
|
|
8
|
+
const DEFAULT_TTL_MS = 2 * 60 * 1000;
|
|
9
|
+
export function attachMediaRoutes(app, ttlMs = DEFAULT_TTL_MS, _runtime = defaultRuntime) {
|
|
10
|
+
const mediaDir = getMediaDir();
|
|
11
|
+
app.get("/media/:id", async (req, res) => {
|
|
12
|
+
const id = req.params.id;
|
|
13
|
+
const mediaRoot = (await fs.realpath(mediaDir)) + path.sep;
|
|
14
|
+
const file = path.resolve(mediaRoot, id);
|
|
15
|
+
try {
|
|
16
|
+
const lstat = await fs.lstat(file);
|
|
17
|
+
if (lstat.isSymbolicLink()) {
|
|
18
|
+
res.status(400).send("invalid path");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const realPath = await fs.realpath(file);
|
|
22
|
+
if (!realPath.startsWith(mediaRoot)) {
|
|
23
|
+
res.status(400).send("invalid path");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const stat = await fs.stat(realPath);
|
|
27
|
+
if (Date.now() - stat.mtimeMs > ttlMs) {
|
|
28
|
+
await fs.rm(realPath).catch(() => { });
|
|
29
|
+
res.status(410).send("expired");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const data = await fs.readFile(realPath);
|
|
33
|
+
const mime = await detectMime({ buffer: data, filePath: realPath });
|
|
34
|
+
if (mime)
|
|
35
|
+
res.type(mime);
|
|
36
|
+
res.send(data);
|
|
37
|
+
// best-effort single-use cleanup after response ends
|
|
38
|
+
res.on("finish", () => {
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
fs.rm(realPath).catch(() => { });
|
|
41
|
+
}, 50);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
res.status(404).send("not found");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// periodic cleanup
|
|
49
|
+
setInterval(() => {
|
|
50
|
+
void cleanOldMedia(ttlMs);
|
|
51
|
+
}, ttlMs).unref();
|
|
52
|
+
}
|
|
53
|
+
export async function startMediaServer(port, ttlMs = DEFAULT_TTL_MS, runtime = defaultRuntime) {
|
|
54
|
+
const app = express();
|
|
55
|
+
attachMediaRoutes(app, ttlMs, runtime);
|
|
56
|
+
return await new Promise((resolve, reject) => {
|
|
57
|
+
const server = app.listen(port);
|
|
58
|
+
server.once("listening", () => resolve(server));
|
|
59
|
+
server.once("error", (err) => {
|
|
60
|
+
runtime.error(danger(`Media server failed: ${String(err)}`));
|
|
61
|
+
reject(err);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { request } from "node:https";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { CONFIG_DIR } from "../utils.js";
|
|
8
|
+
import { detectMime, extensionForMime } from "./mime.js";
|
|
9
|
+
const MEDIA_DIR = path.join(CONFIG_DIR, "media");
|
|
10
|
+
const MAX_BYTES = 5 * 1024 * 1024; // 5MB default
|
|
11
|
+
const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
|
|
12
|
+
export function getMediaDir() {
|
|
13
|
+
return MEDIA_DIR;
|
|
14
|
+
}
|
|
15
|
+
export async function ensureMediaDir() {
|
|
16
|
+
await fs.mkdir(MEDIA_DIR, { recursive: true });
|
|
17
|
+
return MEDIA_DIR;
|
|
18
|
+
}
|
|
19
|
+
export async function cleanOldMedia(ttlMs = DEFAULT_TTL_MS) {
|
|
20
|
+
await ensureMediaDir();
|
|
21
|
+
const entries = await fs.readdir(MEDIA_DIR).catch(() => []);
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
await Promise.all(entries.map(async (file) => {
|
|
24
|
+
const full = path.join(MEDIA_DIR, file);
|
|
25
|
+
const stat = await fs.stat(full).catch(() => null);
|
|
26
|
+
if (!stat)
|
|
27
|
+
return;
|
|
28
|
+
if (now - stat.mtimeMs > ttlMs) {
|
|
29
|
+
await fs.rm(full).catch(() => { });
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
function looksLikeUrl(src) {
|
|
34
|
+
return /^https?:\/\//i.test(src);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Download media to disk while capturing the first few KB for mime sniffing.
|
|
38
|
+
*/
|
|
39
|
+
async function downloadToFile(url, dest, headers, maxRedirects = 5) {
|
|
40
|
+
return await new Promise((resolve, reject) => {
|
|
41
|
+
const req = request(url, { headers }, (res) => {
|
|
42
|
+
// Follow redirects
|
|
43
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400) {
|
|
44
|
+
const location = res.headers.location;
|
|
45
|
+
if (!location || maxRedirects <= 0) {
|
|
46
|
+
reject(new Error(`Redirect loop or missing Location header`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const redirectUrl = new URL(location, url).href;
|
|
50
|
+
resolve(downloadToFile(redirectUrl, dest, headers, maxRedirects - 1));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!res.statusCode || res.statusCode >= 400) {
|
|
54
|
+
reject(new Error(`HTTP ${res.statusCode ?? "?"} downloading media`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let total = 0;
|
|
58
|
+
const sniffChunks = [];
|
|
59
|
+
let sniffLen = 0;
|
|
60
|
+
const out = createWriteStream(dest);
|
|
61
|
+
res.on("data", (chunk) => {
|
|
62
|
+
total += chunk.length;
|
|
63
|
+
if (sniffLen < 16384) {
|
|
64
|
+
sniffChunks.push(chunk);
|
|
65
|
+
sniffLen += chunk.length;
|
|
66
|
+
}
|
|
67
|
+
if (total > MAX_BYTES) {
|
|
68
|
+
req.destroy(new Error("Media exceeds 5MB limit"));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
pipeline(res, out)
|
|
72
|
+
.then(() => {
|
|
73
|
+
const sniffBuffer = Buffer.concat(sniffChunks, Math.min(sniffLen, 16384));
|
|
74
|
+
const rawHeader = res.headers["content-type"];
|
|
75
|
+
const headerMime = Array.isArray(rawHeader)
|
|
76
|
+
? rawHeader[0]
|
|
77
|
+
: rawHeader;
|
|
78
|
+
resolve({
|
|
79
|
+
headerMime,
|
|
80
|
+
sniffBuffer,
|
|
81
|
+
size: total,
|
|
82
|
+
});
|
|
83
|
+
})
|
|
84
|
+
.catch(reject);
|
|
85
|
+
});
|
|
86
|
+
req.on("error", reject);
|
|
87
|
+
req.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export async function saveMediaSource(source, headers, subdir = "") {
|
|
91
|
+
const dir = subdir ? path.join(MEDIA_DIR, subdir) : MEDIA_DIR;
|
|
92
|
+
await fs.mkdir(dir, { recursive: true });
|
|
93
|
+
await cleanOldMedia();
|
|
94
|
+
const baseId = crypto.randomUUID();
|
|
95
|
+
if (looksLikeUrl(source)) {
|
|
96
|
+
const tempDest = path.join(dir, `${baseId}.tmp`);
|
|
97
|
+
const { headerMime, sniffBuffer, size } = await downloadToFile(source, tempDest, headers);
|
|
98
|
+
const mime = await detectMime({
|
|
99
|
+
buffer: sniffBuffer,
|
|
100
|
+
headerMime,
|
|
101
|
+
filePath: source,
|
|
102
|
+
});
|
|
103
|
+
const ext = extensionForMime(mime) ?? path.extname(new URL(source).pathname);
|
|
104
|
+
const id = ext ? `${baseId}${ext}` : baseId;
|
|
105
|
+
const finalDest = path.join(dir, id);
|
|
106
|
+
await fs.rename(tempDest, finalDest);
|
|
107
|
+
return { id, path: finalDest, size, contentType: mime };
|
|
108
|
+
}
|
|
109
|
+
// local path
|
|
110
|
+
const stat = await fs.stat(source);
|
|
111
|
+
if (!stat.isFile()) {
|
|
112
|
+
throw new Error("Media path is not a file");
|
|
113
|
+
}
|
|
114
|
+
if (stat.size > MAX_BYTES) {
|
|
115
|
+
throw new Error("Media exceeds 5MB limit");
|
|
116
|
+
}
|
|
117
|
+
const buffer = await fs.readFile(source);
|
|
118
|
+
const mime = await detectMime({ buffer, filePath: source });
|
|
119
|
+
const ext = extensionForMime(mime) ?? path.extname(source);
|
|
120
|
+
const id = ext ? `${baseId}${ext}` : baseId;
|
|
121
|
+
const dest = path.join(dir, id);
|
|
122
|
+
await fs.writeFile(dest, buffer);
|
|
123
|
+
return { id, path: dest, size: stat.size, contentType: mime };
|
|
124
|
+
}
|
|
125
|
+
export async function saveMediaBuffer(buffer, contentType, subdir = "inbound", maxBytes = MAX_BYTES) {
|
|
126
|
+
if (buffer.byteLength > maxBytes) {
|
|
127
|
+
throw new Error(`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
|
|
128
|
+
}
|
|
129
|
+
const dir = path.join(MEDIA_DIR, subdir);
|
|
130
|
+
await fs.mkdir(dir, { recursive: true });
|
|
131
|
+
const baseId = crypto.randomUUID();
|
|
132
|
+
const headerExt = extensionForMime(contentType?.split(";")[0]?.trim() ?? undefined);
|
|
133
|
+
const mime = await detectMime({ buffer, headerMime: contentType });
|
|
134
|
+
const ext = headerExt ?? extensionForMime(mime);
|
|
135
|
+
const id = ext ? `${baseId}${ext}` : baseId;
|
|
136
|
+
const dest = path.join(dir, id);
|
|
137
|
+
await fs.writeFile(dest, buffer);
|
|
138
|
+
return { id, path: dest, size: buffer.byteLength, contentType: mime };
|
|
139
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Minimal in-process queue to serialize command executions.
|
|
2
|
+
// Default lane ("main") preserves the existing behavior. Additional lanes allow
|
|
3
|
+
// low-risk parallelism (e.g. cron jobs) without interleaving stdin / logs for
|
|
4
|
+
// the main auto-reply workflow.
|
|
5
|
+
const lanes = new Map();
|
|
6
|
+
function getLaneState(lane) {
|
|
7
|
+
const existing = lanes.get(lane);
|
|
8
|
+
if (existing)
|
|
9
|
+
return existing;
|
|
10
|
+
const created = {
|
|
11
|
+
lane,
|
|
12
|
+
queue: [],
|
|
13
|
+
active: 0,
|
|
14
|
+
maxConcurrent: 1,
|
|
15
|
+
draining: false,
|
|
16
|
+
};
|
|
17
|
+
lanes.set(lane, created);
|
|
18
|
+
return created;
|
|
19
|
+
}
|
|
20
|
+
function drainLane(lane) {
|
|
21
|
+
const state = getLaneState(lane);
|
|
22
|
+
if (state.draining)
|
|
23
|
+
return;
|
|
24
|
+
state.draining = true;
|
|
25
|
+
const pump = () => {
|
|
26
|
+
while (state.active < state.maxConcurrent && state.queue.length > 0) {
|
|
27
|
+
const entry = state.queue.shift();
|
|
28
|
+
const waitedMs = Date.now() - entry.enqueuedAt;
|
|
29
|
+
if (waitedMs >= entry.warnAfterMs) {
|
|
30
|
+
entry.onWait?.(waitedMs, state.queue.length);
|
|
31
|
+
}
|
|
32
|
+
state.active += 1;
|
|
33
|
+
void (async () => {
|
|
34
|
+
try {
|
|
35
|
+
const result = await entry.task();
|
|
36
|
+
state.active -= 1;
|
|
37
|
+
pump();
|
|
38
|
+
entry.resolve(result);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
state.active -= 1;
|
|
42
|
+
pump();
|
|
43
|
+
entry.reject(err);
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
}
|
|
47
|
+
state.draining = false;
|
|
48
|
+
};
|
|
49
|
+
pump();
|
|
50
|
+
}
|
|
51
|
+
export function setCommandLaneConcurrency(lane, maxConcurrent) {
|
|
52
|
+
const cleaned = lane.trim() || "main";
|
|
53
|
+
const state = getLaneState(cleaned);
|
|
54
|
+
state.maxConcurrent = Math.max(1, Math.floor(maxConcurrent));
|
|
55
|
+
drainLane(cleaned);
|
|
56
|
+
}
|
|
57
|
+
export function enqueueCommandInLane(lane, task, opts) {
|
|
58
|
+
const cleaned = lane.trim() || "main";
|
|
59
|
+
const warnAfterMs = opts?.warnAfterMs ?? 2_000;
|
|
60
|
+
const state = getLaneState(cleaned);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
state.queue.push({
|
|
63
|
+
task: () => task(),
|
|
64
|
+
resolve: (value) => resolve(value),
|
|
65
|
+
reject,
|
|
66
|
+
enqueuedAt: Date.now(),
|
|
67
|
+
warnAfterMs,
|
|
68
|
+
onWait: opts?.onWait,
|
|
69
|
+
});
|
|
70
|
+
drainLane(cleaned);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function enqueueCommand(task, opts) {
|
|
74
|
+
return enqueueCommandInLane("main", task, opts);
|
|
75
|
+
}
|
|
76
|
+
export function getQueueSize(lane = "main") {
|
|
77
|
+
const state = lanes.get(lane);
|
|
78
|
+
if (!state)
|
|
79
|
+
return 0;
|
|
80
|
+
return state.queue.length + state.active;
|
|
81
|
+
}
|
|
82
|
+
export function getTotalQueueSize() {
|
|
83
|
+
let total = 0;
|
|
84
|
+
for (const s of lanes.values()) {
|
|
85
|
+
total += s.queue.length + s.active;
|
|
86
|
+
}
|
|
87
|
+
return total;
|
|
88
|
+
}
|
|
89
|
+
export function clearCommandLane(lane = "main") {
|
|
90
|
+
const cleaned = lane.trim() || "main";
|
|
91
|
+
const state = lanes.get(cleaned);
|
|
92
|
+
if (!state)
|
|
93
|
+
return 0;
|
|
94
|
+
const removed = state.queue.length;
|
|
95
|
+
state.queue.length = 0;
|
|
96
|
+
return removed;
|
|
97
|
+
}
|