cowork-os 0.3.21
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/LICENSE +21 -0
- package/README.md +1638 -0
- package/bin/cowork.js +42 -0
- package/build/entitlements.mac.plist +16 -0
- package/build/icon.icns +0 -0
- package/build/icon.png +0 -0
- package/dist/electron/electron/activity/ActivityRepository.js +190 -0
- package/dist/electron/electron/agent/browser/browser-service.js +639 -0
- package/dist/electron/electron/agent/context-manager.js +225 -0
- package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
- package/dist/electron/electron/agent/daemon.js +975 -0
- package/dist/electron/electron/agent/executor.js +3561 -0
- package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
- package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
- package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
- package/dist/electron/electron/agent/llm/index.js +34 -0
- package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
- package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
- package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
- package/dist/electron/electron/agent/llm/pricing.js +160 -0
- package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
- package/dist/electron/electron/agent/llm/types.js +178 -0
- package/dist/electron/electron/agent/queue-manager.js +378 -0
- package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
- package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
- package/dist/electron/electron/agent/sandbox/runner.js +410 -0
- package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
- package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
- package/dist/electron/electron/agent/search/brave-provider.js +119 -0
- package/dist/electron/electron/agent/search/google-provider.js +100 -0
- package/dist/electron/electron/agent/search/index.js +28 -0
- package/dist/electron/electron/agent/search/provider-factory.js +395 -0
- package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
- package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
- package/dist/electron/electron/agent/search/types.js +40 -0
- package/dist/electron/electron/agent/security/index.js +12 -0
- package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
- package/dist/electron/electron/agent/security/output-filter.js +217 -0
- package/dist/electron/electron/agent/skill-eligibility.js +281 -0
- package/dist/electron/electron/agent/skill-registry.js +396 -0
- package/dist/electron/electron/agent/skills/document.js +878 -0
- package/dist/electron/electron/agent/skills/image-generator.js +225 -0
- package/dist/electron/electron/agent/skills/organizer.js +141 -0
- package/dist/electron/electron/agent/skills/presentation.js +367 -0
- package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
- package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
- package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
- package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
- package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
- package/dist/electron/electron/agent/tools/file-tools.js +719 -0
- package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
- package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
- package/dist/electron/electron/agent/tools/image-tools.js +111 -0
- package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
- package/dist/electron/electron/agent/tools/node-tools.js +476 -0
- package/dist/electron/electron/agent/tools/registry.js +2719 -0
- package/dist/electron/electron/agent/tools/search-tools.js +91 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
- package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
- package/dist/electron/electron/agent/tools/system-tools.js +578 -0
- package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
- package/dist/electron/electron/agent/tools/x-tools.js +264 -0
- package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
- package/dist/electron/electron/agents/HeartbeatService.js +356 -0
- package/dist/electron/electron/agents/MentionRepository.js +197 -0
- package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
- package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
- package/dist/electron/electron/canvas/canvas-manager.js +714 -0
- package/dist/electron/electron/canvas/canvas-preload.js +53 -0
- package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
- package/dist/electron/electron/canvas/canvas-store.js +174 -0
- package/dist/electron/electron/canvas/index.js +13 -0
- package/dist/electron/electron/control-plane/client.js +364 -0
- package/dist/electron/electron/control-plane/handlers.js +572 -0
- package/dist/electron/electron/control-plane/index.js +41 -0
- package/dist/electron/electron/control-plane/node-manager.js +264 -0
- package/dist/electron/electron/control-plane/protocol.js +194 -0
- package/dist/electron/electron/control-plane/remote-client.js +437 -0
- package/dist/electron/electron/control-plane/server.js +640 -0
- package/dist/electron/electron/control-plane/settings.js +369 -0
- package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
- package/dist/electron/electron/cron/index.js +30 -0
- package/dist/electron/electron/cron/schedule.js +190 -0
- package/dist/electron/electron/cron/service.js +614 -0
- package/dist/electron/electron/cron/store.js +155 -0
- package/dist/electron/electron/cron/types.js +82 -0
- package/dist/electron/electron/cron/webhook.js +258 -0
- package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
- package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
- package/dist/electron/electron/database/repositories.js +1781 -0
- package/dist/electron/electron/database/schema.js +978 -0
- package/dist/electron/electron/extensions/index.js +33 -0
- package/dist/electron/electron/extensions/loader.js +313 -0
- package/dist/electron/electron/extensions/registry.js +485 -0
- package/dist/electron/electron/extensions/types.js +11 -0
- package/dist/electron/electron/gateway/channel-registry.js +1102 -0
- package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
- package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
- package/dist/electron/electron/gateway/channels/discord.js +975 -0
- package/dist/electron/electron/gateway/channels/email-client.js +593 -0
- package/dist/electron/electron/gateway/channels/email.js +443 -0
- package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
- package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
- package/dist/electron/electron/gateway/channels/imessage.js +465 -0
- package/dist/electron/electron/gateway/channels/index.js +36 -0
- package/dist/electron/electron/gateway/channels/line-client.js +470 -0
- package/dist/electron/electron/gateway/channels/line.js +479 -0
- package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
- package/dist/electron/electron/gateway/channels/matrix.js +592 -0
- package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
- package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
- package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
- package/dist/electron/electron/gateway/channels/signal.js +582 -0
- package/dist/electron/electron/gateway/channels/slack.js +415 -0
- package/dist/electron/electron/gateway/channels/teams.js +596 -0
- package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
- package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
- package/dist/electron/electron/gateway/channels/twitch.js +396 -0
- package/dist/electron/electron/gateway/channels/types.js +8 -0
- package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
- package/dist/electron/electron/gateway/context-policy.js +268 -0
- package/dist/electron/electron/gateway/index.js +1063 -0
- package/dist/electron/electron/gateway/infrastructure.js +496 -0
- package/dist/electron/electron/gateway/router.js +2700 -0
- package/dist/electron/electron/gateway/security.js +375 -0
- package/dist/electron/electron/gateway/session.js +115 -0
- package/dist/electron/electron/gateway/tunnel.js +503 -0
- package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
- package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
- package/dist/electron/electron/hooks/index.js +46 -0
- package/dist/electron/electron/hooks/mappings.js +381 -0
- package/dist/electron/electron/hooks/server.js +480 -0
- package/dist/electron/electron/hooks/settings.js +447 -0
- package/dist/electron/electron/hooks/types.js +41 -0
- package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
- package/dist/electron/electron/ipc/handlers.js +3138 -0
- package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
- package/dist/electron/electron/main.js +448 -0
- package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
- package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
- package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
- package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
- package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
- package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
- package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
- package/dist/electron/electron/mcp/settings.js +446 -0
- package/dist/electron/electron/mcp/types.js +59 -0
- package/dist/electron/electron/memory/MemoryService.js +435 -0
- package/dist/electron/electron/notifications/index.js +17 -0
- package/dist/electron/electron/notifications/service.js +118 -0
- package/dist/electron/electron/notifications/store.js +144 -0
- package/dist/electron/electron/preload.js +842 -0
- package/dist/electron/electron/reports/StandupReportService.js +272 -0
- package/dist/electron/electron/security/concurrency.js +293 -0
- package/dist/electron/electron/security/index.js +15 -0
- package/dist/electron/electron/security/policy-manager.js +435 -0
- package/dist/electron/electron/settings/appearance-manager.js +193 -0
- package/dist/electron/electron/settings/personality-manager.js +724 -0
- package/dist/electron/electron/settings/x-manager.js +58 -0
- package/dist/electron/electron/tailscale/exposure.js +188 -0
- package/dist/electron/electron/tailscale/index.js +28 -0
- package/dist/electron/electron/tailscale/settings.js +205 -0
- package/dist/electron/electron/tailscale/tailscale.js +355 -0
- package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
- package/dist/electron/electron/tray/TrayManager.js +895 -0
- package/dist/electron/electron/tray/index.js +9 -0
- package/dist/electron/electron/updater/index.js +6 -0
- package/dist/electron/electron/updater/update-manager.js +418 -0
- package/dist/electron/electron/utils/env-migration.js +209 -0
- package/dist/electron/electron/utils/process.js +102 -0
- package/dist/electron/electron/utils/rate-limiter.js +104 -0
- package/dist/electron/electron/utils/validation.js +419 -0
- package/dist/electron/electron/utils/x-cli.js +177 -0
- package/dist/electron/electron/voice/VoiceService.js +507 -0
- package/dist/electron/electron/voice/index.js +14 -0
- package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
- package/dist/electron/shared/channelMessages.js +170 -0
- package/dist/electron/shared/types.js +1185 -0
- package/package.json +159 -0
- package/resources/skills/1password.json +10 -0
- package/resources/skills/add-documentation.json +31 -0
- package/resources/skills/analyze-csv.json +17 -0
- package/resources/skills/apple-notes.json +10 -0
- package/resources/skills/apple-reminders.json +10 -0
- package/resources/skills/auto-commenter.json +10 -0
- package/resources/skills/bear-notes.json +10 -0
- package/resources/skills/bird.json +35 -0
- package/resources/skills/blogwatcher.json +10 -0
- package/resources/skills/blucli.json +10 -0
- package/resources/skills/bluebubbles.json +10 -0
- package/resources/skills/camsnap.json +10 -0
- package/resources/skills/clean-imports.json +18 -0
- package/resources/skills/code-review.json +18 -0
- package/resources/skills/coding-agent.json +10 -0
- package/resources/skills/compare-files.json +23 -0
- package/resources/skills/convert-code.json +34 -0
- package/resources/skills/create-changelog.json +24 -0
- package/resources/skills/debug-error.json +17 -0
- package/resources/skills/dependency-check.json +10 -0
- package/resources/skills/discord.json +10 -0
- package/resources/skills/eightctl.json +10 -0
- package/resources/skills/explain-code.json +29 -0
- package/resources/skills/extract-todos.json +18 -0
- package/resources/skills/food-order.json +10 -0
- package/resources/skills/gemini.json +10 -0
- package/resources/skills/generate-readme.json +10 -0
- package/resources/skills/gifgrep.json +10 -0
- package/resources/skills/git-commit.json +10 -0
- package/resources/skills/github.json +10 -0
- package/resources/skills/gog.json +10 -0
- package/resources/skills/goplaces.json +10 -0
- package/resources/skills/himalaya.json +10 -0
- package/resources/skills/imsg.json +10 -0
- package/resources/skills/karpathy-guidelines.json +12 -0
- package/resources/skills/last30days.json +26 -0
- package/resources/skills/local-places.json +10 -0
- package/resources/skills/mcporter.json +10 -0
- package/resources/skills/model-usage.json +10 -0
- package/resources/skills/nano-banana-pro.json +10 -0
- package/resources/skills/nano-pdf.json +10 -0
- package/resources/skills/notion.json +10 -0
- package/resources/skills/obsidian.json +10 -0
- package/resources/skills/openai-image-gen.json +10 -0
- package/resources/skills/openai-whisper-api.json +10 -0
- package/resources/skills/openai-whisper.json +10 -0
- package/resources/skills/openhue.json +10 -0
- package/resources/skills/oracle.json +10 -0
- package/resources/skills/ordercli.json +10 -0
- package/resources/skills/peekaboo.json +10 -0
- package/resources/skills/project-structure.json +10 -0
- package/resources/skills/proofread.json +17 -0
- package/resources/skills/refactor-code.json +31 -0
- package/resources/skills/rename-symbol.json +23 -0
- package/resources/skills/sag.json +10 -0
- package/resources/skills/security-audit.json +18 -0
- package/resources/skills/session-logs.json +10 -0
- package/resources/skills/sherpa-onnx-tts.json +10 -0
- package/resources/skills/skill-creator.json +15 -0
- package/resources/skills/skill-hub.json +29 -0
- package/resources/skills/slack.json +10 -0
- package/resources/skills/songsee.json +10 -0
- package/resources/skills/sonoscli.json +10 -0
- package/resources/skills/spotify-player.json +10 -0
- package/resources/skills/startup-cfo.json +55 -0
- package/resources/skills/summarize-folder.json +18 -0
- package/resources/skills/summarize.json +10 -0
- package/resources/skills/things-mac.json +10 -0
- package/resources/skills/tmux.json +10 -0
- package/resources/skills/translate.json +36 -0
- package/resources/skills/trello.json +10 -0
- package/resources/skills/video-frames.json +10 -0
- package/resources/skills/voice-call.json +10 -0
- package/resources/skills/wacli.json +10 -0
- package/resources/skills/weather.json +10 -0
- package/resources/skills/write-tests.json +31 -0
- package/src/electron/activity/ActivityRepository.ts +238 -0
- package/src/electron/agent/browser/browser-service.ts +721 -0
- package/src/electron/agent/context-manager.ts +257 -0
- package/src/electron/agent/custom-skill-loader.ts +634 -0
- package/src/electron/agent/daemon.ts +1097 -0
- package/src/electron/agent/executor.ts +4017 -0
- package/src/electron/agent/llm/anthropic-provider.ts +175 -0
- package/src/electron/agent/llm/bedrock-provider.ts +236 -0
- package/src/electron/agent/llm/gemini-provider.ts +422 -0
- package/src/electron/agent/llm/index.ts +9 -0
- package/src/electron/agent/llm/ollama-provider.ts +347 -0
- package/src/electron/agent/llm/openai-oauth.ts +127 -0
- package/src/electron/agent/llm/openai-provider.ts +686 -0
- package/src/electron/agent/llm/openrouter-provider.ts +273 -0
- package/src/electron/agent/llm/pricing.ts +180 -0
- package/src/electron/agent/llm/provider-factory.ts +971 -0
- package/src/electron/agent/llm/types.ts +291 -0
- package/src/electron/agent/queue-manager.ts +408 -0
- package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
- package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
- package/src/electron/agent/sandbox/runner.ts +453 -0
- package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
- package/src/electron/agent/sandbox/security-utils.ts +251 -0
- package/src/electron/agent/search/brave-provider.ts +141 -0
- package/src/electron/agent/search/google-provider.ts +131 -0
- package/src/electron/agent/search/index.ts +6 -0
- package/src/electron/agent/search/provider-factory.ts +450 -0
- package/src/electron/agent/search/serpapi-provider.ts +138 -0
- package/src/electron/agent/search/tavily-provider.ts +108 -0
- package/src/electron/agent/search/types.ts +118 -0
- package/src/electron/agent/security/index.ts +20 -0
- package/src/electron/agent/security/input-sanitizer.ts +380 -0
- package/src/electron/agent/security/output-filter.ts +259 -0
- package/src/electron/agent/skill-eligibility.ts +334 -0
- package/src/electron/agent/skill-registry.ts +457 -0
- package/src/electron/agent/skills/document.ts +1070 -0
- package/src/electron/agent/skills/image-generator.ts +272 -0
- package/src/electron/agent/skills/organizer.ts +131 -0
- package/src/electron/agent/skills/presentation.ts +418 -0
- package/src/electron/agent/skills/spreadsheet.ts +166 -0
- package/src/electron/agent/tools/browser-tools.ts +546 -0
- package/src/electron/agent/tools/builtin-settings.ts +422 -0
- package/src/electron/agent/tools/canvas-tools.ts +572 -0
- package/src/electron/agent/tools/cron-tools.ts +723 -0
- package/src/electron/agent/tools/edit-tools.ts +196 -0
- package/src/electron/agent/tools/file-tools.ts +811 -0
- package/src/electron/agent/tools/glob-tools.ts +303 -0
- package/src/electron/agent/tools/grep-tools.ts +432 -0
- package/src/electron/agent/tools/image-tools.ts +126 -0
- package/src/electron/agent/tools/mention-tools.ts +371 -0
- package/src/electron/agent/tools/node-tools.ts +550 -0
- package/src/electron/agent/tools/registry.ts +3052 -0
- package/src/electron/agent/tools/search-tools.ts +111 -0
- package/src/electron/agent/tools/shell-tools.ts +651 -0
- package/src/electron/agent/tools/skill-tools.ts +340 -0
- package/src/electron/agent/tools/system-tools.ts +665 -0
- package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
- package/src/electron/agent/tools/x-tools.ts +267 -0
- package/src/electron/agents/AgentRoleRepository.ts +557 -0
- package/src/electron/agents/HeartbeatService.ts +469 -0
- package/src/electron/agents/MentionRepository.ts +242 -0
- package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
- package/src/electron/agents/WorkingStateRepository.ts +278 -0
- package/src/electron/canvas/canvas-manager.ts +818 -0
- package/src/electron/canvas/canvas-preload.ts +102 -0
- package/src/electron/canvas/canvas-protocol.ts +174 -0
- package/src/electron/canvas/canvas-store.ts +200 -0
- package/src/electron/canvas/index.ts +8 -0
- package/src/electron/control-plane/client.ts +527 -0
- package/src/electron/control-plane/handlers.ts +723 -0
- package/src/electron/control-plane/index.ts +51 -0
- package/src/electron/control-plane/node-manager.ts +322 -0
- package/src/electron/control-plane/protocol.ts +269 -0
- package/src/electron/control-plane/remote-client.ts +517 -0
- package/src/electron/control-plane/server.ts +853 -0
- package/src/electron/control-plane/settings.ts +401 -0
- package/src/electron/control-plane/ssh-tunnel.ts +624 -0
- package/src/electron/cron/index.ts +9 -0
- package/src/electron/cron/schedule.ts +217 -0
- package/src/electron/cron/service.ts +743 -0
- package/src/electron/cron/store.ts +165 -0
- package/src/electron/cron/types.ts +291 -0
- package/src/electron/cron/webhook.ts +303 -0
- package/src/electron/database/SecureSettingsRepository.ts +514 -0
- package/src/electron/database/TaskLabelRepository.ts +148 -0
- package/src/electron/database/repositories.ts +2397 -0
- package/src/electron/database/schema.ts +1017 -0
- package/src/electron/extensions/index.ts +18 -0
- package/src/electron/extensions/loader.ts +336 -0
- package/src/electron/extensions/registry.ts +546 -0
- package/src/electron/extensions/types.ts +372 -0
- package/src/electron/gateway/channel-registry.ts +1267 -0
- package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
- package/src/electron/gateway/channels/bluebubbles.ts +509 -0
- package/src/electron/gateway/channels/discord.ts +1150 -0
- package/src/electron/gateway/channels/email-client.ts +708 -0
- package/src/electron/gateway/channels/email.ts +516 -0
- package/src/electron/gateway/channels/google-chat.ts +760 -0
- package/src/electron/gateway/channels/imessage-client.ts +473 -0
- package/src/electron/gateway/channels/imessage.ts +520 -0
- package/src/electron/gateway/channels/index.ts +21 -0
- package/src/electron/gateway/channels/line-client.ts +598 -0
- package/src/electron/gateway/channels/line.ts +559 -0
- package/src/electron/gateway/channels/matrix-client.ts +632 -0
- package/src/electron/gateway/channels/matrix.ts +655 -0
- package/src/electron/gateway/channels/mattermost-client.ts +526 -0
- package/src/electron/gateway/channels/mattermost.ts +550 -0
- package/src/electron/gateway/channels/signal-client.ts +722 -0
- package/src/electron/gateway/channels/signal.ts +666 -0
- package/src/electron/gateway/channels/slack.ts +458 -0
- package/src/electron/gateway/channels/teams.ts +681 -0
- package/src/electron/gateway/channels/telegram.ts +1727 -0
- package/src/electron/gateway/channels/twitch-client.ts +665 -0
- package/src/electron/gateway/channels/twitch.ts +468 -0
- package/src/electron/gateway/channels/types.ts +1002 -0
- package/src/electron/gateway/channels/whatsapp.ts +1101 -0
- package/src/electron/gateway/context-policy.ts +382 -0
- package/src/electron/gateway/index.ts +1274 -0
- package/src/electron/gateway/infrastructure.ts +645 -0
- package/src/electron/gateway/router.ts +3206 -0
- package/src/electron/gateway/security.ts +422 -0
- package/src/electron/gateway/session.ts +144 -0
- package/src/electron/gateway/tunnel.ts +626 -0
- package/src/electron/guardrails/guardrail-manager.ts +380 -0
- package/src/electron/hooks/gmail-watcher.ts +355 -0
- package/src/electron/hooks/index.ts +30 -0
- package/src/electron/hooks/mappings.ts +404 -0
- package/src/electron/hooks/server.ts +574 -0
- package/src/electron/hooks/settings.ts +466 -0
- package/src/electron/hooks/types.ts +245 -0
- package/src/electron/ipc/canvas-handlers.ts +223 -0
- package/src/electron/ipc/handlers.ts +3661 -0
- package/src/electron/ipc/mission-control-handlers.ts +182 -0
- package/src/electron/main.ts +496 -0
- package/src/electron/mcp/client/MCPClientManager.ts +406 -0
- package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
- package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
- package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
- package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
- package/src/electron/mcp/host/MCPHostServer.ts +388 -0
- package/src/electron/mcp/host/ToolAdapter.ts +140 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
- package/src/electron/mcp/settings.ts +468 -0
- package/src/electron/mcp/types.ts +371 -0
- package/src/electron/memory/MemoryService.ts +523 -0
- package/src/electron/notifications/index.ts +16 -0
- package/src/electron/notifications/service.ts +161 -0
- package/src/electron/notifications/store.ts +163 -0
- package/src/electron/preload.ts +2845 -0
- package/src/electron/reports/StandupReportService.ts +356 -0
- package/src/electron/security/concurrency.ts +333 -0
- package/src/electron/security/index.ts +17 -0
- package/src/electron/security/policy-manager.ts +539 -0
- package/src/electron/settings/appearance-manager.ts +182 -0
- package/src/electron/settings/personality-manager.ts +800 -0
- package/src/electron/settings/x-manager.ts +62 -0
- package/src/electron/tailscale/exposure.ts +262 -0
- package/src/electron/tailscale/index.ts +34 -0
- package/src/electron/tailscale/settings.ts +218 -0
- package/src/electron/tailscale/tailscale.ts +379 -0
- package/src/electron/tray/QuickInputWindow.ts +609 -0
- package/src/electron/tray/TrayManager.ts +1005 -0
- package/src/electron/tray/index.ts +6 -0
- package/src/electron/updater/index.ts +1 -0
- package/src/electron/updater/update-manager.ts +447 -0
- package/src/electron/utils/env-migration.ts +203 -0
- package/src/electron/utils/process.ts +124 -0
- package/src/electron/utils/rate-limiter.ts +130 -0
- package/src/electron/utils/validation.ts +493 -0
- package/src/electron/utils/x-cli.ts +198 -0
- package/src/electron/voice/VoiceService.ts +583 -0
- package/src/electron/voice/index.ts +9 -0
- package/src/electron/voice/voice-settings-manager.ts +403 -0
- package/src/renderer/App.tsx +775 -0
- package/src/renderer/components/ActivityFeed.tsx +407 -0
- package/src/renderer/components/ActivityFeedItem.tsx +285 -0
- package/src/renderer/components/AgentRoleCard.tsx +343 -0
- package/src/renderer/components/AgentRoleEditor.tsx +805 -0
- package/src/renderer/components/AgentSquadSettings.tsx +295 -0
- package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
- package/src/renderer/components/AppearanceSettings.tsx +122 -0
- package/src/renderer/components/ApprovalDialog.tsx +100 -0
- package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
- package/src/renderer/components/CanvasPreview.tsx +1189 -0
- package/src/renderer/components/CommandOutput.tsx +202 -0
- package/src/renderer/components/ContextPolicySettings.tsx +523 -0
- package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
- package/src/renderer/components/DisclaimerModal.tsx +124 -0
- package/src/renderer/components/DiscordSettings.tsx +436 -0
- package/src/renderer/components/EmailSettings.tsx +606 -0
- package/src/renderer/components/ExtensionsSettings.tsx +542 -0
- package/src/renderer/components/FileViewer.tsx +224 -0
- package/src/renderer/components/GoogleChatSettings.tsx +535 -0
- package/src/renderer/components/GuardrailSettings.tsx +487 -0
- package/src/renderer/components/HooksSettings.tsx +581 -0
- package/src/renderer/components/ImessageSettings.tsx +484 -0
- package/src/renderer/components/LineSettings.tsx +483 -0
- package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
- package/src/renderer/components/MCPSettings.tsx +943 -0
- package/src/renderer/components/MainContent.tsx +2433 -0
- package/src/renderer/components/MatrixSettings.tsx +510 -0
- package/src/renderer/components/MattermostSettings.tsx +473 -0
- package/src/renderer/components/MemorySettings.tsx +247 -0
- package/src/renderer/components/MentionBadge.tsx +87 -0
- package/src/renderer/components/MentionInput.tsx +409 -0
- package/src/renderer/components/MentionList.tsx +476 -0
- package/src/renderer/components/MissionControlPanel.tsx +1995 -0
- package/src/renderer/components/NodesSettings.tsx +316 -0
- package/src/renderer/components/NotificationPanel.tsx +481 -0
- package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
- package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
- package/src/renderer/components/Onboarding/index.ts +3 -0
- package/src/renderer/components/OnboardingModal.tsx +698 -0
- package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
- package/src/renderer/components/PersonalitySettings.tsx +597 -0
- package/src/renderer/components/QueueSettings.tsx +119 -0
- package/src/renderer/components/QuickTaskFAB.tsx +71 -0
- package/src/renderer/components/RightPanel.tsx +413 -0
- package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
- package/src/renderer/components/SearchSettings.tsx +328 -0
- package/src/renderer/components/Settings.tsx +1504 -0
- package/src/renderer/components/Sidebar.tsx +344 -0
- package/src/renderer/components/SignalSettings.tsx +673 -0
- package/src/renderer/components/SkillHubBrowser.tsx +458 -0
- package/src/renderer/components/SkillParameterModal.tsx +185 -0
- package/src/renderer/components/SkillsSettings.tsx +451 -0
- package/src/renderer/components/SlackSettings.tsx +442 -0
- package/src/renderer/components/StandupReportViewer.tsx +614 -0
- package/src/renderer/components/TaskBoard.tsx +498 -0
- package/src/renderer/components/TaskBoardCard.tsx +357 -0
- package/src/renderer/components/TaskBoardColumn.tsx +211 -0
- package/src/renderer/components/TaskLabelManager.tsx +472 -0
- package/src/renderer/components/TaskQueuePanel.tsx +144 -0
- package/src/renderer/components/TaskQuickActions.tsx +492 -0
- package/src/renderer/components/TaskTimeline.tsx +216 -0
- package/src/renderer/components/TaskView.tsx +162 -0
- package/src/renderer/components/TeamsSettings.tsx +518 -0
- package/src/renderer/components/TelegramSettings.tsx +421 -0
- package/src/renderer/components/Toast.tsx +76 -0
- package/src/renderer/components/TraySettings.tsx +189 -0
- package/src/renderer/components/TwitchSettings.tsx +511 -0
- package/src/renderer/components/UpdateSettings.tsx +295 -0
- package/src/renderer/components/VoiceIndicator.tsx +270 -0
- package/src/renderer/components/VoiceSettings.tsx +867 -0
- package/src/renderer/components/WhatsAppSettings.tsx +721 -0
- package/src/renderer/components/WorkingStateEditor.tsx +309 -0
- package/src/renderer/components/WorkingStateHistory.tsx +481 -0
- package/src/renderer/components/WorkspaceSelector.tsx +150 -0
- package/src/renderer/components/XSettings.tsx +311 -0
- package/src/renderer/global.d.ts +9 -0
- package/src/renderer/hooks/useAgentContext.ts +153 -0
- package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
- package/src/renderer/hooks/useVoiceInput.ts +268 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +10 -0
- package/src/renderer/public/cowork-os-logo.png +0 -0
- package/src/renderer/quick-input.html +164 -0
- package/src/renderer/styles/index.css +14504 -0
- package/src/renderer/utils/agentMessages.ts +749 -0
- package/src/renderer/utils/voice-directives.ts +169 -0
- package/src/shared/channelMessages.ts +213 -0
- package/src/shared/types.ts +3608 -0
- package/tsconfig.electron.json +26 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +23 -0
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord Channel Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the ChannelAdapter interface using discord.js for Discord Bot API.
|
|
5
|
+
* Supports slash commands, direct messages, button components, embeds, and threads.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Client,
|
|
10
|
+
GatewayIntentBits,
|
|
11
|
+
Partials,
|
|
12
|
+
Events,
|
|
13
|
+
Message,
|
|
14
|
+
REST,
|
|
15
|
+
Routes,
|
|
16
|
+
SlashCommandBuilder,
|
|
17
|
+
AttachmentBuilder,
|
|
18
|
+
ChatInputCommandInteraction,
|
|
19
|
+
TextChannel,
|
|
20
|
+
DMChannel,
|
|
21
|
+
ThreadChannel,
|
|
22
|
+
ChannelType as DiscordChannelType,
|
|
23
|
+
ActionRowBuilder,
|
|
24
|
+
ButtonBuilder,
|
|
25
|
+
ButtonStyle,
|
|
26
|
+
ButtonInteraction,
|
|
27
|
+
EmbedBuilder,
|
|
28
|
+
ColorResolvable,
|
|
29
|
+
StringSelectMenuBuilder,
|
|
30
|
+
StringSelectMenuInteraction,
|
|
31
|
+
} from 'discord.js';
|
|
32
|
+
import * as fs from 'fs';
|
|
33
|
+
import * as path from 'path';
|
|
34
|
+
import {
|
|
35
|
+
ChannelAdapter,
|
|
36
|
+
ChannelStatus,
|
|
37
|
+
IncomingMessage,
|
|
38
|
+
OutgoingMessage,
|
|
39
|
+
MessageHandler,
|
|
40
|
+
ErrorHandler,
|
|
41
|
+
StatusHandler,
|
|
42
|
+
ChannelInfo,
|
|
43
|
+
DiscordConfig,
|
|
44
|
+
CallbackQuery,
|
|
45
|
+
CallbackQueryHandler,
|
|
46
|
+
InlineKeyboardButton,
|
|
47
|
+
Poll,
|
|
48
|
+
SelectMenu,
|
|
49
|
+
SelectMenuHandler,
|
|
50
|
+
} from './types';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Embed color constants for different message types
|
|
54
|
+
*/
|
|
55
|
+
const EMBED_COLORS = {
|
|
56
|
+
pending: 0xffa500 as ColorResolvable, // Orange
|
|
57
|
+
success: 0x57f287 as ColorResolvable, // Green
|
|
58
|
+
error: 0xed4245 as ColorResolvable, // Red
|
|
59
|
+
info: 0x5865f2 as ColorResolvable, // Blue (Discord blurple)
|
|
60
|
+
warning: 0xfee75c as ColorResolvable, // Yellow
|
|
61
|
+
neutral: 0x99aab5 as ColorResolvable, // Gray
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
export class DiscordAdapter implements ChannelAdapter {
|
|
65
|
+
readonly type = 'discord' as const;
|
|
66
|
+
|
|
67
|
+
private client: Client | null = null;
|
|
68
|
+
private _status: ChannelStatus = 'disconnected';
|
|
69
|
+
private _botUsername?: string;
|
|
70
|
+
private _botId?: string;
|
|
71
|
+
private messageHandlers: MessageHandler[] = [];
|
|
72
|
+
private errorHandlers: ErrorHandler[] = [];
|
|
73
|
+
private statusHandlers: StatusHandler[] = [];
|
|
74
|
+
private callbackQueryHandlers: CallbackQueryHandler[] = [];
|
|
75
|
+
private selectMenuHandlers: SelectMenuHandler[] = [];
|
|
76
|
+
private config: DiscordConfig;
|
|
77
|
+
|
|
78
|
+
// Track pending interactions that need reply (chatId -> interaction)
|
|
79
|
+
private pendingInteractions: Map<string, ChatInputCommandInteraction> = new Map();
|
|
80
|
+
|
|
81
|
+
// Track thread starters for context (threadId -> starter info)
|
|
82
|
+
private threadStarterCache: Map<string, { authorId: string; authorName: string; content: string }> = new Map();
|
|
83
|
+
|
|
84
|
+
constructor(config: DiscordConfig) {
|
|
85
|
+
this.config = config;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get status(): ChannelStatus {
|
|
89
|
+
return this._status;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get botUsername(): string | undefined {
|
|
93
|
+
return this._botUsername;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Connect to Discord
|
|
98
|
+
*/
|
|
99
|
+
async connect(): Promise<void> {
|
|
100
|
+
if (this._status === 'connected' || this._status === 'connecting') {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.setStatus('connecting');
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Create client instance with required intents and partials
|
|
108
|
+
// Partials.Channel is required to receive DM messages
|
|
109
|
+
this.client = new Client({
|
|
110
|
+
intents: [
|
|
111
|
+
GatewayIntentBits.Guilds,
|
|
112
|
+
GatewayIntentBits.GuildMessages,
|
|
113
|
+
GatewayIntentBits.DirectMessages,
|
|
114
|
+
GatewayIntentBits.MessageContent,
|
|
115
|
+
],
|
|
116
|
+
partials: [
|
|
117
|
+
Partials.Channel, // Required to receive DMs
|
|
118
|
+
Partials.Message, // Required for uncached message events
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Set up event handlers
|
|
123
|
+
this.client.once(Events.ClientReady, async (client) => {
|
|
124
|
+
this._botUsername = client.user.username;
|
|
125
|
+
this._botId = client.user.id;
|
|
126
|
+
console.log(`Discord bot @${this._botUsername} is ready`);
|
|
127
|
+
|
|
128
|
+
// Register slash commands
|
|
129
|
+
await this.registerSlashCommands();
|
|
130
|
+
|
|
131
|
+
this.setStatus('connected');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Handle regular messages (for conversations)
|
|
135
|
+
this.client.on(Events.MessageCreate, async (message) => {
|
|
136
|
+
// Ignore bot messages
|
|
137
|
+
if (message.author.bot) return;
|
|
138
|
+
|
|
139
|
+
// Handle DMs and mentions in guilds
|
|
140
|
+
const isDM = message.channel.type === DiscordChannelType.DM;
|
|
141
|
+
const isMentioned = message.mentions.has(this.client!.user!);
|
|
142
|
+
const isThread = message.channel.isThread();
|
|
143
|
+
|
|
144
|
+
console.log(`Discord message received: isDM=${isDM}, isMentioned=${isMentioned}, isThread=${isThread}, content="${message.content.slice(0, 50)}"`);
|
|
145
|
+
|
|
146
|
+
if (isDM || isMentioned) {
|
|
147
|
+
const incomingMessage = this.mapMessageToIncoming(message);
|
|
148
|
+
console.log(`Processing Discord message from ${message.author.username}: ${incomingMessage.text.slice(0, 50)}`);
|
|
149
|
+
await this.handleIncomingMessage(incomingMessage);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Handle slash command, button, and select menu interactions
|
|
154
|
+
this.client.on(Events.InteractionCreate, async (interaction) => {
|
|
155
|
+
// Handle button interactions
|
|
156
|
+
if (interaction.isButton()) {
|
|
157
|
+
await this.handleButtonInteraction(interaction);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle select menu interactions
|
|
162
|
+
if (interaction.isStringSelectMenu()) {
|
|
163
|
+
await this.handleSelectMenuInteraction(interaction);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!interaction.isChatInputCommand()) return;
|
|
168
|
+
|
|
169
|
+
// Defer the reply FIRST to avoid interaction timeout (Discord requires response within 3 seconds)
|
|
170
|
+
try {
|
|
171
|
+
await interaction.deferReply();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Failed to defer reply:', error);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Store the interaction so sendMessage can use editReply for the first response
|
|
178
|
+
if (interaction.channelId) {
|
|
179
|
+
this.pendingInteractions.set(interaction.channelId, interaction);
|
|
180
|
+
|
|
181
|
+
// Auto-clear after 14 minutes (interactions expire after 15 minutes)
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
this.pendingInteractions.delete(interaction.channelId!);
|
|
184
|
+
}, 14 * 60 * 1000);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Convert slash command to message format
|
|
188
|
+
const incomingMessage = this.mapInteractionToIncoming(interaction);
|
|
189
|
+
await this.handleIncomingMessage(incomingMessage);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Handle errors
|
|
193
|
+
this.client.on(Events.Error, (error) => {
|
|
194
|
+
console.error('Discord client error:', error);
|
|
195
|
+
this.handleError(error, 'client.error');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Login
|
|
199
|
+
await this.client.login(this.config.botToken);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
202
|
+
this.setStatus('error', err);
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Handle button interaction (callback query equivalent)
|
|
209
|
+
*/
|
|
210
|
+
private async handleButtonInteraction(interaction: ButtonInteraction): Promise<void> {
|
|
211
|
+
const customId = interaction.customId;
|
|
212
|
+
|
|
213
|
+
// Create callback query object matching our interface
|
|
214
|
+
const callbackQuery: CallbackQuery = {
|
|
215
|
+
id: interaction.id,
|
|
216
|
+
userId: interaction.user.id,
|
|
217
|
+
userName: interaction.user.displayName || interaction.user.username,
|
|
218
|
+
chatId: interaction.channelId!,
|
|
219
|
+
messageId: interaction.message.id,
|
|
220
|
+
data: customId,
|
|
221
|
+
threadId: interaction.channel?.isThread() ? interaction.channelId! : undefined,
|
|
222
|
+
raw: interaction,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Notify all registered handlers
|
|
226
|
+
for (const handler of this.callbackQueryHandlers) {
|
|
227
|
+
try {
|
|
228
|
+
await handler(callbackQuery);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('Error in callback query handler:', error);
|
|
231
|
+
this.handleError(
|
|
232
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
233
|
+
'callbackQueryHandler'
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Register slash commands with Discord
|
|
241
|
+
*/
|
|
242
|
+
private async registerSlashCommands(): Promise<void> {
|
|
243
|
+
if (!this.client?.user) return;
|
|
244
|
+
|
|
245
|
+
const commands = [
|
|
246
|
+
new SlashCommandBuilder()
|
|
247
|
+
.setName('start')
|
|
248
|
+
.setDescription('Start the bot and get help'),
|
|
249
|
+
new SlashCommandBuilder()
|
|
250
|
+
.setName('help')
|
|
251
|
+
.setDescription('Show available commands'),
|
|
252
|
+
new SlashCommandBuilder()
|
|
253
|
+
.setName('workspaces')
|
|
254
|
+
.setDescription('List available workspaces'),
|
|
255
|
+
new SlashCommandBuilder()
|
|
256
|
+
.setName('workspace')
|
|
257
|
+
.setDescription('Select or show current workspace')
|
|
258
|
+
.addStringOption(option =>
|
|
259
|
+
option.setName('path')
|
|
260
|
+
.setDescription('Workspace path to select')
|
|
261
|
+
.setRequired(false)),
|
|
262
|
+
new SlashCommandBuilder()
|
|
263
|
+
.setName('addworkspace')
|
|
264
|
+
.setDescription('Add a new workspace by path')
|
|
265
|
+
.addStringOption(option =>
|
|
266
|
+
option.setName('path')
|
|
267
|
+
.setDescription('Path to the workspace folder')
|
|
268
|
+
.setRequired(true)),
|
|
269
|
+
new SlashCommandBuilder()
|
|
270
|
+
.setName('newtask')
|
|
271
|
+
.setDescription('Start a fresh task/conversation'),
|
|
272
|
+
new SlashCommandBuilder()
|
|
273
|
+
.setName('provider')
|
|
274
|
+
.setDescription('Change or show current LLM provider')
|
|
275
|
+
.addStringOption(option =>
|
|
276
|
+
option.setName('name')
|
|
277
|
+
.setDescription('Provider name (anthropic, gemini, openrouter, bedrock, ollama)')
|
|
278
|
+
.setRequired(false)),
|
|
279
|
+
new SlashCommandBuilder()
|
|
280
|
+
.setName('providers')
|
|
281
|
+
.setDescription('List all available providers'),
|
|
282
|
+
new SlashCommandBuilder()
|
|
283
|
+
.setName('models')
|
|
284
|
+
.setDescription('List available AI models'),
|
|
285
|
+
new SlashCommandBuilder()
|
|
286
|
+
.setName('model')
|
|
287
|
+
.setDescription('Change or show current model')
|
|
288
|
+
.addStringOption(option =>
|
|
289
|
+
option.setName('name')
|
|
290
|
+
.setDescription('Model name to use')
|
|
291
|
+
.setRequired(false)),
|
|
292
|
+
new SlashCommandBuilder()
|
|
293
|
+
.setName('status')
|
|
294
|
+
.setDescription('Check bot status'),
|
|
295
|
+
new SlashCommandBuilder()
|
|
296
|
+
.setName('cancel')
|
|
297
|
+
.setDescription('Cancel current task'),
|
|
298
|
+
new SlashCommandBuilder()
|
|
299
|
+
.setName('task')
|
|
300
|
+
.setDescription('Run a task')
|
|
301
|
+
.addStringOption(option =>
|
|
302
|
+
option.setName('prompt')
|
|
303
|
+
.setDescription('Task description')
|
|
304
|
+
.setRequired(true)),
|
|
305
|
+
new SlashCommandBuilder()
|
|
306
|
+
.setName('pair')
|
|
307
|
+
.setDescription('Pair with a pairing code to gain access')
|
|
308
|
+
.addStringOption(option =>
|
|
309
|
+
option.setName('code')
|
|
310
|
+
.setDescription('The pairing code from CoWork OS app')
|
|
311
|
+
.setRequired(true)),
|
|
312
|
+
new SlashCommandBuilder()
|
|
313
|
+
.setName('approve')
|
|
314
|
+
.setDescription('Approve the pending action'),
|
|
315
|
+
new SlashCommandBuilder()
|
|
316
|
+
.setName('deny')
|
|
317
|
+
.setDescription('Deny the pending action'),
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const rest = new REST().setToken(this.config.botToken);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
console.log('Registering Discord slash commands...');
|
|
324
|
+
|
|
325
|
+
// Register commands globally or to specific guilds
|
|
326
|
+
if (this.config.guildIds && this.config.guildIds.length > 0) {
|
|
327
|
+
// Register to specific guilds (faster for development)
|
|
328
|
+
for (const guildId of this.config.guildIds) {
|
|
329
|
+
await rest.put(
|
|
330
|
+
Routes.applicationGuildCommands(this.config.applicationId, guildId),
|
|
331
|
+
{ body: commands.map(c => c.toJSON()) }
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
// Register globally (takes up to 1 hour to propagate)
|
|
336
|
+
await rest.put(
|
|
337
|
+
Routes.applicationCommands(this.config.applicationId),
|
|
338
|
+
{ body: commands.map(c => c.toJSON()) }
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log('Discord slash commands registered');
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('Failed to register Discord slash commands:', error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Disconnect from Discord
|
|
350
|
+
*/
|
|
351
|
+
async disconnect(): Promise<void> {
|
|
352
|
+
if (this.client) {
|
|
353
|
+
this.client.destroy();
|
|
354
|
+
this.client = null;
|
|
355
|
+
}
|
|
356
|
+
this._botUsername = undefined;
|
|
357
|
+
this._botId = undefined;
|
|
358
|
+
this.setStatus('disconnected');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Send a message to a Discord channel
|
|
363
|
+
*/
|
|
364
|
+
async sendMessage(message: OutgoingMessage): Promise<string> {
|
|
365
|
+
if (!this.client || this._status !== 'connected') {
|
|
366
|
+
throw new Error('Discord bot is not connected');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Process text for Discord compatibility
|
|
370
|
+
let processedText = message.text;
|
|
371
|
+
if (message.parseMode === 'markdown') {
|
|
372
|
+
processedText = this.convertMarkdownForDiscord(message.text);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Build button components if inline keyboard is provided
|
|
376
|
+
const components = message.inlineKeyboard && message.inlineKeyboard.length > 0
|
|
377
|
+
? this.buildButtonComponents(message.inlineKeyboard)
|
|
378
|
+
: [];
|
|
379
|
+
|
|
380
|
+
// Use smart chunking that preserves code fences
|
|
381
|
+
const chunks = this.splitMessageSmart(processedText, 2000);
|
|
382
|
+
let lastMessageId = '';
|
|
383
|
+
|
|
384
|
+
// Check if there's a pending interaction for this chat that needs reply
|
|
385
|
+
const pendingInteraction = this.pendingInteractions.get(message.chatId);
|
|
386
|
+
|
|
387
|
+
// Determine target channel (could be a thread)
|
|
388
|
+
let targetChannelId = message.chatId;
|
|
389
|
+
if (message.threadId) {
|
|
390
|
+
targetChannelId = message.threadId;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
395
|
+
const chunk = chunks[i];
|
|
396
|
+
const isLastChunk = i === chunks.length - 1;
|
|
397
|
+
|
|
398
|
+
// Only add buttons to the last chunk
|
|
399
|
+
const chunkComponents = isLastChunk ? components : [];
|
|
400
|
+
|
|
401
|
+
// First chunk: use interaction reply if available
|
|
402
|
+
if (i === 0 && pendingInteraction) {
|
|
403
|
+
try {
|
|
404
|
+
const reply = await pendingInteraction.editReply({
|
|
405
|
+
content: chunk,
|
|
406
|
+
components: chunkComponents,
|
|
407
|
+
});
|
|
408
|
+
lastMessageId = typeof reply === 'object' && 'id' in reply ? reply.id : pendingInteraction.id;
|
|
409
|
+
// Clear the pending interaction after first reply
|
|
410
|
+
this.pendingInteractions.delete(message.chatId);
|
|
411
|
+
continue;
|
|
412
|
+
} catch (interactionError) {
|
|
413
|
+
// Interaction may have expired, fall back to channel.send
|
|
414
|
+
console.warn('Interaction reply failed, falling back to channel.send:', interactionError);
|
|
415
|
+
this.pendingInteractions.delete(message.chatId);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Regular channel message
|
|
420
|
+
const channel = await this.client.channels.fetch(targetChannelId);
|
|
421
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
422
|
+
throw new Error('Invalid channel or channel is not text-based');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const sent = await (channel as TextChannel | DMChannel | ThreadChannel).send({
|
|
426
|
+
content: chunk,
|
|
427
|
+
components: chunkComponents,
|
|
428
|
+
reply: message.replyTo && i === 0 ? { messageReference: message.replyTo } : undefined,
|
|
429
|
+
});
|
|
430
|
+
lastMessageId = sent.id;
|
|
431
|
+
}
|
|
432
|
+
} catch (error: unknown) {
|
|
433
|
+
// If markdown parsing fails, retry without formatting
|
|
434
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
435
|
+
if (errorMessage.includes('parse') || errorMessage.includes('format')) {
|
|
436
|
+
console.log('Markdown parsing failed, retrying without formatting');
|
|
437
|
+
return this.sendMessagePlain(targetChannelId, message.text, message.replyTo, components);
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return lastMessageId;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Send a message with an embed (rich format)
|
|
447
|
+
*/
|
|
448
|
+
async sendEmbed(
|
|
449
|
+
chatId: string,
|
|
450
|
+
options: {
|
|
451
|
+
title?: string;
|
|
452
|
+
description?: string;
|
|
453
|
+
color?: keyof typeof EMBED_COLORS;
|
|
454
|
+
fields?: Array<{ name: string; value: string; inline?: boolean }>;
|
|
455
|
+
footer?: string;
|
|
456
|
+
timestamp?: boolean;
|
|
457
|
+
},
|
|
458
|
+
buttons?: InlineKeyboardButton[][]
|
|
459
|
+
): Promise<string> {
|
|
460
|
+
if (!this.client || this._status !== 'connected') {
|
|
461
|
+
throw new Error('Discord bot is not connected');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const embed = new EmbedBuilder();
|
|
465
|
+
|
|
466
|
+
if (options.title) embed.setTitle(options.title);
|
|
467
|
+
if (options.description) embed.setDescription(options.description);
|
|
468
|
+
if (options.color) embed.setColor(EMBED_COLORS[options.color]);
|
|
469
|
+
if (options.fields) {
|
|
470
|
+
for (const field of options.fields) {
|
|
471
|
+
embed.addFields({ name: field.name, value: field.value, inline: field.inline });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (options.footer) embed.setFooter({ text: options.footer });
|
|
475
|
+
if (options.timestamp) embed.setTimestamp();
|
|
476
|
+
|
|
477
|
+
const components = buttons ? this.buildButtonComponents(buttons) : [];
|
|
478
|
+
|
|
479
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
480
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
481
|
+
throw new Error('Invalid channel');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const sent = await (channel as TextChannel | DMChannel | ThreadChannel).send({
|
|
485
|
+
embeds: [embed],
|
|
486
|
+
components,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return sent.id;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Build Discord button components from our button format
|
|
494
|
+
*/
|
|
495
|
+
private buildButtonComponents(buttons: InlineKeyboardButton[][]): ActionRowBuilder<ButtonBuilder>[] {
|
|
496
|
+
const rows: ActionRowBuilder<ButtonBuilder>[] = [];
|
|
497
|
+
|
|
498
|
+
for (const rowButtons of buttons) {
|
|
499
|
+
if (rowButtons.length === 0) continue;
|
|
500
|
+
|
|
501
|
+
const row = new ActionRowBuilder<ButtonBuilder>();
|
|
502
|
+
let buttonCount = 0;
|
|
503
|
+
|
|
504
|
+
for (const button of rowButtons) {
|
|
505
|
+
if (buttonCount >= 5) break; // Discord max 5 buttons per row
|
|
506
|
+
|
|
507
|
+
const discordButton = new ButtonBuilder()
|
|
508
|
+
.setLabel(button.text.substring(0, 80)); // Discord max 80 chars
|
|
509
|
+
|
|
510
|
+
if (button.url) {
|
|
511
|
+
discordButton.setStyle(ButtonStyle.Link);
|
|
512
|
+
discordButton.setURL(button.url);
|
|
513
|
+
} else if (button.callbackData) {
|
|
514
|
+
// Determine button style based on callback data
|
|
515
|
+
if (button.callbackData.startsWith('approve')) {
|
|
516
|
+
discordButton.setStyle(ButtonStyle.Success);
|
|
517
|
+
} else if (button.callbackData.startsWith('deny')) {
|
|
518
|
+
discordButton.setStyle(ButtonStyle.Danger);
|
|
519
|
+
} else {
|
|
520
|
+
discordButton.setStyle(ButtonStyle.Primary);
|
|
521
|
+
}
|
|
522
|
+
discordButton.setCustomId(button.callbackData.substring(0, 100)); // Discord max 100 chars
|
|
523
|
+
} else {
|
|
524
|
+
continue; // Skip buttons without action
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
row.addComponents(discordButton);
|
|
528
|
+
buttonCount++;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (buttonCount > 0) {
|
|
532
|
+
rows.push(row);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (rows.length >= 5) break; // Discord max 5 rows
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return rows;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Send a plain text message without formatting
|
|
543
|
+
*/
|
|
544
|
+
private async sendMessagePlain(
|
|
545
|
+
chatId: string,
|
|
546
|
+
text: string,
|
|
547
|
+
replyTo?: string,
|
|
548
|
+
components: ActionRowBuilder<ButtonBuilder>[] = []
|
|
549
|
+
): Promise<string> {
|
|
550
|
+
const channel = await this.client!.channels.fetch(chatId);
|
|
551
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
552
|
+
throw new Error('Invalid channel');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const chunks = this.splitMessageSmart(text, 2000);
|
|
556
|
+
let lastMessageId = '';
|
|
557
|
+
|
|
558
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
559
|
+
const isLastChunk = i === chunks.length - 1;
|
|
560
|
+
const sent = await (channel as TextChannel | DMChannel | ThreadChannel).send({
|
|
561
|
+
content: chunks[i],
|
|
562
|
+
components: isLastChunk ? components : [],
|
|
563
|
+
reply: replyTo && i === 0 ? { messageReference: replyTo } : undefined,
|
|
564
|
+
});
|
|
565
|
+
lastMessageId = sent.id;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return lastMessageId;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Convert GitHub-flavored markdown to Discord-compatible format
|
|
573
|
+
*/
|
|
574
|
+
private convertMarkdownForDiscord(text: string): string {
|
|
575
|
+
let result = text;
|
|
576
|
+
|
|
577
|
+
// Convert markdown headers (## Header) to bold (**Header**)
|
|
578
|
+
result = result.replace(/^#{1,6}\s+(.+)$/gm, '**$1**');
|
|
579
|
+
|
|
580
|
+
// Convert horizontal rules (---, ***) to a line
|
|
581
|
+
result = result.replace(/^[-*]{3,}$/gm, '───────────────────');
|
|
582
|
+
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Smart message splitting that preserves code fences
|
|
588
|
+
*/
|
|
589
|
+
private splitMessageSmart(text: string, maxLength: number): string[] {
|
|
590
|
+
if (text.length <= maxLength) {
|
|
591
|
+
return [text];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const chunks: string[] = [];
|
|
595
|
+
let remaining = text;
|
|
596
|
+
let inCodeBlock = false;
|
|
597
|
+
let codeBlockLang = '';
|
|
598
|
+
|
|
599
|
+
while (remaining.length > 0) {
|
|
600
|
+
if (remaining.length <= maxLength) {
|
|
601
|
+
// Close any open code block at the end
|
|
602
|
+
if (inCodeBlock) {
|
|
603
|
+
chunks.push(remaining);
|
|
604
|
+
} else {
|
|
605
|
+
chunks.push(remaining);
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Find the best breaking point
|
|
611
|
+
let breakIndex = this.findBreakPoint(remaining, maxLength, inCodeBlock);
|
|
612
|
+
let chunk = remaining.substring(0, breakIndex);
|
|
613
|
+
|
|
614
|
+
// Check if we're entering or leaving a code block
|
|
615
|
+
const codeBlockMatches = chunk.match(/```(\w*)/g) || [];
|
|
616
|
+
for (const match of codeBlockMatches) {
|
|
617
|
+
if (inCodeBlock) {
|
|
618
|
+
inCodeBlock = false;
|
|
619
|
+
codeBlockLang = '';
|
|
620
|
+
} else {
|
|
621
|
+
inCodeBlock = true;
|
|
622
|
+
codeBlockLang = match.replace('```', '');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// If we're in a code block and the chunk doesn't close it, close it manually
|
|
627
|
+
if (inCodeBlock && !chunk.endsWith('```')) {
|
|
628
|
+
chunk += '\n```';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
chunks.push(chunk);
|
|
632
|
+
remaining = remaining.substring(breakIndex).trimStart();
|
|
633
|
+
|
|
634
|
+
// If we closed a code block, reopen it in the next chunk
|
|
635
|
+
if (inCodeBlock && remaining.length > 0) {
|
|
636
|
+
remaining = '```' + codeBlockLang + '\n' + remaining;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return chunks;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Find the best break point for message splitting
|
|
645
|
+
*/
|
|
646
|
+
private findBreakPoint(text: string, maxLength: number, inCodeBlock: boolean): number {
|
|
647
|
+
// Reserve space for potential code fence closure
|
|
648
|
+
const reservedSpace = inCodeBlock ? 4 : 0;
|
|
649
|
+
const effectiveMax = maxLength - reservedSpace;
|
|
650
|
+
|
|
651
|
+
// Try to break at a newline
|
|
652
|
+
let breakIndex = text.lastIndexOf('\n', effectiveMax);
|
|
653
|
+
if (breakIndex > effectiveMax / 2) {
|
|
654
|
+
return breakIndex + 1;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Try to break at a space
|
|
658
|
+
breakIndex = text.lastIndexOf(' ', effectiveMax);
|
|
659
|
+
if (breakIndex > effectiveMax / 2) {
|
|
660
|
+
return breakIndex + 1;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Force break at max length
|
|
664
|
+
return effectiveMax;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Legacy split method for compatibility
|
|
669
|
+
*/
|
|
670
|
+
private splitMessage(text: string, maxLength: number): string[] {
|
|
671
|
+
return this.splitMessageSmart(text, maxLength);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Edit an existing message
|
|
676
|
+
*/
|
|
677
|
+
async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
|
|
678
|
+
if (!this.client || this._status !== 'connected') {
|
|
679
|
+
throw new Error('Discord bot is not connected');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
683
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
684
|
+
throw new Error('Invalid channel');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const message = await (channel as TextChannel | DMChannel | ThreadChannel).messages.fetch(messageId);
|
|
688
|
+
await message.edit(text);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Delete a message
|
|
693
|
+
*/
|
|
694
|
+
async deleteMessage(chatId: string, messageId: string): Promise<void> {
|
|
695
|
+
if (!this.client || this._status !== 'connected') {
|
|
696
|
+
throw new Error('Discord bot is not connected');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
700
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
701
|
+
throw new Error('Invalid channel');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const message = await (channel as TextChannel | DMChannel | ThreadChannel).messages.fetch(messageId);
|
|
705
|
+
await message.delete();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Send a document/file to a channel
|
|
710
|
+
*/
|
|
711
|
+
async sendDocument(chatId: string, filePath: string, caption?: string): Promise<string> {
|
|
712
|
+
if (!this.client || this._status !== 'connected') {
|
|
713
|
+
throw new Error('Discord bot is not connected');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Check if file exists
|
|
717
|
+
if (!fs.existsSync(filePath)) {
|
|
718
|
+
throw new Error(`File not found: ${filePath}`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
722
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
723
|
+
throw new Error('Invalid channel');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const fileName = path.basename(filePath);
|
|
727
|
+
const attachment = new AttachmentBuilder(filePath, { name: fileName });
|
|
728
|
+
|
|
729
|
+
const sent = await (channel as TextChannel | DMChannel | ThreadChannel).send({
|
|
730
|
+
content: caption,
|
|
731
|
+
files: [attachment],
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
return sent.id;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Register a message handler
|
|
739
|
+
*/
|
|
740
|
+
onMessage(handler: MessageHandler): void {
|
|
741
|
+
this.messageHandlers.push(handler);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Register a callback query handler (for button interactions)
|
|
746
|
+
*/
|
|
747
|
+
onCallbackQuery(handler: CallbackQueryHandler): void {
|
|
748
|
+
this.callbackQueryHandlers.push(handler);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Answer a callback query (acknowledge button press)
|
|
753
|
+
* For Discord, this updates the message or sends an ephemeral response
|
|
754
|
+
*/
|
|
755
|
+
async answerCallbackQuery(queryId: string, text?: string, showAlert?: boolean): Promise<void> {
|
|
756
|
+
// In Discord, we need to use the interaction object stored in the raw field
|
|
757
|
+
// The queryId is the interaction ID, but we need the actual interaction object
|
|
758
|
+
// This is typically handled directly in handleButtonInteraction
|
|
759
|
+
// This method provides API compatibility with Telegram
|
|
760
|
+
console.log(`answerCallbackQuery called: ${queryId}, text: ${text}, showAlert: ${showAlert}`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Edit a message with a new inline keyboard
|
|
765
|
+
*/
|
|
766
|
+
async editMessageWithKeyboard(
|
|
767
|
+
chatId: string,
|
|
768
|
+
messageId: string,
|
|
769
|
+
text?: string,
|
|
770
|
+
inlineKeyboard?: InlineKeyboardButton[][]
|
|
771
|
+
): Promise<void> {
|
|
772
|
+
if (!this.client || this._status !== 'connected') {
|
|
773
|
+
throw new Error('Discord bot is not connected');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
777
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
778
|
+
throw new Error('Invalid channel');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const message = await (channel as TextChannel | DMChannel | ThreadChannel).messages.fetch(messageId);
|
|
782
|
+
const components = inlineKeyboard ? this.buildButtonComponents(inlineKeyboard) : [];
|
|
783
|
+
|
|
784
|
+
await message.edit({
|
|
785
|
+
content: text || message.content,
|
|
786
|
+
components,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ============================================================================
|
|
791
|
+
// Extended Features
|
|
792
|
+
// ============================================================================
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Send typing indicator
|
|
796
|
+
*/
|
|
797
|
+
async sendTyping(chatId: string): Promise<void> {
|
|
798
|
+
if (!this.client || this._status !== 'connected') {
|
|
799
|
+
throw new Error('Discord bot is not connected');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
803
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
804
|
+
throw new Error('Invalid channel');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
await (channel as TextChannel | DMChannel | ThreadChannel).sendTyping();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Add reaction to a message
|
|
812
|
+
*/
|
|
813
|
+
async addReaction(chatId: string, messageId: string, emoji: string): Promise<void> {
|
|
814
|
+
if (!this.client || this._status !== 'connected') {
|
|
815
|
+
throw new Error('Discord bot is not connected');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
819
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
820
|
+
throw new Error('Invalid channel');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const message = await (channel as TextChannel | DMChannel | ThreadChannel).messages.fetch(messageId);
|
|
824
|
+
await message.react(emoji);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Remove reaction from a message
|
|
829
|
+
*/
|
|
830
|
+
async removeReaction(chatId: string, messageId: string, emoji: string): Promise<void> {
|
|
831
|
+
if (!this.client || this._status !== 'connected') {
|
|
832
|
+
throw new Error('Discord bot is not connected');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
836
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
837
|
+
throw new Error('Invalid channel');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const message = await (channel as TextChannel | DMChannel | ThreadChannel).messages.fetch(messageId);
|
|
841
|
+
const reaction = message.reactions.cache.get(emoji);
|
|
842
|
+
if (reaction && this._botId) {
|
|
843
|
+
await reaction.users.remove(this._botId);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Send a poll (Discord native polls)
|
|
849
|
+
*/
|
|
850
|
+
async sendPoll(chatId: string, poll: Poll): Promise<string> {
|
|
851
|
+
if (!this.client || this._status !== 'connected') {
|
|
852
|
+
throw new Error('Discord bot is not connected');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
856
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
857
|
+
throw new Error('Invalid channel');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Discord polls require specific formatting
|
|
861
|
+
const pollData = {
|
|
862
|
+
question: { text: poll.question },
|
|
863
|
+
answers: poll.options.map(opt => ({ text: opt.text })),
|
|
864
|
+
duration: poll.openPeriod ? Math.ceil(poll.openPeriod / 3600) : 24, // Convert seconds to hours
|
|
865
|
+
allow_multiselect: poll.allowsMultipleAnswers ?? false,
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const sent = await (channel as TextChannel | DMChannel | ThreadChannel).send({
|
|
869
|
+
poll: pollData as any,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
return sent.id;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Send a message with a select menu (dropdown)
|
|
877
|
+
*/
|
|
878
|
+
async sendWithSelectMenu(chatId: string, text: string, menu: SelectMenu): Promise<string> {
|
|
879
|
+
if (!this.client || this._status !== 'connected') {
|
|
880
|
+
throw new Error('Discord bot is not connected');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
884
|
+
if (!channel || !this.isTextBasedChannel(channel)) {
|
|
885
|
+
throw new Error('Invalid channel');
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
889
|
+
.setCustomId(menu.customId)
|
|
890
|
+
.setPlaceholder(menu.placeholder || 'Select an option')
|
|
891
|
+
.setMinValues(menu.minValues ?? 1)
|
|
892
|
+
.setMaxValues(menu.maxValues ?? 1)
|
|
893
|
+
.addOptions(
|
|
894
|
+
menu.options.map(opt => ({
|
|
895
|
+
label: opt.label,
|
|
896
|
+
value: opt.value,
|
|
897
|
+
description: opt.description,
|
|
898
|
+
emoji: opt.emoji ? { name: opt.emoji } : undefined,
|
|
899
|
+
default: opt.default,
|
|
900
|
+
}))
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
if (menu.disabled) {
|
|
904
|
+
selectMenu.setDisabled(true);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu);
|
|
908
|
+
|
|
909
|
+
const sent = await (channel as TextChannel | DMChannel | ThreadChannel).send({
|
|
910
|
+
content: text,
|
|
911
|
+
components: [row],
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
return sent.id;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Register a select menu handler
|
|
919
|
+
*/
|
|
920
|
+
onSelectMenu(handler: SelectMenuHandler): void {
|
|
921
|
+
this.selectMenuHandlers.push(handler);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Handle select menu interaction
|
|
926
|
+
*/
|
|
927
|
+
private async handleSelectMenuInteraction(interaction: StringSelectMenuInteraction): Promise<void> {
|
|
928
|
+
const customId = interaction.customId;
|
|
929
|
+
const values = interaction.values;
|
|
930
|
+
|
|
931
|
+
// Acknowledge the interaction
|
|
932
|
+
try {
|
|
933
|
+
await interaction.deferUpdate();
|
|
934
|
+
} catch (error) {
|
|
935
|
+
console.error('Failed to defer select menu update:', error);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Notify all registered handlers
|
|
939
|
+
for (const handler of this.selectMenuHandlers) {
|
|
940
|
+
try {
|
|
941
|
+
await handler(
|
|
942
|
+
customId,
|
|
943
|
+
values,
|
|
944
|
+
interaction.user.id,
|
|
945
|
+
interaction.channelId!,
|
|
946
|
+
interaction.message.id,
|
|
947
|
+
interaction
|
|
948
|
+
);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
console.error('Error in select menu handler:', error);
|
|
951
|
+
this.handleError(
|
|
952
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
953
|
+
'selectMenuHandler'
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ============================================================================
|
|
960
|
+
// Handler Registration
|
|
961
|
+
// ============================================================================
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Register an error handler
|
|
965
|
+
*/
|
|
966
|
+
onError(handler: ErrorHandler): void {
|
|
967
|
+
this.errorHandlers.push(handler);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Register a status change handler
|
|
972
|
+
*/
|
|
973
|
+
onStatusChange(handler: StatusHandler): void {
|
|
974
|
+
this.statusHandlers.push(handler);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Get channel info
|
|
979
|
+
*/
|
|
980
|
+
async getInfo(): Promise<ChannelInfo> {
|
|
981
|
+
return {
|
|
982
|
+
type: 'discord',
|
|
983
|
+
status: this._status,
|
|
984
|
+
botId: this._botId,
|
|
985
|
+
botUsername: this._botUsername,
|
|
986
|
+
botDisplayName: this._botUsername,
|
|
987
|
+
extra: {
|
|
988
|
+
applicationId: this.config.applicationId,
|
|
989
|
+
guildIds: this.config.guildIds,
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Private methods
|
|
995
|
+
|
|
996
|
+
private isTextBasedChannel(channel: unknown): channel is TextChannel | DMChannel | ThreadChannel {
|
|
997
|
+
const ch = channel as { type?: DiscordChannelType };
|
|
998
|
+
return ch.type === DiscordChannelType.GuildText ||
|
|
999
|
+
ch.type === DiscordChannelType.DM ||
|
|
1000
|
+
ch.type === DiscordChannelType.PublicThread ||
|
|
1001
|
+
ch.type === DiscordChannelType.PrivateThread;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private mapMessageToIncoming(message: Message): IncomingMessage {
|
|
1005
|
+
// Remove bot mention from the text if present
|
|
1006
|
+
let text = message.content;
|
|
1007
|
+
if (this._botId) {
|
|
1008
|
+
text = text.replace(new RegExp(`<@!?${this._botId}>\\s*`, 'g'), '').trim();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Map Discord message to command format if it looks like a command
|
|
1012
|
+
const commandText = this.parseCommand(text);
|
|
1013
|
+
|
|
1014
|
+
// Check for thread context
|
|
1015
|
+
const isThread = message.channel.isThread();
|
|
1016
|
+
const threadId = isThread ? message.channelId : undefined;
|
|
1017
|
+
|
|
1018
|
+
return {
|
|
1019
|
+
messageId: message.id,
|
|
1020
|
+
channel: 'discord',
|
|
1021
|
+
userId: message.author.id,
|
|
1022
|
+
userName: message.author.displayName || message.author.username,
|
|
1023
|
+
chatId: isThread ? (message.channel as ThreadChannel).parentId! : message.channelId,
|
|
1024
|
+
text: commandText || text,
|
|
1025
|
+
timestamp: message.createdAt,
|
|
1026
|
+
replyTo: message.reference?.messageId,
|
|
1027
|
+
threadId,
|
|
1028
|
+
isForumTopic: isThread,
|
|
1029
|
+
raw: message,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
private mapInteractionToIncoming(interaction: ChatInputCommandInteraction): IncomingMessage {
|
|
1034
|
+
const commandName = interaction.commandName;
|
|
1035
|
+
let text = `/${commandName}`;
|
|
1036
|
+
|
|
1037
|
+
// Add options to the command text
|
|
1038
|
+
const options = interaction.options;
|
|
1039
|
+
|
|
1040
|
+
// Handle specific commands with their options
|
|
1041
|
+
switch (commandName) {
|
|
1042
|
+
case 'workspace': {
|
|
1043
|
+
const wsPath = options.getString('path');
|
|
1044
|
+
if (wsPath) text += ` ${wsPath}`;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
case 'addworkspace': {
|
|
1048
|
+
const addPath = options.getString('path');
|
|
1049
|
+
if (addPath) text += ` ${addPath}`;
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
case 'provider': {
|
|
1053
|
+
const provider = options.getString('name');
|
|
1054
|
+
if (provider) text += ` ${provider}`;
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
case 'model': {
|
|
1058
|
+
const model = options.getString('name');
|
|
1059
|
+
if (model) text += ` ${model}`;
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
case 'task': {
|
|
1063
|
+
const prompt = options.getString('prompt');
|
|
1064
|
+
if (prompt) text = prompt; // Task prompt becomes the text directly
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case 'pair': {
|
|
1068
|
+
const code = options.getString('code');
|
|
1069
|
+
if (code) text += ` ${code}`;
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Check for thread context
|
|
1075
|
+
const isThread = interaction.channel?.isThread() ?? false;
|
|
1076
|
+
|
|
1077
|
+
return {
|
|
1078
|
+
messageId: interaction.id,
|
|
1079
|
+
channel: 'discord',
|
|
1080
|
+
userId: interaction.user.id,
|
|
1081
|
+
userName: interaction.user.displayName || interaction.user.username,
|
|
1082
|
+
chatId: interaction.channelId!,
|
|
1083
|
+
text,
|
|
1084
|
+
timestamp: new Date(interaction.createdTimestamp),
|
|
1085
|
+
threadId: isThread ? interaction.channelId! : undefined,
|
|
1086
|
+
isForumTopic: isThread,
|
|
1087
|
+
raw: interaction,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Parse text to see if it's a command (starts with /)
|
|
1093
|
+
*/
|
|
1094
|
+
private parseCommand(text: string): string | null {
|
|
1095
|
+
// Check if text starts with a command
|
|
1096
|
+
const commandMatch = text.match(/^\/(\w+)(?:\s+(.*))?$/);
|
|
1097
|
+
if (commandMatch) {
|
|
1098
|
+
return text; // Already in command format
|
|
1099
|
+
}
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
private async handleIncomingMessage(message: IncomingMessage): Promise<void> {
|
|
1104
|
+
for (const handler of this.messageHandlers) {
|
|
1105
|
+
try {
|
|
1106
|
+
await handler(message);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
console.error('Error in message handler:', error);
|
|
1109
|
+
this.handleError(
|
|
1110
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1111
|
+
'messageHandler'
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
private handleError(error: Error, context?: string): void {
|
|
1118
|
+
for (const handler of this.errorHandlers) {
|
|
1119
|
+
try {
|
|
1120
|
+
handler(error, context);
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
console.error('Error in error handler:', e);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private setStatus(status: ChannelStatus, error?: Error): void {
|
|
1128
|
+
this._status = status;
|
|
1129
|
+
for (const handler of this.statusHandlers) {
|
|
1130
|
+
try {
|
|
1131
|
+
handler(status, error);
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
console.error('Error in status handler:', e);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Create a Discord adapter from configuration
|
|
1141
|
+
*/
|
|
1142
|
+
export function createDiscordAdapter(config: DiscordConfig): DiscordAdapter {
|
|
1143
|
+
if (!config.botToken) {
|
|
1144
|
+
throw new Error('Discord bot token is required');
|
|
1145
|
+
}
|
|
1146
|
+
if (!config.applicationId) {
|
|
1147
|
+
throw new Error('Discord application ID is required');
|
|
1148
|
+
}
|
|
1149
|
+
return new DiscordAdapter(config);
|
|
1150
|
+
}
|