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,3206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Router
|
|
3
|
+
*
|
|
4
|
+
* Routes incoming messages from channels to appropriate handlers.
|
|
5
|
+
* Manages message flow: Security → Session → Task/Response
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BrowserWindow } from 'electron';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import {
|
|
12
|
+
ChannelAdapter,
|
|
13
|
+
IncomingMessage,
|
|
14
|
+
OutgoingMessage,
|
|
15
|
+
ChannelType,
|
|
16
|
+
GatewayEvent,
|
|
17
|
+
GatewayEventHandler,
|
|
18
|
+
CallbackQuery,
|
|
19
|
+
InlineKeyboardButton,
|
|
20
|
+
} from './channels/types';
|
|
21
|
+
import { TelegramAdapter } from './channels/telegram';
|
|
22
|
+
import { SecurityManager } from './security';
|
|
23
|
+
import { SessionManager } from './session';
|
|
24
|
+
import {
|
|
25
|
+
ChannelRepository,
|
|
26
|
+
ChannelUserRepository,
|
|
27
|
+
ChannelSessionRepository,
|
|
28
|
+
ChannelMessageRepository,
|
|
29
|
+
WorkspaceRepository,
|
|
30
|
+
TaskRepository,
|
|
31
|
+
ArtifactRepository,
|
|
32
|
+
} from '../database/repositories';
|
|
33
|
+
import Database from 'better-sqlite3';
|
|
34
|
+
import { AgentDaemon } from '../agent/daemon';
|
|
35
|
+
import { Task, IPC_CHANNELS, TEMP_WORKSPACE_ID, TEMP_WORKSPACE_NAME, Workspace } from '../../shared/types';
|
|
36
|
+
import * as os from 'os';
|
|
37
|
+
import { LLMProviderFactory, LLMSettings } from '../agent/llm/provider-factory';
|
|
38
|
+
import { ModelKey, LLMProviderType } from '../agent/llm/types';
|
|
39
|
+
import { getCustomSkillLoader } from '../agent/custom-skill-loader';
|
|
40
|
+
import { app } from 'electron';
|
|
41
|
+
import { getVoiceService } from '../voice/VoiceService';
|
|
42
|
+
import { PersonalityManager } from '../settings/personality-manager';
|
|
43
|
+
import {
|
|
44
|
+
getChannelMessage,
|
|
45
|
+
getCompletionMessage,
|
|
46
|
+
DEFAULT_CHANNEL_CONTEXT,
|
|
47
|
+
type ChannelMessageContext,
|
|
48
|
+
} from '../../shared/channelMessages';
|
|
49
|
+
import { DEFAULT_QUIRKS } from '../../shared/types';
|
|
50
|
+
|
|
51
|
+
export interface RouterConfig {
|
|
52
|
+
/** Default workspace ID to use for new sessions */
|
|
53
|
+
defaultWorkspaceId?: string;
|
|
54
|
+
/** Welcome message for new users */
|
|
55
|
+
welcomeMessage?: string;
|
|
56
|
+
/** Message shown when user is not authorized */
|
|
57
|
+
unauthorizedMessage?: string;
|
|
58
|
+
/** Message shown when pairing is required */
|
|
59
|
+
pairingRequiredMessage?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEFAULT_CONFIG: RouterConfig = {
|
|
63
|
+
welcomeMessage: '👋 Welcome to CoWork! I can help you with tasks in your workspace.',
|
|
64
|
+
unauthorizedMessage: '⚠️ You are not authorized to use this bot. Please contact the administrator.',
|
|
65
|
+
pairingRequiredMessage: '🔐 Please enter your pairing code to get started.',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export class MessageRouter {
|
|
69
|
+
private adapters: Map<ChannelType, ChannelAdapter> = new Map();
|
|
70
|
+
private securityManager: SecurityManager;
|
|
71
|
+
private sessionManager: SessionManager;
|
|
72
|
+
private config: RouterConfig;
|
|
73
|
+
private eventHandlers: GatewayEventHandler[] = [];
|
|
74
|
+
private mainWindow: BrowserWindow | null = null;
|
|
75
|
+
private agentDaemon?: AgentDaemon;
|
|
76
|
+
private db: Database.Database;
|
|
77
|
+
|
|
78
|
+
// Repositories
|
|
79
|
+
private channelRepo: ChannelRepository;
|
|
80
|
+
private userRepo: ChannelUserRepository;
|
|
81
|
+
private sessionRepo: ChannelSessionRepository;
|
|
82
|
+
private messageRepo: ChannelMessageRepository;
|
|
83
|
+
private workspaceRepo: WorkspaceRepository;
|
|
84
|
+
private taskRepo: TaskRepository;
|
|
85
|
+
private artifactRepo: ArtifactRepository;
|
|
86
|
+
|
|
87
|
+
// Track pending responses for tasks
|
|
88
|
+
private pendingTaskResponses: Map<string, {
|
|
89
|
+
adapter: ChannelAdapter;
|
|
90
|
+
chatId: string;
|
|
91
|
+
sessionId: string;
|
|
92
|
+
originalMessageId?: string; // For reaction updates
|
|
93
|
+
}> = new Map();
|
|
94
|
+
|
|
95
|
+
// Track pending approval requests for Discord/Telegram
|
|
96
|
+
private pendingApprovals: Map<string, { taskId: string; approval: any; sessionId: string }> = new Map();
|
|
97
|
+
|
|
98
|
+
constructor(db: Database.Database, config: RouterConfig = {}, agentDaemon?: AgentDaemon) {
|
|
99
|
+
this.db = db;
|
|
100
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
101
|
+
this.agentDaemon = agentDaemon;
|
|
102
|
+
|
|
103
|
+
// Initialize repositories
|
|
104
|
+
this.channelRepo = new ChannelRepository(db);
|
|
105
|
+
this.userRepo = new ChannelUserRepository(db);
|
|
106
|
+
this.sessionRepo = new ChannelSessionRepository(db);
|
|
107
|
+
this.messageRepo = new ChannelMessageRepository(db);
|
|
108
|
+
this.workspaceRepo = new WorkspaceRepository(db);
|
|
109
|
+
this.taskRepo = new TaskRepository(db);
|
|
110
|
+
this.artifactRepo = new ArtifactRepository(db);
|
|
111
|
+
|
|
112
|
+
// Initialize managers
|
|
113
|
+
this.securityManager = new SecurityManager(db);
|
|
114
|
+
this.sessionManager = new SessionManager(db);
|
|
115
|
+
|
|
116
|
+
// Listen for task events if agent daemon is available
|
|
117
|
+
if (this.agentDaemon) {
|
|
118
|
+
this.setupTaskEventListener();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Set up listener for task events to send responses back to channels
|
|
124
|
+
*/
|
|
125
|
+
private setupTaskEventListener(): void {
|
|
126
|
+
// We'll listen for task events through BrowserWindow IPC
|
|
127
|
+
// The agent daemon emits events to all windows
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Set the main window for sending IPC events
|
|
132
|
+
*/
|
|
133
|
+
setMainWindow(window: BrowserWindow): void {
|
|
134
|
+
this.mainWindow = window;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the main window for sending IPC events
|
|
139
|
+
*/
|
|
140
|
+
getMainWindow(): BrowserWindow | null {
|
|
141
|
+
return this.mainWindow;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the channel message context from personality settings
|
|
146
|
+
*/
|
|
147
|
+
private getMessageContext(): ChannelMessageContext {
|
|
148
|
+
try {
|
|
149
|
+
if (PersonalityManager.isInitialized()) {
|
|
150
|
+
const settings = PersonalityManager.loadSettings();
|
|
151
|
+
return {
|
|
152
|
+
agentName: settings.agentName || 'CoWork',
|
|
153
|
+
userName: settings.relationship?.userName,
|
|
154
|
+
personality: settings.activePersonality || 'professional',
|
|
155
|
+
emojiUsage: settings.responseStyle?.emojiUsage || 'minimal',
|
|
156
|
+
quirks: settings.quirks || DEFAULT_QUIRKS,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('[MessageRouter] Failed to load personality settings:', error);
|
|
161
|
+
}
|
|
162
|
+
return DEFAULT_CHANNEL_CONTEXT;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get or create the temp workspace for sessions without a workspace
|
|
167
|
+
*/
|
|
168
|
+
private getOrCreateTempWorkspace(): Workspace {
|
|
169
|
+
// Check if temp workspace exists
|
|
170
|
+
const existing = this.workspaceRepo.findById(TEMP_WORKSPACE_ID);
|
|
171
|
+
if (existing) {
|
|
172
|
+
const updatedPermissions = {
|
|
173
|
+
...existing.permissions,
|
|
174
|
+
read: true,
|
|
175
|
+
write: true,
|
|
176
|
+
delete: true,
|
|
177
|
+
network: true,
|
|
178
|
+
shell: existing.permissions.shell ?? false,
|
|
179
|
+
unrestrictedFileAccess: true,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (!existing.permissions.unrestrictedFileAccess) {
|
|
183
|
+
this.workspaceRepo.updatePermissions(existing.id, updatedPermissions);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Verify directory exists
|
|
187
|
+
if (fs.existsSync(existing.path)) {
|
|
188
|
+
return { ...existing, permissions: updatedPermissions, isTemp: true };
|
|
189
|
+
}
|
|
190
|
+
// Directory was deleted, recreate it
|
|
191
|
+
const tempDir = path.join(os.tmpdir(), 'cowork-os-temp');
|
|
192
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
193
|
+
return { ...existing, permissions: updatedPermissions, isTemp: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Create temp directory
|
|
197
|
+
const tempDir = path.join(os.tmpdir(), 'cowork-os-temp');
|
|
198
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
199
|
+
|
|
200
|
+
// Create workspace record
|
|
201
|
+
const tempWorkspace: Workspace = {
|
|
202
|
+
id: TEMP_WORKSPACE_ID,
|
|
203
|
+
name: TEMP_WORKSPACE_NAME,
|
|
204
|
+
path: tempDir,
|
|
205
|
+
createdAt: Date.now(),
|
|
206
|
+
permissions: {
|
|
207
|
+
read: true,
|
|
208
|
+
write: true,
|
|
209
|
+
delete: true,
|
|
210
|
+
network: true,
|
|
211
|
+
shell: false,
|
|
212
|
+
unrestrictedFileAccess: true,
|
|
213
|
+
},
|
|
214
|
+
isTemp: true,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const stmt = this.db.prepare(`
|
|
218
|
+
INSERT OR REPLACE INTO workspaces (id, name, path, created_at, permissions)
|
|
219
|
+
VALUES (?, ?, ?, ?, ?)
|
|
220
|
+
`);
|
|
221
|
+
stmt.run(
|
|
222
|
+
tempWorkspace.id,
|
|
223
|
+
tempWorkspace.name,
|
|
224
|
+
tempWorkspace.path,
|
|
225
|
+
tempWorkspace.createdAt,
|
|
226
|
+
JSON.stringify(tempWorkspace.permissions)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return tempWorkspace;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Register a channel adapter
|
|
234
|
+
*/
|
|
235
|
+
registerAdapter(adapter: ChannelAdapter): void {
|
|
236
|
+
// Set up message handler
|
|
237
|
+
adapter.onMessage(async (message) => {
|
|
238
|
+
await this.handleMessage(adapter, message);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Set up callback query handler for inline keyboards
|
|
242
|
+
if (adapter.onCallbackQuery) {
|
|
243
|
+
adapter.onCallbackQuery(async (query) => {
|
|
244
|
+
await this.handleCallbackQuery(adapter, query);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Set up error handler
|
|
249
|
+
adapter.onError((error, context) => {
|
|
250
|
+
console.error(`[${adapter.type}] Error in ${context}:`, error);
|
|
251
|
+
this.emitEvent({
|
|
252
|
+
type: 'channel:error',
|
|
253
|
+
channel: adapter.type,
|
|
254
|
+
timestamp: new Date(),
|
|
255
|
+
data: { error: error.message, context },
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Set up status handler
|
|
260
|
+
adapter.onStatusChange((status, error) => {
|
|
261
|
+
const eventType = status === 'connected' ? 'channel:connected' : 'channel:disconnected';
|
|
262
|
+
this.emitEvent({
|
|
263
|
+
type: eventType,
|
|
264
|
+
channel: adapter.type,
|
|
265
|
+
timestamp: new Date(),
|
|
266
|
+
data: { status, error: error?.message },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Update channel status in database
|
|
270
|
+
const channel = this.channelRepo.findByType(adapter.type);
|
|
271
|
+
if (channel) {
|
|
272
|
+
this.channelRepo.update(channel.id, {
|
|
273
|
+
status,
|
|
274
|
+
botUsername: adapter.botUsername,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
this.adapters.set(adapter.type, adapter);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get a registered adapter
|
|
284
|
+
*/
|
|
285
|
+
getAdapter(type: ChannelType): ChannelAdapter | undefined {
|
|
286
|
+
return this.adapters.get(type);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get all registered adapters
|
|
291
|
+
*/
|
|
292
|
+
getAllAdapters(): ChannelAdapter[] {
|
|
293
|
+
return Array.from(this.adapters.values());
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Connect all enabled adapters
|
|
298
|
+
*/
|
|
299
|
+
async connectAll(): Promise<void> {
|
|
300
|
+
const enabledChannels = this.channelRepo.findEnabled();
|
|
301
|
+
|
|
302
|
+
for (const channel of enabledChannels) {
|
|
303
|
+
const adapter = this.adapters.get(channel.type as ChannelType);
|
|
304
|
+
if (adapter && adapter.status !== 'connected') {
|
|
305
|
+
try {
|
|
306
|
+
await adapter.connect();
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error(`Failed to connect ${channel.type}:`, error);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Disconnect all adapters
|
|
316
|
+
*/
|
|
317
|
+
async disconnectAll(): Promise<void> {
|
|
318
|
+
for (const adapter of this.adapters.values()) {
|
|
319
|
+
if (adapter.status === 'connected') {
|
|
320
|
+
try {
|
|
321
|
+
await adapter.disconnect();
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error(`Failed to disconnect ${adapter.type}:`, error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Send a message through a channel
|
|
331
|
+
*/
|
|
332
|
+
async sendMessage(channelType: ChannelType, message: OutgoingMessage): Promise<string> {
|
|
333
|
+
const adapter = this.adapters.get(channelType);
|
|
334
|
+
if (!adapter) {
|
|
335
|
+
throw new Error(`No adapter registered for channel type: ${channelType}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (adapter.status !== 'connected') {
|
|
339
|
+
throw new Error(`Adapter ${channelType} is not connected`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const messageId = await adapter.sendMessage(message);
|
|
343
|
+
|
|
344
|
+
// Find channel for logging
|
|
345
|
+
const channel = this.channelRepo.findByType(channelType);
|
|
346
|
+
if (channel) {
|
|
347
|
+
// Log outgoing message
|
|
348
|
+
this.messageRepo.create({
|
|
349
|
+
channelId: channel.id,
|
|
350
|
+
channelMessageId: messageId,
|
|
351
|
+
chatId: message.chatId,
|
|
352
|
+
direction: 'outgoing',
|
|
353
|
+
content: message.text,
|
|
354
|
+
timestamp: Date.now(),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
this.emitEvent({
|
|
358
|
+
type: 'message:sent',
|
|
359
|
+
channel: channelType,
|
|
360
|
+
timestamp: new Date(),
|
|
361
|
+
data: { chatId: message.chatId, messageId },
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return messageId;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Register an event handler
|
|
370
|
+
*/
|
|
371
|
+
onEvent(handler: GatewayEventHandler): void {
|
|
372
|
+
this.eventHandlers.push(handler);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Private methods
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Transcribe audio attachments in a message
|
|
379
|
+
* Downloads audio from URL or uses buffer, transcribes via VoiceService
|
|
380
|
+
* Saves audio file to a temp folder for transcription and sets message text to include full transcript with context
|
|
381
|
+
*/
|
|
382
|
+
private async transcribeAudioAttachments(message: IncomingMessage, workspacePath?: string): Promise<void> {
|
|
383
|
+
if (!message.attachments || message.attachments.length === 0) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const audioAttachments = message.attachments.filter(a => a.type === 'audio');
|
|
388
|
+
if (audioAttachments.length === 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const voiceService = getVoiceService();
|
|
393
|
+
|
|
394
|
+
// Check if transcription is available
|
|
395
|
+
if (!voiceService.isTranscriptionAvailable()) {
|
|
396
|
+
console.log('[Router] Audio transcription not available - no STT provider configured');
|
|
397
|
+
// Add placeholder for audio messages
|
|
398
|
+
for (const attachment of audioAttachments) {
|
|
399
|
+
const fileName = attachment.fileName || 'voice message';
|
|
400
|
+
message.text += message.text ? `\n[Audio: ${fileName} - transcription unavailable]` : `[Audio: ${fileName} - transcription unavailable]`;
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(`[Router] Transcribing ${audioAttachments.length} audio attachment(s)...`);
|
|
406
|
+
|
|
407
|
+
for (const attachment of audioAttachments) {
|
|
408
|
+
let savedAudioPath: string | undefined;
|
|
409
|
+
try {
|
|
410
|
+
let audioBuffer: Buffer | undefined;
|
|
411
|
+
|
|
412
|
+
// Get audio data from buffer or file
|
|
413
|
+
if (attachment.data) {
|
|
414
|
+
audioBuffer = attachment.data;
|
|
415
|
+
} else if (attachment.url) {
|
|
416
|
+
// Check if it's a local file path
|
|
417
|
+
if (attachment.url.startsWith('/') || attachment.url.startsWith('file://')) {
|
|
418
|
+
const filePath = attachment.url.replace('file://', '');
|
|
419
|
+
if (fs.existsSync(filePath)) {
|
|
420
|
+
audioBuffer = fs.readFileSync(filePath);
|
|
421
|
+
}
|
|
422
|
+
} else if (attachment.url.startsWith('http')) {
|
|
423
|
+
// Download from URL
|
|
424
|
+
try {
|
|
425
|
+
const response = await fetch(attachment.url);
|
|
426
|
+
if (response.ok) {
|
|
427
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
428
|
+
audioBuffer = Buffer.from(arrayBuffer);
|
|
429
|
+
}
|
|
430
|
+
} catch (fetchError) {
|
|
431
|
+
console.error('[Router] Failed to download audio:', fetchError);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!audioBuffer || audioBuffer.length === 0) {
|
|
437
|
+
console.log('[Router] No audio data available for transcription');
|
|
438
|
+
const fileName = attachment.fileName || 'voice message';
|
|
439
|
+
message.text += message.text ? `\n[Audio: ${fileName} - could not load]` : `[Audio: ${fileName} - could not load]`;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Save audio file to temp directory for transcription
|
|
444
|
+
try {
|
|
445
|
+
const tempDir = path.join(os.tmpdir(), 'cowork-audio');
|
|
446
|
+
if (!fs.existsSync(tempDir)) {
|
|
447
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
448
|
+
}
|
|
449
|
+
const audioFileName = attachment.fileName || `voice_message_${Date.now()}.ogg`;
|
|
450
|
+
savedAudioPath = path.join(tempDir, audioFileName);
|
|
451
|
+
fs.writeFileSync(savedAudioPath, audioBuffer);
|
|
452
|
+
console.log(`[Router] Saved audio file to: ${savedAudioPath}`);
|
|
453
|
+
} catch (saveError) {
|
|
454
|
+
console.error('[Router] Failed to save audio file:', saveError);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Transcribe the audio
|
|
458
|
+
const transcript = await voiceService.transcribe(audioBuffer, { force: true });
|
|
459
|
+
|
|
460
|
+
if (transcript && transcript.trim()) {
|
|
461
|
+
console.log(`[Router] Transcribed audio: "${transcript.substring(0, 100)}${transcript.length > 100 ? '...' : ''}"`);
|
|
462
|
+
|
|
463
|
+
// Create a structured message with the full transcript
|
|
464
|
+
// This ensures the agent knows it's a voice message and has the complete transcript
|
|
465
|
+
const voiceMessageContext = [
|
|
466
|
+
'📢 **Voice Message Received**',
|
|
467
|
+
'',
|
|
468
|
+
'The user sent a voice message. Here is the complete transcription:',
|
|
469
|
+
'',
|
|
470
|
+
'---',
|
|
471
|
+
transcript,
|
|
472
|
+
'---',
|
|
473
|
+
'',
|
|
474
|
+
'Please respond to the user\'s voice message above.',
|
|
475
|
+
].filter(line => line !== undefined).join('\n');
|
|
476
|
+
|
|
477
|
+
// Append or set the transcribed text with context
|
|
478
|
+
if (message.text && message.text.trim()) {
|
|
479
|
+
message.text += `\n\n${voiceMessageContext}`;
|
|
480
|
+
} else {
|
|
481
|
+
message.text = voiceMessageContext;
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
const fileName = attachment.fileName || 'voice message';
|
|
485
|
+
message.text += message.text ? `\n[Audio: ${fileName} - no speech detected]` : `[Audio: ${fileName} - no speech detected]`;
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.error('[Router] Failed to transcribe audio:', error);
|
|
489
|
+
const fileName = attachment.fileName || 'voice message';
|
|
490
|
+
message.text += message.text ? `\n[Audio: ${fileName} - transcription failed]` : `[Audio: ${fileName} - transcription failed]`;
|
|
491
|
+
} finally {
|
|
492
|
+
if (savedAudioPath && fs.existsSync(savedAudioPath)) {
|
|
493
|
+
try {
|
|
494
|
+
fs.unlinkSync(savedAudioPath);
|
|
495
|
+
} catch (cleanupError) {
|
|
496
|
+
console.error('[Router] Failed to delete temp audio file:', cleanupError);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Handle an incoming message
|
|
505
|
+
*/
|
|
506
|
+
private async handleMessage(adapter: ChannelAdapter, message: IncomingMessage): Promise<void> {
|
|
507
|
+
const channelType = adapter.type;
|
|
508
|
+
const channel = this.channelRepo.findByType(channelType);
|
|
509
|
+
|
|
510
|
+
if (!channel) {
|
|
511
|
+
console.error(`No channel configuration found for ${channelType}`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Transcribe any audio attachments before processing
|
|
516
|
+
await this.transcribeAudioAttachments(message);
|
|
517
|
+
|
|
518
|
+
// Log incoming message
|
|
519
|
+
this.messageRepo.create({
|
|
520
|
+
channelId: channel.id,
|
|
521
|
+
channelMessageId: message.messageId,
|
|
522
|
+
chatId: message.chatId,
|
|
523
|
+
direction: 'incoming',
|
|
524
|
+
content: message.text,
|
|
525
|
+
timestamp: message.timestamp.getTime(),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
this.emitEvent({
|
|
529
|
+
type: 'message:received',
|
|
530
|
+
channel: channelType,
|
|
531
|
+
timestamp: new Date(),
|
|
532
|
+
data: {
|
|
533
|
+
messageId: message.messageId,
|
|
534
|
+
chatId: message.chatId,
|
|
535
|
+
userId: message.userId,
|
|
536
|
+
preview: message.text.slice(0, 100),
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Security check
|
|
541
|
+
const securityResult = await this.securityManager.checkAccess(channel, message);
|
|
542
|
+
|
|
543
|
+
if (!securityResult.allowed) {
|
|
544
|
+
// Handle unauthorized access
|
|
545
|
+
await this.handleUnauthorizedMessage(adapter, message, securityResult);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Update user's last seen
|
|
550
|
+
if (securityResult.user) {
|
|
551
|
+
this.userRepo.update(securityResult.user.id, {
|
|
552
|
+
lastSeenAt: Date.now(),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Get or create session
|
|
557
|
+
const session = await this.sessionManager.getOrCreateSession(
|
|
558
|
+
channel,
|
|
559
|
+
message.chatId,
|
|
560
|
+
securityResult.user?.id,
|
|
561
|
+
this.config.defaultWorkspaceId
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// Handle the message based on content
|
|
565
|
+
await this.routeMessage(adapter, message, session.id);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Handle unauthorized message
|
|
570
|
+
*/
|
|
571
|
+
private async handleUnauthorizedMessage(
|
|
572
|
+
adapter: ChannelAdapter,
|
|
573
|
+
message: IncomingMessage,
|
|
574
|
+
securityResult: { reason?: string; pairingRequired?: boolean }
|
|
575
|
+
): Promise<void> {
|
|
576
|
+
// If pairing is required, check if the message IS a pairing code or /pair command
|
|
577
|
+
if (securityResult.pairingRequired) {
|
|
578
|
+
const text = message.text.trim();
|
|
579
|
+
|
|
580
|
+
// Check if it's a /pair command
|
|
581
|
+
if (text.toLowerCase().startsWith('/pair ')) {
|
|
582
|
+
const code = text.slice(6).trim(); // Remove '/pair ' prefix
|
|
583
|
+
if (code) {
|
|
584
|
+
await this.handlePairingAttempt(adapter, message, code);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Check if the raw text looks like a pairing code
|
|
590
|
+
if (this.looksLikePairingCode(text)) {
|
|
591
|
+
// This looks like a pairing code - try to verify it
|
|
592
|
+
await this.handlePairingAttempt(adapter, message, text);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Not a pairing code or pairing not required - send appropriate message
|
|
598
|
+
let responseText: string;
|
|
599
|
+
|
|
600
|
+
if (securityResult.pairingRequired) {
|
|
601
|
+
responseText = this.config.pairingRequiredMessage!;
|
|
602
|
+
} else {
|
|
603
|
+
responseText = this.config.unauthorizedMessage!;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
await adapter.sendMessage({
|
|
608
|
+
chatId: message.chatId,
|
|
609
|
+
text: responseText,
|
|
610
|
+
replyTo: message.messageId,
|
|
611
|
+
});
|
|
612
|
+
} catch (error) {
|
|
613
|
+
console.error('Failed to send unauthorized message response:', error);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Route message to appropriate handler
|
|
619
|
+
*/
|
|
620
|
+
private async routeMessage(
|
|
621
|
+
adapter: ChannelAdapter,
|
|
622
|
+
message: IncomingMessage,
|
|
623
|
+
sessionId: string
|
|
624
|
+
): Promise<void> {
|
|
625
|
+
const text = message.text.trim();
|
|
626
|
+
|
|
627
|
+
// Handle commands
|
|
628
|
+
if (text.startsWith('/')) {
|
|
629
|
+
await this.handleCommand(adapter, message, sessionId);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Check if this is a pairing code
|
|
634
|
+
if (this.looksLikePairingCode(text)) {
|
|
635
|
+
await this.handlePairingAttempt(adapter, message, text);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Check if session has no workspace - might be workspace selection
|
|
640
|
+
const session = this.sessionRepo.findById(sessionId);
|
|
641
|
+
if (!session?.workspaceId) {
|
|
642
|
+
// Check if this looks like workspace selection (number or short name)
|
|
643
|
+
const workspaces = this.workspaceRepo.findAll();
|
|
644
|
+
if (workspaces.length > 0) {
|
|
645
|
+
// Try to match by number
|
|
646
|
+
const num = parseInt(text, 10);
|
|
647
|
+
if (!isNaN(num) && num > 0 && num <= workspaces.length) {
|
|
648
|
+
const workspace = workspaces[num - 1];
|
|
649
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
|
|
650
|
+
await adapter.sendMessage({
|
|
651
|
+
chatId: message.chatId,
|
|
652
|
+
text: `✅ *${workspace.name}* selected!\n\nYou can now send me tasks.\n\nExample: "Create a new React component called Button"`,
|
|
653
|
+
parseMode: 'markdown',
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Try to match by name (case-insensitive partial match)
|
|
659
|
+
const matchedWorkspace = workspaces.find(
|
|
660
|
+
ws => ws.name.toLowerCase() === text.toLowerCase() ||
|
|
661
|
+
ws.name.toLowerCase().startsWith(text.toLowerCase())
|
|
662
|
+
);
|
|
663
|
+
if (matchedWorkspace) {
|
|
664
|
+
this.sessionManager.setSessionWorkspace(sessionId, matchedWorkspace.id);
|
|
665
|
+
await adapter.sendMessage({
|
|
666
|
+
chatId: message.chatId,
|
|
667
|
+
text: `✅ *${matchedWorkspace.name}* selected!\n\nYou can now send me tasks.\n\nExample: "Create a new React component called Button"`,
|
|
668
|
+
parseMode: 'markdown',
|
|
669
|
+
});
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// No workspace match found - auto-assign temp workspace so tasks can proceed
|
|
675
|
+
const tempWorkspace = this.getOrCreateTempWorkspace();
|
|
676
|
+
this.sessionManager.setSessionWorkspace(sessionId, tempWorkspace.id);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Regular message - send to desktop app for task processing
|
|
680
|
+
await this.forwardToDesktopApp(adapter, message, sessionId);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Handle bot commands
|
|
685
|
+
*/
|
|
686
|
+
private async handleCommand(
|
|
687
|
+
adapter: ChannelAdapter,
|
|
688
|
+
message: IncomingMessage,
|
|
689
|
+
sessionId: string
|
|
690
|
+
): Promise<void> {
|
|
691
|
+
const [command, ...args] = message.text.trim().split(/\s+/);
|
|
692
|
+
|
|
693
|
+
switch (command.toLowerCase()) {
|
|
694
|
+
case '/start':
|
|
695
|
+
await this.handleStartCommand(adapter, message, sessionId);
|
|
696
|
+
break;
|
|
697
|
+
|
|
698
|
+
case '/help':
|
|
699
|
+
await adapter.sendMessage({
|
|
700
|
+
chatId: message.chatId,
|
|
701
|
+
text: this.getHelpText(adapter.type),
|
|
702
|
+
parseMode: 'markdown',
|
|
703
|
+
});
|
|
704
|
+
break;
|
|
705
|
+
|
|
706
|
+
case '/status':
|
|
707
|
+
await this.handleStatusCommand(adapter, message, sessionId);
|
|
708
|
+
break;
|
|
709
|
+
|
|
710
|
+
case '/workspaces':
|
|
711
|
+
await this.handleWorkspacesCommand(adapter, message);
|
|
712
|
+
break;
|
|
713
|
+
|
|
714
|
+
case '/workspace':
|
|
715
|
+
await this.handleWorkspaceCommand(adapter, message, sessionId, args);
|
|
716
|
+
break;
|
|
717
|
+
|
|
718
|
+
case '/cancel':
|
|
719
|
+
// Cancel current task if any
|
|
720
|
+
await this.handleCancelCommand(adapter, message, sessionId);
|
|
721
|
+
break;
|
|
722
|
+
|
|
723
|
+
case '/newtask':
|
|
724
|
+
// Start a new task (unlink current session)
|
|
725
|
+
await this.handleNewTaskCommand(adapter, message, sessionId);
|
|
726
|
+
break;
|
|
727
|
+
|
|
728
|
+
case '/addworkspace':
|
|
729
|
+
await this.handleAddWorkspaceCommand(adapter, message, sessionId, args);
|
|
730
|
+
break;
|
|
731
|
+
|
|
732
|
+
case '/models':
|
|
733
|
+
await this.handleModelsCommand(adapter, message);
|
|
734
|
+
break;
|
|
735
|
+
|
|
736
|
+
case '/model':
|
|
737
|
+
await this.handleModelCommand(adapter, message, args);
|
|
738
|
+
break;
|
|
739
|
+
|
|
740
|
+
case '/provider':
|
|
741
|
+
await this.handleProviderCommand(adapter, message, args);
|
|
742
|
+
break;
|
|
743
|
+
|
|
744
|
+
case '/pair':
|
|
745
|
+
// Handle pairing code
|
|
746
|
+
if (args.length === 0) {
|
|
747
|
+
await adapter.sendMessage({
|
|
748
|
+
chatId: message.chatId,
|
|
749
|
+
text: '🔐 Please provide a pairing code.\n\nUsage: `/pair <code>`',
|
|
750
|
+
parseMode: 'markdown',
|
|
751
|
+
});
|
|
752
|
+
} else {
|
|
753
|
+
const code = args[0].trim();
|
|
754
|
+
await this.handlePairingAttempt(adapter, message, code);
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
757
|
+
|
|
758
|
+
case '/shell':
|
|
759
|
+
await this.handleShellCommand(adapter, message, sessionId, args);
|
|
760
|
+
break;
|
|
761
|
+
|
|
762
|
+
case '/approve':
|
|
763
|
+
case '/yes':
|
|
764
|
+
case '/y':
|
|
765
|
+
await this.handleApproveCommand(adapter, message, sessionId);
|
|
766
|
+
break;
|
|
767
|
+
|
|
768
|
+
case '/deny':
|
|
769
|
+
case '/no':
|
|
770
|
+
case '/n':
|
|
771
|
+
await this.handleDenyCommand(adapter, message, sessionId);
|
|
772
|
+
break;
|
|
773
|
+
|
|
774
|
+
case '/queue':
|
|
775
|
+
await this.handleQueueCommand(adapter, message, args);
|
|
776
|
+
break;
|
|
777
|
+
|
|
778
|
+
case '/removeworkspace':
|
|
779
|
+
await this.handleRemoveWorkspaceCommand(adapter, message, sessionId, args);
|
|
780
|
+
break;
|
|
781
|
+
|
|
782
|
+
case '/retry':
|
|
783
|
+
await this.handleRetryCommand(adapter, message, sessionId);
|
|
784
|
+
break;
|
|
785
|
+
|
|
786
|
+
case '/history':
|
|
787
|
+
await this.handleHistoryCommand(adapter, message, sessionId);
|
|
788
|
+
break;
|
|
789
|
+
|
|
790
|
+
case '/skills':
|
|
791
|
+
await this.handleSkillsCommand(adapter, message, sessionId);
|
|
792
|
+
break;
|
|
793
|
+
|
|
794
|
+
case '/skill':
|
|
795
|
+
await this.handleSkillCommand(adapter, message, sessionId, args);
|
|
796
|
+
break;
|
|
797
|
+
|
|
798
|
+
case '/providers':
|
|
799
|
+
await this.handleProvidersCommand(adapter, message);
|
|
800
|
+
break;
|
|
801
|
+
|
|
802
|
+
case '/settings':
|
|
803
|
+
await this.handleSettingsCommand(adapter, message, sessionId);
|
|
804
|
+
break;
|
|
805
|
+
|
|
806
|
+
case '/debug':
|
|
807
|
+
await this.handleDebugCommand(adapter, message, sessionId);
|
|
808
|
+
break;
|
|
809
|
+
|
|
810
|
+
case '/version':
|
|
811
|
+
await this.handleVersionCommand(adapter, message);
|
|
812
|
+
break;
|
|
813
|
+
|
|
814
|
+
default:
|
|
815
|
+
await adapter.sendMessage({
|
|
816
|
+
chatId: message.chatId,
|
|
817
|
+
text: `Unknown command: ${command}\n\nUse /help to see available commands.`,
|
|
818
|
+
replyTo: message.messageId,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Handle /status command
|
|
825
|
+
*/
|
|
826
|
+
private async handleStatusCommand(
|
|
827
|
+
adapter: ChannelAdapter,
|
|
828
|
+
message: IncomingMessage,
|
|
829
|
+
sessionId: string
|
|
830
|
+
): Promise<void> {
|
|
831
|
+
const session = this.sessionRepo.findById(sessionId);
|
|
832
|
+
let statusText = '✅ Bot is online and ready.\n\n';
|
|
833
|
+
|
|
834
|
+
if (session?.workspaceId) {
|
|
835
|
+
const workspace = this.workspaceRepo.findById(session.workspaceId);
|
|
836
|
+
if (workspace) {
|
|
837
|
+
statusText += `📁 Current workspace: ${workspace.name}\n`;
|
|
838
|
+
statusText += ` Path: ${workspace.path}\n`;
|
|
839
|
+
}
|
|
840
|
+
} else {
|
|
841
|
+
statusText += '⚠️ No workspace selected. Use /workspaces to see available workspaces.';
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (session?.taskId) {
|
|
845
|
+
const task = this.taskRepo.findById(session.taskId);
|
|
846
|
+
if (task) {
|
|
847
|
+
statusText += `\n🔄 Active task: ${task.title} (${task.status})`;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
await adapter.sendMessage({
|
|
852
|
+
chatId: message.chatId,
|
|
853
|
+
text: statusText,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Handle /workspaces command - list available workspaces
|
|
859
|
+
*/
|
|
860
|
+
private async handleWorkspacesCommand(
|
|
861
|
+
adapter: ChannelAdapter,
|
|
862
|
+
message: IncomingMessage
|
|
863
|
+
): Promise<void> {
|
|
864
|
+
const workspaces = this.workspaceRepo.findAll();
|
|
865
|
+
|
|
866
|
+
if (workspaces.length === 0) {
|
|
867
|
+
await adapter.sendMessage({
|
|
868
|
+
chatId: message.chatId,
|
|
869
|
+
text: '📁 No workspaces configured yet.\n\nAdd a workspace in the CoWork desktop app first, or use:\n`/addworkspace /path/to/your/project`',
|
|
870
|
+
parseMode: 'markdown',
|
|
871
|
+
});
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// WhatsApp and iMessage don't support inline keyboards - use text-based selection
|
|
876
|
+
if (adapter.type === 'whatsapp' || adapter.type === 'imessage') {
|
|
877
|
+
let text = '📁 *Available Workspaces*\n\n';
|
|
878
|
+
workspaces.forEach((ws, index) => {
|
|
879
|
+
text += `${index + 1}. *${ws.name}*\n \`${ws.path}\`\n\n`;
|
|
880
|
+
});
|
|
881
|
+
text += '━━━━━━━━━━━━━━━\n';
|
|
882
|
+
text += 'Reply with the number or name to select.\n';
|
|
883
|
+
text += 'Example: `1` or `myproject`';
|
|
884
|
+
|
|
885
|
+
await adapter.sendMessage({
|
|
886
|
+
chatId: message.chatId,
|
|
887
|
+
text,
|
|
888
|
+
parseMode: 'markdown',
|
|
889
|
+
});
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Build inline keyboard with workspace buttons for Telegram/Discord
|
|
894
|
+
const keyboard: InlineKeyboardButton[][] = [];
|
|
895
|
+
for (const ws of workspaces) {
|
|
896
|
+
// Create one button per row for better readability
|
|
897
|
+
keyboard.push([{
|
|
898
|
+
text: `📁 ${ws.name}`,
|
|
899
|
+
callbackData: `workspace:${ws.id}`,
|
|
900
|
+
}]);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
let text = '📁 *Available Workspaces*\n\nTap a workspace to select it:';
|
|
904
|
+
|
|
905
|
+
await adapter.sendMessage({
|
|
906
|
+
chatId: message.chatId,
|
|
907
|
+
text,
|
|
908
|
+
parseMode: 'markdown',
|
|
909
|
+
inlineKeyboard: keyboard,
|
|
910
|
+
threadId: message.threadId,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Handle /workspace command - set current workspace
|
|
916
|
+
*/
|
|
917
|
+
private async handleWorkspaceCommand(
|
|
918
|
+
adapter: ChannelAdapter,
|
|
919
|
+
message: IncomingMessage,
|
|
920
|
+
sessionId: string,
|
|
921
|
+
args: string[]
|
|
922
|
+
): Promise<void> {
|
|
923
|
+
if (args.length === 0) {
|
|
924
|
+
// Show current workspace
|
|
925
|
+
let session = this.sessionRepo.findById(sessionId);
|
|
926
|
+
|
|
927
|
+
// Auto-assign temp workspace if none selected
|
|
928
|
+
if (!session?.workspaceId) {
|
|
929
|
+
const tempWorkspace = this.getOrCreateTempWorkspace();
|
|
930
|
+
this.sessionRepo.update(sessionId, { workspaceId: tempWorkspace.id });
|
|
931
|
+
session = this.sessionRepo.findById(sessionId);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (session?.workspaceId) {
|
|
935
|
+
const workspace = this.workspaceRepo.findById(session.workspaceId);
|
|
936
|
+
if (workspace) {
|
|
937
|
+
const isTempWorkspace = workspace.id === TEMP_WORKSPACE_ID;
|
|
938
|
+
const displayName = isTempWorkspace ? 'Temporary Workspace (work in a folder for persistence)' : workspace.name;
|
|
939
|
+
await adapter.sendMessage({
|
|
940
|
+
chatId: message.chatId,
|
|
941
|
+
text: `📁 Current workspace: *${displayName}*\n\`${workspace.path}\`\n\nUse \`/workspaces\` to see available workspaces.`,
|
|
942
|
+
parseMode: 'markdown',
|
|
943
|
+
});
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
await adapter.sendMessage({
|
|
948
|
+
chatId: message.chatId,
|
|
949
|
+
text: 'No workspace selected. Use `/workspaces` to see available workspaces.',
|
|
950
|
+
parseMode: 'markdown',
|
|
951
|
+
});
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const workspaces = this.workspaceRepo.findAll();
|
|
956
|
+
const selector = args.join(' ');
|
|
957
|
+
let workspace;
|
|
958
|
+
|
|
959
|
+
// Try to find by number
|
|
960
|
+
const num = parseInt(selector, 10);
|
|
961
|
+
if (!isNaN(num) && num > 0 && num <= workspaces.length) {
|
|
962
|
+
workspace = workspaces[num - 1];
|
|
963
|
+
} else {
|
|
964
|
+
// Try to find by name (case-insensitive)
|
|
965
|
+
workspace = workspaces.find(
|
|
966
|
+
ws => ws.name.toLowerCase() === selector.toLowerCase()
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (!workspace) {
|
|
971
|
+
await adapter.sendMessage({
|
|
972
|
+
chatId: message.chatId,
|
|
973
|
+
text: `❌ Workspace not found: "${selector}"\n\nUse /workspaces to see available workspaces.`,
|
|
974
|
+
});
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Update session workspace
|
|
979
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
|
|
980
|
+
|
|
981
|
+
await adapter.sendMessage({
|
|
982
|
+
chatId: message.chatId,
|
|
983
|
+
text: `✅ Workspace set to: *${workspace.name}*\n\`${workspace.path}\`\n\nYou can now send messages to create tasks in this workspace.`,
|
|
984
|
+
parseMode: 'markdown',
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Handle /addworkspace command - add a new workspace by path
|
|
990
|
+
*/
|
|
991
|
+
private async handleAddWorkspaceCommand(
|
|
992
|
+
adapter: ChannelAdapter,
|
|
993
|
+
message: IncomingMessage,
|
|
994
|
+
sessionId: string,
|
|
995
|
+
args: string[]
|
|
996
|
+
): Promise<void> {
|
|
997
|
+
if (args.length === 0) {
|
|
998
|
+
await adapter.sendMessage({
|
|
999
|
+
chatId: message.chatId,
|
|
1000
|
+
text: '📁 *Add Workspace*\n\nUsage: `/addworkspace <path>`\n\nExample:\n`/addworkspace /Users/john/projects/myapp`\n`/addworkspace ~/Documents`',
|
|
1001
|
+
parseMode: 'markdown',
|
|
1002
|
+
});
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Join args to handle paths with spaces
|
|
1007
|
+
let workspacePath = args.join(' ');
|
|
1008
|
+
|
|
1009
|
+
// Expand ~ to home directory
|
|
1010
|
+
if (workspacePath.startsWith('~')) {
|
|
1011
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
1012
|
+
workspacePath = workspacePath.replace('~', homeDir);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Resolve to absolute path
|
|
1016
|
+
workspacePath = path.resolve(workspacePath);
|
|
1017
|
+
|
|
1018
|
+
// Check if path exists and is a directory
|
|
1019
|
+
try {
|
|
1020
|
+
const stats = fs.statSync(workspacePath);
|
|
1021
|
+
if (!stats.isDirectory()) {
|
|
1022
|
+
await adapter.sendMessage({
|
|
1023
|
+
chatId: message.chatId,
|
|
1024
|
+
text: `❌ Path is not a directory: \`${workspacePath}\``,
|
|
1025
|
+
parseMode: 'markdown',
|
|
1026
|
+
});
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
} catch {
|
|
1030
|
+
await adapter.sendMessage({
|
|
1031
|
+
chatId: message.chatId,
|
|
1032
|
+
text: `❌ Directory not found: \`${workspacePath}\``,
|
|
1033
|
+
parseMode: 'markdown',
|
|
1034
|
+
});
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Check if workspace already exists
|
|
1039
|
+
const existingWorkspaces = this.workspaceRepo.findAll();
|
|
1040
|
+
const existing = existingWorkspaces.find(ws => ws.path === workspacePath);
|
|
1041
|
+
if (existing) {
|
|
1042
|
+
// Workspace exists, just select it
|
|
1043
|
+
this.sessionManager.setSessionWorkspace(sessionId, existing.id);
|
|
1044
|
+
await adapter.sendMessage({
|
|
1045
|
+
chatId: message.chatId,
|
|
1046
|
+
text: `📁 Workspace already exists!\n\n✅ Selected: *${existing.name}*\n\`${existing.path}\``,
|
|
1047
|
+
parseMode: 'markdown',
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Create workspace name from path
|
|
1053
|
+
const workspaceName = path.basename(workspacePath);
|
|
1054
|
+
|
|
1055
|
+
// Create new workspace with default permissions
|
|
1056
|
+
// Note: network is enabled by default for browser tools (web access)
|
|
1057
|
+
const workspace = this.workspaceRepo.create(
|
|
1058
|
+
workspaceName,
|
|
1059
|
+
workspacePath,
|
|
1060
|
+
{
|
|
1061
|
+
read: true,
|
|
1062
|
+
write: true,
|
|
1063
|
+
delete: false, // Requires approval
|
|
1064
|
+
network: true,
|
|
1065
|
+
shell: false, // Requires approval
|
|
1066
|
+
}
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
// Set as current workspace
|
|
1070
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
|
|
1071
|
+
|
|
1072
|
+
// Notify desktop app
|
|
1073
|
+
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
1074
|
+
this.mainWindow.webContents.send('workspace:added', {
|
|
1075
|
+
id: workspace.id,
|
|
1076
|
+
name: workspace.name,
|
|
1077
|
+
path: workspace.path,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
await adapter.sendMessage({
|
|
1082
|
+
chatId: message.chatId,
|
|
1083
|
+
text: `✅ Workspace added and selected!\n\n📁 *${workspace.name}*\n\`${workspace.path}\`\n\nYou can now send messages to create tasks in this workspace.`,
|
|
1084
|
+
parseMode: 'markdown',
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Handle /models command - list available models and providers
|
|
1090
|
+
*/
|
|
1091
|
+
private async handleModelsCommand(
|
|
1092
|
+
adapter: ChannelAdapter,
|
|
1093
|
+
message: IncomingMessage
|
|
1094
|
+
): Promise<void> {
|
|
1095
|
+
const status = LLMProviderFactory.getConfigStatus();
|
|
1096
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
1097
|
+
const providerType = status.currentProvider;
|
|
1098
|
+
|
|
1099
|
+
let text = '🤖 *AI Models & Providers*\n\n';
|
|
1100
|
+
|
|
1101
|
+
// Get provider-specific models and current model
|
|
1102
|
+
let models: Array<{ key: string; displayName: string }> = [];
|
|
1103
|
+
let currentModel = settings.modelKey;
|
|
1104
|
+
|
|
1105
|
+
// Provider display names
|
|
1106
|
+
const providerModelNames: Record<string, string> = {
|
|
1107
|
+
'anthropic': 'Claude',
|
|
1108
|
+
'bedrock': 'Claude',
|
|
1109
|
+
'openai': 'OpenAI',
|
|
1110
|
+
'gemini': 'Gemini',
|
|
1111
|
+
'openrouter': 'OpenRouter',
|
|
1112
|
+
'ollama': 'Ollama',
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
// Get models based on current provider
|
|
1116
|
+
switch (providerType) {
|
|
1117
|
+
case 'anthropic':
|
|
1118
|
+
case 'bedrock':
|
|
1119
|
+
models = status.models;
|
|
1120
|
+
break;
|
|
1121
|
+
|
|
1122
|
+
case 'openai': {
|
|
1123
|
+
currentModel = settings.openai?.model || 'gpt-4o-mini';
|
|
1124
|
+
const cachedOpenAI = LLMProviderFactory.getCachedModels('openai');
|
|
1125
|
+
if (cachedOpenAI && cachedOpenAI.length > 0) {
|
|
1126
|
+
models = cachedOpenAI;
|
|
1127
|
+
} else {
|
|
1128
|
+
// Default OpenAI models
|
|
1129
|
+
models = [
|
|
1130
|
+
{ key: 'gpt-4o', displayName: 'GPT-4o' },
|
|
1131
|
+
{ key: 'gpt-4o-mini', displayName: 'GPT-4o Mini' },
|
|
1132
|
+
{ key: 'gpt-4-turbo', displayName: 'GPT-4 Turbo' },
|
|
1133
|
+
{ key: 'gpt-3.5-turbo', displayName: 'GPT-3.5 Turbo' },
|
|
1134
|
+
{ key: 'o1', displayName: 'o1' },
|
|
1135
|
+
{ key: 'o1-mini', displayName: 'o1 Mini' },
|
|
1136
|
+
];
|
|
1137
|
+
}
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
case 'gemini': {
|
|
1142
|
+
currentModel = settings.gemini?.model || 'gemini-2.0-flash';
|
|
1143
|
+
const cachedGemini = LLMProviderFactory.getCachedModels('gemini');
|
|
1144
|
+
if (cachedGemini && cachedGemini.length > 0) {
|
|
1145
|
+
models = cachedGemini;
|
|
1146
|
+
} else {
|
|
1147
|
+
models = [
|
|
1148
|
+
{ key: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash' },
|
|
1149
|
+
{ key: 'gemini-1.5-pro', displayName: 'Gemini 1.5 Pro' },
|
|
1150
|
+
{ key: 'gemini-1.5-flash', displayName: 'Gemini 1.5 Flash' },
|
|
1151
|
+
];
|
|
1152
|
+
}
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
case 'openrouter': {
|
|
1157
|
+
currentModel = settings.openrouter?.model || 'anthropic/claude-3.5-sonnet';
|
|
1158
|
+
const cachedOpenRouter = LLMProviderFactory.getCachedModels('openrouter');
|
|
1159
|
+
if (cachedOpenRouter && cachedOpenRouter.length > 0) {
|
|
1160
|
+
models = cachedOpenRouter.slice(0, 10); // Limit to 10 for readability
|
|
1161
|
+
} else {
|
|
1162
|
+
models = [
|
|
1163
|
+
{ key: 'anthropic/claude-3.5-sonnet', displayName: 'Claude 3.5 Sonnet' },
|
|
1164
|
+
{ key: 'openai/gpt-4o', displayName: 'GPT-4o' },
|
|
1165
|
+
{ key: 'google/gemini-pro', displayName: 'Gemini Pro' },
|
|
1166
|
+
];
|
|
1167
|
+
}
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
case 'ollama': {
|
|
1172
|
+
// Ollama handled separately below
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
default:
|
|
1177
|
+
models = status.models;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Current configuration
|
|
1181
|
+
text += '*Current:*\n';
|
|
1182
|
+
const currentProvider = status.providers.find(p => p.type === providerType);
|
|
1183
|
+
text += `• Provider: ${currentProvider?.name || providerType}\n`;
|
|
1184
|
+
|
|
1185
|
+
if (providerType === 'ollama') {
|
|
1186
|
+
const ollamaModel = settings.ollama?.model || 'llama3.2';
|
|
1187
|
+
text += `• Model: ${ollamaModel}\n\n`;
|
|
1188
|
+
} else {
|
|
1189
|
+
const modelInfo = models.find(m => m.key === currentModel);
|
|
1190
|
+
text += `• Model: ${modelInfo?.displayName || currentModel}\n\n`;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Available providers
|
|
1194
|
+
text += '*Available Providers:*\n';
|
|
1195
|
+
status.providers.forEach(provider => {
|
|
1196
|
+
const isActive = provider.type === providerType ? ' ✓' : '';
|
|
1197
|
+
const configStatus = provider.configured ? '🟢' : '⚪';
|
|
1198
|
+
text += `${configStatus} ${provider.name}${isActive}\n`;
|
|
1199
|
+
});
|
|
1200
|
+
text += '\n';
|
|
1201
|
+
|
|
1202
|
+
// Available models - show different list based on provider
|
|
1203
|
+
if (providerType === 'ollama') {
|
|
1204
|
+
text += '*Available Ollama Models:*\n';
|
|
1205
|
+
try {
|
|
1206
|
+
const ollamaModels = await LLMProviderFactory.getOllamaModels();
|
|
1207
|
+
const currentOllamaModel = settings.ollama?.model || 'llama3.2';
|
|
1208
|
+
|
|
1209
|
+
if (ollamaModels.length === 0) {
|
|
1210
|
+
text += '⚠️ No models found. Run `ollama pull <model>` to download.\n';
|
|
1211
|
+
} else {
|
|
1212
|
+
ollamaModels.slice(0, 10).forEach((model, index) => {
|
|
1213
|
+
const isActive = model.name === currentOllamaModel ? ' ✓' : '';
|
|
1214
|
+
const sizeGB = (model.size / 1e9).toFixed(1);
|
|
1215
|
+
text += `${index + 1}. ${model.name} (${sizeGB}GB)${isActive}\n`;
|
|
1216
|
+
});
|
|
1217
|
+
if (ollamaModels.length > 10) {
|
|
1218
|
+
text += ` ... and ${ollamaModels.length - 10} more\n`;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
} catch {
|
|
1222
|
+
text += '⚠️ Could not fetch Ollama models. Is Ollama running?\n';
|
|
1223
|
+
}
|
|
1224
|
+
text += '\n💡 Use `/model <name>` to switch (e.g., `/model llama3.2`)';
|
|
1225
|
+
} else {
|
|
1226
|
+
const modelBrand = providerModelNames[providerType] || 'Available';
|
|
1227
|
+
text += `*Available ${modelBrand} Models:*\n`;
|
|
1228
|
+
models.forEach((model, index) => {
|
|
1229
|
+
const isActive = model.key === currentModel ? ' ✓' : '';
|
|
1230
|
+
text += `${index + 1}. ${model.displayName}${isActive}\n`;
|
|
1231
|
+
});
|
|
1232
|
+
text += '\n💡 Use `/model <name>` to switch\n';
|
|
1233
|
+
text += 'Example: `/model 2` or `/model <model-name>`';
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
await adapter.sendMessage({
|
|
1237
|
+
chatId: message.chatId,
|
|
1238
|
+
text,
|
|
1239
|
+
parseMode: 'markdown',
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Handle /model command - show or change current model within current provider
|
|
1245
|
+
*/
|
|
1246
|
+
private async handleModelCommand(
|
|
1247
|
+
adapter: ChannelAdapter,
|
|
1248
|
+
message: IncomingMessage,
|
|
1249
|
+
args: string[]
|
|
1250
|
+
): Promise<void> {
|
|
1251
|
+
const status = LLMProviderFactory.getConfigStatus();
|
|
1252
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
1253
|
+
const providerType = status.currentProvider;
|
|
1254
|
+
const currentProviderInfo = status.providers.find(p => p.type === providerType);
|
|
1255
|
+
|
|
1256
|
+
// Get provider-specific models and current model
|
|
1257
|
+
let models: Array<{ key: string; displayName: string }> = [];
|
|
1258
|
+
let currentModel = settings.modelKey;
|
|
1259
|
+
|
|
1260
|
+
// Get models based on current provider
|
|
1261
|
+
switch (providerType) {
|
|
1262
|
+
case 'anthropic':
|
|
1263
|
+
case 'bedrock':
|
|
1264
|
+
models = status.models;
|
|
1265
|
+
break;
|
|
1266
|
+
|
|
1267
|
+
case 'openai': {
|
|
1268
|
+
currentModel = settings.openai?.model || 'gpt-4o-mini';
|
|
1269
|
+
const cachedOpenAI = LLMProviderFactory.getCachedModels('openai');
|
|
1270
|
+
if (cachedOpenAI && cachedOpenAI.length > 0) {
|
|
1271
|
+
models = cachedOpenAI;
|
|
1272
|
+
} else {
|
|
1273
|
+
models = [
|
|
1274
|
+
{ key: 'gpt-4o', displayName: 'GPT-4o' },
|
|
1275
|
+
{ key: 'gpt-4o-mini', displayName: 'GPT-4o Mini' },
|
|
1276
|
+
{ key: 'gpt-4-turbo', displayName: 'GPT-4 Turbo' },
|
|
1277
|
+
{ key: 'gpt-3.5-turbo', displayName: 'GPT-3.5 Turbo' },
|
|
1278
|
+
{ key: 'o1', displayName: 'o1' },
|
|
1279
|
+
{ key: 'o1-mini', displayName: 'o1 Mini' },
|
|
1280
|
+
];
|
|
1281
|
+
}
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
case 'gemini': {
|
|
1286
|
+
currentModel = settings.gemini?.model || 'gemini-2.0-flash';
|
|
1287
|
+
const cachedGemini = LLMProviderFactory.getCachedModels('gemini');
|
|
1288
|
+
if (cachedGemini && cachedGemini.length > 0) {
|
|
1289
|
+
models = cachedGemini;
|
|
1290
|
+
} else {
|
|
1291
|
+
models = [
|
|
1292
|
+
{ key: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash' },
|
|
1293
|
+
{ key: 'gemini-1.5-pro', displayName: 'Gemini 1.5 Pro' },
|
|
1294
|
+
{ key: 'gemini-1.5-flash', displayName: 'Gemini 1.5 Flash' },
|
|
1295
|
+
];
|
|
1296
|
+
}
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
case 'openrouter': {
|
|
1301
|
+
currentModel = settings.openrouter?.model || 'anthropic/claude-3.5-sonnet';
|
|
1302
|
+
const cachedOpenRouter = LLMProviderFactory.getCachedModels('openrouter');
|
|
1303
|
+
if (cachedOpenRouter && cachedOpenRouter.length > 0) {
|
|
1304
|
+
models = cachedOpenRouter.slice(0, 10);
|
|
1305
|
+
} else {
|
|
1306
|
+
models = [
|
|
1307
|
+
{ key: 'anthropic/claude-3.5-sonnet', displayName: 'Claude 3.5 Sonnet' },
|
|
1308
|
+
{ key: 'openai/gpt-4o', displayName: 'GPT-4o' },
|
|
1309
|
+
{ key: 'google/gemini-pro', displayName: 'Gemini Pro' },
|
|
1310
|
+
];
|
|
1311
|
+
}
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
case 'ollama':
|
|
1316
|
+
// Handled separately
|
|
1317
|
+
break;
|
|
1318
|
+
|
|
1319
|
+
default:
|
|
1320
|
+
models = status.models;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// If no args, show current model and available models
|
|
1324
|
+
if (args.length === 0) {
|
|
1325
|
+
let text = '🤖 *Current Model*\n\n';
|
|
1326
|
+
text += `• Provider: ${currentProviderInfo?.name || providerType}\n`;
|
|
1327
|
+
|
|
1328
|
+
if (providerType === 'ollama') {
|
|
1329
|
+
const ollamaModel = settings.ollama?.model || 'llama3.2';
|
|
1330
|
+
text += `• Model: ${ollamaModel}\n\n`;
|
|
1331
|
+
|
|
1332
|
+
text += '*Available Models:*\n';
|
|
1333
|
+
try {
|
|
1334
|
+
const ollamaModels = await LLMProviderFactory.getOllamaModels();
|
|
1335
|
+
if (ollamaModels.length === 0) {
|
|
1336
|
+
text += '⚠️ No models found.\n';
|
|
1337
|
+
} else {
|
|
1338
|
+
ollamaModels.slice(0, 8).forEach((model, index) => {
|
|
1339
|
+
const isActive = model.name === ollamaModel ? ' ✓' : '';
|
|
1340
|
+
const sizeGB = (model.size / 1e9).toFixed(1);
|
|
1341
|
+
text += `${index + 1}. ${model.name} (${sizeGB}GB)${isActive}\n`;
|
|
1342
|
+
});
|
|
1343
|
+
if (ollamaModels.length > 8) {
|
|
1344
|
+
text += ` ... and ${ollamaModels.length - 8} more\n`;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
text += '⚠️ Could not fetch models.\n';
|
|
1349
|
+
}
|
|
1350
|
+
text += '\n💡 Use `/model <name>` or `/model <number>` to switch';
|
|
1351
|
+
} else {
|
|
1352
|
+
const modelInfo = models.find(m => m.key === currentModel);
|
|
1353
|
+
text += `• Model: ${modelInfo?.displayName || currentModel}\n\n`;
|
|
1354
|
+
|
|
1355
|
+
text += '*Available Models:*\n';
|
|
1356
|
+
models.forEach((model, index) => {
|
|
1357
|
+
const isActive = model.key === currentModel ? ' ✓' : '';
|
|
1358
|
+
text += `${index + 1}. ${model.displayName}${isActive}\n`;
|
|
1359
|
+
});
|
|
1360
|
+
text += '\n💡 Use `/model <name>` or `/model <number>` to switch';
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
await adapter.sendMessage({
|
|
1364
|
+
chatId: message.chatId,
|
|
1365
|
+
text,
|
|
1366
|
+
parseMode: 'markdown',
|
|
1367
|
+
});
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Change model within current provider
|
|
1372
|
+
const selector = args.join(' ').toLowerCase();
|
|
1373
|
+
|
|
1374
|
+
if (providerType === 'ollama') {
|
|
1375
|
+
const result = await this.selectOllamaModel(selector, args);
|
|
1376
|
+
if (!result.success) {
|
|
1377
|
+
await adapter.sendMessage({
|
|
1378
|
+
chatId: message.chatId,
|
|
1379
|
+
text: result.error!,
|
|
1380
|
+
parseMode: 'markdown',
|
|
1381
|
+
});
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const newSettings: LLMSettings = {
|
|
1386
|
+
...settings,
|
|
1387
|
+
ollama: {
|
|
1388
|
+
...settings.ollama,
|
|
1389
|
+
model: result.model!,
|
|
1390
|
+
},
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
LLMProviderFactory.saveSettings(newSettings);
|
|
1394
|
+
LLMProviderFactory.clearCache();
|
|
1395
|
+
|
|
1396
|
+
await adapter.sendMessage({
|
|
1397
|
+
chatId: message.chatId,
|
|
1398
|
+
text: `✅ Model changed to: *${result.model}*`,
|
|
1399
|
+
parseMode: 'markdown',
|
|
1400
|
+
});
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// For all other providers, use the provider-specific model list
|
|
1405
|
+
const result = this.selectClaudeModel(selector, models);
|
|
1406
|
+
if (!result.success) {
|
|
1407
|
+
await adapter.sendMessage({
|
|
1408
|
+
chatId: message.chatId,
|
|
1409
|
+
text: result.error!,
|
|
1410
|
+
});
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Save to the appropriate provider-specific setting
|
|
1415
|
+
let newSettings: LLMSettings = { ...settings };
|
|
1416
|
+
|
|
1417
|
+
switch (providerType) {
|
|
1418
|
+
case 'openai':
|
|
1419
|
+
newSettings.openai = {
|
|
1420
|
+
...settings.openai,
|
|
1421
|
+
model: result.model!.key,
|
|
1422
|
+
};
|
|
1423
|
+
break;
|
|
1424
|
+
|
|
1425
|
+
case 'gemini':
|
|
1426
|
+
newSettings.gemini = {
|
|
1427
|
+
...settings.gemini,
|
|
1428
|
+
model: result.model!.key,
|
|
1429
|
+
};
|
|
1430
|
+
break;
|
|
1431
|
+
|
|
1432
|
+
case 'openrouter':
|
|
1433
|
+
newSettings.openrouter = {
|
|
1434
|
+
...settings.openrouter,
|
|
1435
|
+
model: result.model!.key,
|
|
1436
|
+
};
|
|
1437
|
+
break;
|
|
1438
|
+
|
|
1439
|
+
case 'anthropic':
|
|
1440
|
+
case 'bedrock':
|
|
1441
|
+
default:
|
|
1442
|
+
newSettings.modelKey = result.model!.key as ModelKey;
|
|
1443
|
+
break;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
LLMProviderFactory.saveSettings(newSettings);
|
|
1447
|
+
LLMProviderFactory.clearCache();
|
|
1448
|
+
|
|
1449
|
+
await adapter.sendMessage({
|
|
1450
|
+
chatId: message.chatId,
|
|
1451
|
+
text: `✅ Model changed to: *${result.model!.displayName}*`,
|
|
1452
|
+
parseMode: 'markdown',
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Handle /provider command - show or change current provider
|
|
1458
|
+
*/
|
|
1459
|
+
private async handleProviderCommand(
|
|
1460
|
+
adapter: ChannelAdapter,
|
|
1461
|
+
message: IncomingMessage,
|
|
1462
|
+
args: string[]
|
|
1463
|
+
): Promise<void> {
|
|
1464
|
+
const status = LLMProviderFactory.getConfigStatus();
|
|
1465
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
1466
|
+
|
|
1467
|
+
// If no args, show current provider and available options
|
|
1468
|
+
if (args.length === 0) {
|
|
1469
|
+
const currentProvider = status.providers.find(p => p.type === status.currentProvider);
|
|
1470
|
+
|
|
1471
|
+
let text = '🔌 *Current Provider*\n\n';
|
|
1472
|
+
text += `• Provider: ${currentProvider?.name || status.currentProvider}\n`;
|
|
1473
|
+
|
|
1474
|
+
// Show current model for context
|
|
1475
|
+
if (status.currentProvider === 'ollama') {
|
|
1476
|
+
text += `• Model: ${settings.ollama?.model || 'gpt-oss:20b'}\n\n`;
|
|
1477
|
+
} else {
|
|
1478
|
+
const currentModel = status.models.find(m => m.key === status.currentModel);
|
|
1479
|
+
text += `• Model: ${currentModel?.displayName || status.currentModel}\n\n`;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
text += '*Available Providers:*\n';
|
|
1483
|
+
text += '1. anthropic - Anthropic API (direct)\n';
|
|
1484
|
+
text += '2. openai - OpenAI/ChatGPT\n';
|
|
1485
|
+
text += '3. gemini - Google Gemini\n';
|
|
1486
|
+
text += '4. openrouter - OpenRouter\n';
|
|
1487
|
+
text += '5. bedrock - AWS Bedrock\n';
|
|
1488
|
+
text += '6. ollama - Ollama (local)\n\n';
|
|
1489
|
+
|
|
1490
|
+
text += '💡 Use `/provider <name>` to switch\n';
|
|
1491
|
+
text += 'Example: `/provider bedrock` or `/provider 2`';
|
|
1492
|
+
|
|
1493
|
+
await adapter.sendMessage({
|
|
1494
|
+
chatId: message.chatId,
|
|
1495
|
+
text,
|
|
1496
|
+
parseMode: 'markdown',
|
|
1497
|
+
});
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const selector = args[0].toLowerCase();
|
|
1502
|
+
|
|
1503
|
+
// Map of provider shortcuts
|
|
1504
|
+
const providerMap: Record<string, LLMProviderType> = {
|
|
1505
|
+
'1': 'anthropic',
|
|
1506
|
+
'anthropic': 'anthropic',
|
|
1507
|
+
'api': 'anthropic',
|
|
1508
|
+
'2': 'openai',
|
|
1509
|
+
'openai': 'openai',
|
|
1510
|
+
'chatgpt': 'openai',
|
|
1511
|
+
'3': 'gemini',
|
|
1512
|
+
'gemini': 'gemini',
|
|
1513
|
+
'google': 'gemini',
|
|
1514
|
+
'4': 'openrouter',
|
|
1515
|
+
'openrouter': 'openrouter',
|
|
1516
|
+
'or': 'openrouter',
|
|
1517
|
+
'5': 'bedrock',
|
|
1518
|
+
'bedrock': 'bedrock',
|
|
1519
|
+
'aws': 'bedrock',
|
|
1520
|
+
'6': 'ollama',
|
|
1521
|
+
'ollama': 'ollama',
|
|
1522
|
+
'local': 'ollama',
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
const targetProvider = providerMap[selector];
|
|
1526
|
+
if (!targetProvider) {
|
|
1527
|
+
await adapter.sendMessage({
|
|
1528
|
+
chatId: message.chatId,
|
|
1529
|
+
text: `❌ Unknown provider: "${args[0]}"\n\n*Available providers:*\n1. anthropic\n2. openai\n3. gemini\n4. openrouter\n5. bedrock\n6. ollama\n\nUse \`/provider <name>\` or \`/provider <number>\``,
|
|
1530
|
+
parseMode: 'markdown',
|
|
1531
|
+
});
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Update provider
|
|
1536
|
+
const newSettings: LLMSettings = {
|
|
1537
|
+
...settings,
|
|
1538
|
+
providerType: targetProvider,
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
LLMProviderFactory.saveSettings(newSettings);
|
|
1542
|
+
LLMProviderFactory.clearCache();
|
|
1543
|
+
|
|
1544
|
+
// Get provider display info
|
|
1545
|
+
const providerInfo = status.providers.find(p => p.type === targetProvider);
|
|
1546
|
+
let modelInfo: string;
|
|
1547
|
+
|
|
1548
|
+
if (targetProvider === 'ollama') {
|
|
1549
|
+
modelInfo = settings.ollama?.model || 'gpt-oss:20b';
|
|
1550
|
+
} else {
|
|
1551
|
+
const model = status.models.find(m => m.key === settings.modelKey);
|
|
1552
|
+
modelInfo = model?.displayName || settings.modelKey;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
await adapter.sendMessage({
|
|
1556
|
+
chatId: message.chatId,
|
|
1557
|
+
text: `✅ Provider changed to: *${providerInfo?.name || targetProvider}*\n\nCurrent model: ${modelInfo}\n\nUse \`/model\` to see available models for this provider.`,
|
|
1558
|
+
parseMode: 'markdown',
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Handle /shell command - enable or disable shell execution permission
|
|
1564
|
+
*/
|
|
1565
|
+
private async handleShellCommand(
|
|
1566
|
+
adapter: ChannelAdapter,
|
|
1567
|
+
message: IncomingMessage,
|
|
1568
|
+
sessionId: string,
|
|
1569
|
+
args: string[]
|
|
1570
|
+
): Promise<void> {
|
|
1571
|
+
let session = this.sessionRepo.findById(sessionId);
|
|
1572
|
+
|
|
1573
|
+
// Auto-assign temp workspace if none selected
|
|
1574
|
+
if (!session?.workspaceId) {
|
|
1575
|
+
const tempWorkspace = this.getOrCreateTempWorkspace();
|
|
1576
|
+
this.sessionRepo.update(sessionId, { workspaceId: tempWorkspace.id });
|
|
1577
|
+
session = this.sessionRepo.findById(sessionId);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const workspace = this.workspaceRepo.findById(session!.workspaceId!);
|
|
1581
|
+
if (!workspace) {
|
|
1582
|
+
await adapter.sendMessage({
|
|
1583
|
+
chatId: message.chatId,
|
|
1584
|
+
text: '❌ Workspace not found.',
|
|
1585
|
+
});
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// If no args, show current status
|
|
1590
|
+
if (args.length === 0) {
|
|
1591
|
+
const status = workspace.permissions.shell ? '🟢 Enabled' : '🔴 Disabled';
|
|
1592
|
+
await adapter.sendMessage({
|
|
1593
|
+
chatId: message.chatId,
|
|
1594
|
+
text: `🖥️ *Shell Commands*\n\nStatus: ${status}\n\nWhen enabled, the AI can execute shell commands like \`npm install\`, \`git\`, etc. Each command requires your approval before running.\n\n*Usage:*\n• \`/shell on\` - Enable shell commands\n• \`/shell off\` - Disable shell commands`,
|
|
1595
|
+
parseMode: 'markdown',
|
|
1596
|
+
});
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const action = args[0].toLowerCase();
|
|
1601
|
+
let newShellPermission: boolean;
|
|
1602
|
+
|
|
1603
|
+
if (action === 'on' || action === 'enable' || action === '1' || action === 'true') {
|
|
1604
|
+
newShellPermission = true;
|
|
1605
|
+
} else if (action === 'off' || action === 'disable' || action === '0' || action === 'false') {
|
|
1606
|
+
newShellPermission = false;
|
|
1607
|
+
} else {
|
|
1608
|
+
await adapter.sendMessage({
|
|
1609
|
+
chatId: message.chatId,
|
|
1610
|
+
text: '❌ Invalid option. Use `/shell on` or `/shell off`',
|
|
1611
|
+
parseMode: 'markdown',
|
|
1612
|
+
});
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Update workspace permissions
|
|
1617
|
+
const updatedPermissions = {
|
|
1618
|
+
...workspace.permissions,
|
|
1619
|
+
shell: newShellPermission,
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
// Update in database
|
|
1623
|
+
this.workspaceRepo.updatePermissions(workspace.id, updatedPermissions);
|
|
1624
|
+
|
|
1625
|
+
const statusText = newShellPermission ? '🟢 enabled' : '🔴 disabled';
|
|
1626
|
+
const warning = newShellPermission
|
|
1627
|
+
? '\n\n⚠️ The AI will now ask for approval before running each command.'
|
|
1628
|
+
: '';
|
|
1629
|
+
|
|
1630
|
+
await adapter.sendMessage({
|
|
1631
|
+
chatId: message.chatId,
|
|
1632
|
+
text: `✅ Shell commands ${statusText} for workspace *${workspace.name}*${warning}`,
|
|
1633
|
+
parseMode: 'markdown',
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Helper to select an Ollama model from available models
|
|
1639
|
+
*/
|
|
1640
|
+
private async selectOllamaModel(
|
|
1641
|
+
selector: string,
|
|
1642
|
+
originalArgs: string[]
|
|
1643
|
+
): Promise<{ success: boolean; model?: string; error?: string }> {
|
|
1644
|
+
let ollamaModels: Array<{ name: string; size: number; modified: string }> = [];
|
|
1645
|
+
try {
|
|
1646
|
+
ollamaModels = await LLMProviderFactory.getOllamaModels();
|
|
1647
|
+
} catch {
|
|
1648
|
+
return {
|
|
1649
|
+
success: false,
|
|
1650
|
+
error: `❌ Could not fetch Ollama models. Is Ollama running?\n\nMake sure Ollama is running with \`ollama serve\``,
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
if (ollamaModels.length === 0) {
|
|
1655
|
+
return {
|
|
1656
|
+
success: false,
|
|
1657
|
+
error: `❌ No Ollama models found.\n\nRun \`ollama pull <model>\` to download a model first.`,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
let selectedModel: string | undefined;
|
|
1662
|
+
|
|
1663
|
+
// Try to find model by number
|
|
1664
|
+
const num = parseInt(selector, 10);
|
|
1665
|
+
if (!isNaN(num) && num > 0 && num <= ollamaModels.length) {
|
|
1666
|
+
selectedModel = ollamaModels[num - 1].name;
|
|
1667
|
+
} else {
|
|
1668
|
+
// Try to find by name (exact or partial match)
|
|
1669
|
+
const match = ollamaModels.find(
|
|
1670
|
+
m => m.name.toLowerCase() === selector ||
|
|
1671
|
+
m.name.toLowerCase().includes(selector)
|
|
1672
|
+
);
|
|
1673
|
+
if (match) {
|
|
1674
|
+
selectedModel = match.name;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
if (!selectedModel) {
|
|
1679
|
+
const modelList = ollamaModels.slice(0, 5).map((m, i) => `${i + 1}. ${m.name}`).join('\n');
|
|
1680
|
+
const moreText = ollamaModels.length > 5 ? `\n ... and ${ollamaModels.length - 5} more` : '';
|
|
1681
|
+
return {
|
|
1682
|
+
success: false,
|
|
1683
|
+
error: `❌ Model not found: "${originalArgs.join(' ')}"\n\n*Available Ollama models:*\n${modelList}${moreText}\n\nUse \`/model <name>\` or \`/model <number>\``,
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
return { success: true, model: selectedModel };
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Helper to select a Claude model from available models
|
|
1692
|
+
*/
|
|
1693
|
+
private selectClaudeModel(
|
|
1694
|
+
selector: string,
|
|
1695
|
+
models: Array<{ key: string; displayName: string }>
|
|
1696
|
+
): { success: boolean; model?: { key: string; displayName: string }; error?: string } {
|
|
1697
|
+
let selectedModel: { key: string; displayName: string } | undefined;
|
|
1698
|
+
|
|
1699
|
+
// Try to find model by number
|
|
1700
|
+
const num = parseInt(selector, 10);
|
|
1701
|
+
if (!isNaN(num) && num > 0 && num <= models.length) {
|
|
1702
|
+
selectedModel = models[num - 1];
|
|
1703
|
+
} else {
|
|
1704
|
+
// Try to find by name (partial match)
|
|
1705
|
+
selectedModel = models.find(
|
|
1706
|
+
m => m.key.toLowerCase() === selector ||
|
|
1707
|
+
m.key.toLowerCase().includes(selector) ||
|
|
1708
|
+
m.displayName.toLowerCase().includes(selector)
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (!selectedModel) {
|
|
1713
|
+
return {
|
|
1714
|
+
success: false,
|
|
1715
|
+
error: `❌ Model not found: "${selector}"\n\nUse /models to see available options.`,
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
return { success: true, model: selectedModel };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Check if text looks like a pairing code
|
|
1724
|
+
*/
|
|
1725
|
+
private looksLikePairingCode(text: string): boolean {
|
|
1726
|
+
// Pairing codes are typically 6-8 alphanumeric characters
|
|
1727
|
+
return /^[A-Z0-9]{6,8}$/i.test(text);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Handle pairing code attempt
|
|
1732
|
+
*/
|
|
1733
|
+
private async handlePairingAttempt(
|
|
1734
|
+
adapter: ChannelAdapter,
|
|
1735
|
+
message: IncomingMessage,
|
|
1736
|
+
code: string
|
|
1737
|
+
): Promise<void> {
|
|
1738
|
+
const channel = this.channelRepo.findByType(adapter.type);
|
|
1739
|
+
if (!channel) return;
|
|
1740
|
+
|
|
1741
|
+
const result = await this.securityManager.verifyPairingCode(channel, message.userId, code);
|
|
1742
|
+
|
|
1743
|
+
if (result.success) {
|
|
1744
|
+
await adapter.sendMessage({
|
|
1745
|
+
chatId: message.chatId,
|
|
1746
|
+
text: '✅ Pairing successful! You can now use the bot.',
|
|
1747
|
+
replyTo: message.messageId,
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
this.emitEvent({
|
|
1751
|
+
type: 'user:paired',
|
|
1752
|
+
channel: adapter.type,
|
|
1753
|
+
timestamp: new Date(),
|
|
1754
|
+
data: { userId: message.userId, userName: message.userName },
|
|
1755
|
+
});
|
|
1756
|
+
} else {
|
|
1757
|
+
await adapter.sendMessage({
|
|
1758
|
+
chatId: message.chatId,
|
|
1759
|
+
text: `❌ ${result.error || 'Invalid pairing code. Please try again.'}`,
|
|
1760
|
+
replyTo: message.messageId,
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Forward message to desktop app / create task
|
|
1767
|
+
*/
|
|
1768
|
+
private async forwardToDesktopApp(
|
|
1769
|
+
adapter: ChannelAdapter,
|
|
1770
|
+
message: IncomingMessage,
|
|
1771
|
+
sessionId: string
|
|
1772
|
+
): Promise<void> {
|
|
1773
|
+
let session = this.sessionRepo.findById(sessionId);
|
|
1774
|
+
|
|
1775
|
+
// Auto-assign temp workspace if none selected
|
|
1776
|
+
if (!session?.workspaceId) {
|
|
1777
|
+
const tempWorkspace = this.getOrCreateTempWorkspace();
|
|
1778
|
+
this.sessionManager.setSessionWorkspace(sessionId, tempWorkspace.id);
|
|
1779
|
+
session = this.sessionRepo.findById(sessionId);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// Check if there's an existing task for this session (active or completed)
|
|
1783
|
+
if (session!.taskId) {
|
|
1784
|
+
const existingTask = this.taskRepo.findById(session!.taskId);
|
|
1785
|
+
if (existingTask) {
|
|
1786
|
+
// For active tasks, send follow-up message
|
|
1787
|
+
// For completed tasks, also allow follow-up (continues the conversation)
|
|
1788
|
+
const activeStatuses = ['pending', 'planning', 'executing', 'paused'];
|
|
1789
|
+
const isActive = activeStatuses.includes(existingTask.status);
|
|
1790
|
+
const isCompleted = existingTask.status === 'completed';
|
|
1791
|
+
|
|
1792
|
+
if (isActive || isCompleted) {
|
|
1793
|
+
if (this.agentDaemon) {
|
|
1794
|
+
try {
|
|
1795
|
+
const statusMsg = isActive
|
|
1796
|
+
? '💬 Sending follow-up message...'
|
|
1797
|
+
: '💬 Continuing conversation...';
|
|
1798
|
+
await adapter.sendMessage({
|
|
1799
|
+
chatId: message.chatId,
|
|
1800
|
+
text: statusMsg,
|
|
1801
|
+
replyTo: message.messageId,
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
// Re-register task for response tracking (may have been removed after initial completion)
|
|
1805
|
+
this.pendingTaskResponses.set(session!.taskId!, {
|
|
1806
|
+
adapter,
|
|
1807
|
+
chatId: message.chatId,
|
|
1808
|
+
sessionId,
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
await this.agentDaemon.sendMessage(session!.taskId!, message.text);
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
console.error('Error sending follow-up message:', error);
|
|
1814
|
+
await adapter.sendMessage({
|
|
1815
|
+
chatId: message.chatId,
|
|
1816
|
+
text: '❌ Failed to send message. Use /newtask to start a new task.',
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
// Task is in failed/cancelled state - unlink and create new task
|
|
1823
|
+
this.sessionManager.unlinkSessionFromTask(sessionId);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Create a new task
|
|
1828
|
+
if (!this.agentDaemon) {
|
|
1829
|
+
await adapter.sendMessage({
|
|
1830
|
+
chatId: message.chatId,
|
|
1831
|
+
text: '❌ Agent not available. Please try again later.',
|
|
1832
|
+
replyTo: message.messageId,
|
|
1833
|
+
});
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// Get workspace
|
|
1838
|
+
const workspace = this.workspaceRepo.findById(session!.workspaceId!);
|
|
1839
|
+
if (!workspace) {
|
|
1840
|
+
await adapter.sendMessage({
|
|
1841
|
+
chatId: message.chatId,
|
|
1842
|
+
text: '❌ Workspace not found. Please select a workspace with /workspace.',
|
|
1843
|
+
replyTo: message.messageId,
|
|
1844
|
+
});
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Create task
|
|
1849
|
+
const taskTitle = message.text.length > 50
|
|
1850
|
+
? message.text.substring(0, 50) + '...'
|
|
1851
|
+
: message.text;
|
|
1852
|
+
|
|
1853
|
+
const task = this.taskRepo.create({
|
|
1854
|
+
workspaceId: workspace.id,
|
|
1855
|
+
title: taskTitle,
|
|
1856
|
+
prompt: message.text,
|
|
1857
|
+
status: 'pending',
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// Link session to task
|
|
1861
|
+
this.sessionManager.linkSessionToTask(sessionId, task.id);
|
|
1862
|
+
|
|
1863
|
+
// Track this task for response handling
|
|
1864
|
+
this.pendingTaskResponses.set(task.id, {
|
|
1865
|
+
adapter,
|
|
1866
|
+
chatId: message.chatId,
|
|
1867
|
+
sessionId,
|
|
1868
|
+
originalMessageId: message.messageId, // Track for reaction updates
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
// Start draft streaming for real-time response preview (Telegram)
|
|
1872
|
+
if (adapter instanceof TelegramAdapter) {
|
|
1873
|
+
await adapter.startDraftStream(message.chatId);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// Send acknowledgment - concise for WhatsApp and iMessage
|
|
1877
|
+
const ackMessage = (adapter.type === 'whatsapp' || adapter.type === 'imessage')
|
|
1878
|
+
? `⏳ Working on it...`
|
|
1879
|
+
: `🚀 Task Started: "${taskTitle}"\n\nI'll notify you when it's complete or if I need your input.`;
|
|
1880
|
+
await adapter.sendMessage({
|
|
1881
|
+
chatId: message.chatId,
|
|
1882
|
+
text: ackMessage,
|
|
1883
|
+
replyTo: message.messageId,
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
// Notify desktop app via IPC
|
|
1887
|
+
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
1888
|
+
this.mainWindow.webContents.send('gateway:message', {
|
|
1889
|
+
channel: adapter.type,
|
|
1890
|
+
sessionId,
|
|
1891
|
+
taskId: task.id,
|
|
1892
|
+
message: {
|
|
1893
|
+
id: message.messageId,
|
|
1894
|
+
userId: message.userId,
|
|
1895
|
+
userName: message.userName,
|
|
1896
|
+
chatId: message.chatId,
|
|
1897
|
+
text: message.text,
|
|
1898
|
+
timestamp: message.timestamp.getTime(),
|
|
1899
|
+
},
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Start task execution
|
|
1904
|
+
try {
|
|
1905
|
+
await this.agentDaemon.startTask(task);
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
console.error('Error starting task:', error);
|
|
1908
|
+
await adapter.sendMessage({
|
|
1909
|
+
chatId: message.chatId,
|
|
1910
|
+
text: `❌ Failed to start task: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// Cleanup
|
|
1914
|
+
this.pendingTaskResponses.delete(task.id);
|
|
1915
|
+
this.sessionManager.unlinkSessionFromTask(sessionId);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* Send task update to channel
|
|
1921
|
+
* Uses draft streaming for Telegram to show real-time progress
|
|
1922
|
+
*/
|
|
1923
|
+
async sendTaskUpdate(taskId: string, text: string, isStreaming = false): Promise<void> {
|
|
1924
|
+
const pending = this.pendingTaskResponses.get(taskId);
|
|
1925
|
+
if (!pending) {
|
|
1926
|
+
// This is expected for tasks started from the UI (not via Telegram)
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
try {
|
|
1931
|
+
// Use draft streaming for Telegram when streaming content
|
|
1932
|
+
if (isStreaming && pending.adapter instanceof TelegramAdapter) {
|
|
1933
|
+
await pending.adapter.updateDraftStream(pending.chatId, text);
|
|
1934
|
+
} else {
|
|
1935
|
+
await pending.adapter.sendMessage({
|
|
1936
|
+
chatId: pending.chatId,
|
|
1937
|
+
text,
|
|
1938
|
+
parseMode: 'markdown',
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
console.error('Error sending task update:', error);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/**
|
|
1947
|
+
* Send typing indicator to channel
|
|
1948
|
+
*/
|
|
1949
|
+
async sendTypingIndicator(taskId: string): Promise<void> {
|
|
1950
|
+
const pending = this.pendingTaskResponses.get(taskId);
|
|
1951
|
+
if (!pending) return;
|
|
1952
|
+
|
|
1953
|
+
if (pending.adapter instanceof TelegramAdapter) {
|
|
1954
|
+
await pending.adapter.sendTyping(pending.chatId);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/**
|
|
1959
|
+
* Send any artifacts (images, documents) created during task execution
|
|
1960
|
+
* Called when follow-ups complete to deliver screenshots, etc.
|
|
1961
|
+
*/
|
|
1962
|
+
async sendArtifacts(taskId: string): Promise<void> {
|
|
1963
|
+
const pending = this.pendingTaskResponses.get(taskId);
|
|
1964
|
+
if (!pending) {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
await this.sendTaskArtifacts(taskId, pending.adapter, pending.chatId);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Handle task completion
|
|
1973
|
+
* Note: We keep the session linked to the task for follow-up messages
|
|
1974
|
+
*/
|
|
1975
|
+
async handleTaskCompletion(taskId: string, result?: string): Promise<void> {
|
|
1976
|
+
const pending = this.pendingTaskResponses.get(taskId);
|
|
1977
|
+
if (!pending) return;
|
|
1978
|
+
|
|
1979
|
+
try {
|
|
1980
|
+
// WhatsApp/iMessage-optimized completion message (no follow-up hint)
|
|
1981
|
+
const isSimpleMessaging = pending.adapter.type === 'whatsapp' || pending.adapter.type === 'imessage';
|
|
1982
|
+
const msgCtx = this.getMessageContext();
|
|
1983
|
+
const message = getCompletionMessage(msgCtx, result, !isSimpleMessaging);
|
|
1984
|
+
|
|
1985
|
+
// Finalize draft stream if using Telegram
|
|
1986
|
+
if (pending.adapter instanceof TelegramAdapter) {
|
|
1987
|
+
// Finalize the streaming draft with final message
|
|
1988
|
+
await pending.adapter.finalizeDraftStream(pending.chatId, message);
|
|
1989
|
+
|
|
1990
|
+
// Update reaction from 👀 to ✅ on the original message
|
|
1991
|
+
if (pending.originalMessageId) {
|
|
1992
|
+
await pending.adapter.sendCompletionReaction(pending.chatId, pending.originalMessageId);
|
|
1993
|
+
}
|
|
1994
|
+
} else {
|
|
1995
|
+
// Split long messages (Telegram has 4096 char limit, WhatsApp/iMessage ~65k but keep it reasonable)
|
|
1996
|
+
const maxLen = isSimpleMessaging ? 4000 : 4000;
|
|
1997
|
+
const chunks = this.splitMessage(message, maxLen);
|
|
1998
|
+
for (const chunk of chunks) {
|
|
1999
|
+
await pending.adapter.sendMessage({
|
|
2000
|
+
chatId: pending.chatId,
|
|
2001
|
+
text: chunk,
|
|
2002
|
+
parseMode: 'markdown',
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Send artifacts if any were created
|
|
2008
|
+
await this.sendTaskArtifacts(taskId, pending.adapter, pending.chatId);
|
|
2009
|
+
|
|
2010
|
+
// Don't unlink session - keep it linked for follow-up messages
|
|
2011
|
+
// User can use /newtask to explicitly start a new task
|
|
2012
|
+
} catch (error) {
|
|
2013
|
+
console.error('Error sending task completion:', error);
|
|
2014
|
+
} finally {
|
|
2015
|
+
this.pendingTaskResponses.delete(taskId);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Send task artifacts as documents/images to the channel
|
|
2021
|
+
*/
|
|
2022
|
+
private async sendTaskArtifacts(
|
|
2023
|
+
taskId: string,
|
|
2024
|
+
adapter: ChannelAdapter,
|
|
2025
|
+
chatId: string
|
|
2026
|
+
): Promise<void> {
|
|
2027
|
+
try {
|
|
2028
|
+
const artifacts = this.artifactRepo.findByTaskId(taskId);
|
|
2029
|
+
if (artifacts.length === 0) return;
|
|
2030
|
+
|
|
2031
|
+
// Image extensions
|
|
2032
|
+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
|
|
2033
|
+
|
|
2034
|
+
// Document extensions
|
|
2035
|
+
const documentExtensions = [
|
|
2036
|
+
'.docx', '.xlsx', '.pptx', '.pdf', '.doc', '.xls', '.ppt',
|
|
2037
|
+
'.txt', '.csv', '.json', '.md', '.html', '.xml'
|
|
2038
|
+
];
|
|
2039
|
+
|
|
2040
|
+
// Filter for sendable file types
|
|
2041
|
+
const sendableArtifacts = artifacts.filter(artifact => {
|
|
2042
|
+
const ext = path.extname(artifact.path).toLowerCase();
|
|
2043
|
+
return (imageExtensions.includes(ext) || documentExtensions.includes(ext)) && fs.existsSync(artifact.path);
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
if (sendableArtifacts.length === 0) return;
|
|
2047
|
+
|
|
2048
|
+
// Send each artifact
|
|
2049
|
+
for (const artifact of sendableArtifacts) {
|
|
2050
|
+
try {
|
|
2051
|
+
const ext = path.extname(artifact.path).toLowerCase();
|
|
2052
|
+
const fileName = path.basename(artifact.path);
|
|
2053
|
+
|
|
2054
|
+
if (imageExtensions.includes(ext) && adapter.sendPhoto) {
|
|
2055
|
+
// Send as photo for better display
|
|
2056
|
+
await adapter.sendPhoto(chatId, artifact.path, `📷 ${fileName}`);
|
|
2057
|
+
console.log(`Sent image: ${fileName}`);
|
|
2058
|
+
} else if (adapter.sendDocument) {
|
|
2059
|
+
// Send as document
|
|
2060
|
+
await adapter.sendDocument(chatId, artifact.path, `📎 ${fileName}`);
|
|
2061
|
+
console.log(`Sent document: ${fileName}`);
|
|
2062
|
+
} else {
|
|
2063
|
+
console.log(`Adapter does not support sending ${ext} files, skipping: ${fileName}`);
|
|
2064
|
+
}
|
|
2065
|
+
} catch (err) {
|
|
2066
|
+
console.error(`Failed to send artifact ${artifact.path}:`, err);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
console.error('Error sending task artifacts:', error);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/**
|
|
2075
|
+
* Handle task failure
|
|
2076
|
+
*/
|
|
2077
|
+
async handleTaskFailure(taskId: string, error: string): Promise<void> {
|
|
2078
|
+
const pending = this.pendingTaskResponses.get(taskId);
|
|
2079
|
+
if (!pending) return;
|
|
2080
|
+
|
|
2081
|
+
try {
|
|
2082
|
+
// Cancel any draft stream
|
|
2083
|
+
if (pending.adapter instanceof TelegramAdapter) {
|
|
2084
|
+
await pending.adapter.cancelDraftStream(pending.chatId);
|
|
2085
|
+
|
|
2086
|
+
// Remove ACK reaction on failure
|
|
2087
|
+
if (pending.originalMessageId) {
|
|
2088
|
+
await pending.adapter.removeAckReaction(pending.chatId, pending.originalMessageId);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const message = getChannelMessage('taskFailed', this.getMessageContext(), { error });
|
|
2093
|
+
await pending.adapter.sendMessage({
|
|
2094
|
+
chatId: pending.chatId,
|
|
2095
|
+
text: message,
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
// Unlink session from task
|
|
2099
|
+
this.sessionManager.unlinkSessionFromTask(pending.sessionId);
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
console.error('Error sending task failure:', err);
|
|
2102
|
+
} finally {
|
|
2103
|
+
this.pendingTaskResponses.delete(taskId);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
/**
|
|
2108
|
+
* Send approval request to Discord/Telegram
|
|
2109
|
+
*/
|
|
2110
|
+
async sendApprovalRequest(taskId: string, approval: any): Promise<void> {
|
|
2111
|
+
const pending = this.pendingTaskResponses.get(taskId);
|
|
2112
|
+
if (!pending) return;
|
|
2113
|
+
|
|
2114
|
+
// Store approval for response handling
|
|
2115
|
+
this.pendingApprovals.set(approval.id, {
|
|
2116
|
+
taskId,
|
|
2117
|
+
approval,
|
|
2118
|
+
sessionId: pending.sessionId,
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
// Format approval message
|
|
2122
|
+
let message = `🔐 *Approval Required*\n\n`;
|
|
2123
|
+
message += `**${approval.description}**\n\n`;
|
|
2124
|
+
|
|
2125
|
+
if (approval.type === 'run_command' && approval.details?.command) {
|
|
2126
|
+
message += `\`\`\`\n${approval.details.command}\n\`\`\`\n\n`;
|
|
2127
|
+
} else if (approval.details) {
|
|
2128
|
+
message += `Details: ${JSON.stringify(approval.details, null, 2)}\n\n`;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
message += `⏳ _Expires in 5 minutes_`;
|
|
2132
|
+
|
|
2133
|
+
// WhatsApp/iMessage don't support inline keyboards - use text commands
|
|
2134
|
+
if (pending.adapter.type === 'whatsapp' || pending.adapter.type === 'imessage') {
|
|
2135
|
+
message += `\n\n━━━━━━━━━━━━━━━\nReply */approve* or */deny*`;
|
|
2136
|
+
|
|
2137
|
+
try {
|
|
2138
|
+
await pending.adapter.sendMessage({
|
|
2139
|
+
chatId: pending.chatId,
|
|
2140
|
+
text: message,
|
|
2141
|
+
parseMode: 'markdown',
|
|
2142
|
+
});
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
console.error('Error sending approval request:', error);
|
|
2145
|
+
}
|
|
2146
|
+
} else {
|
|
2147
|
+
// Create inline keyboard with Approve/Deny buttons for Telegram/Discord
|
|
2148
|
+
const keyboard: InlineKeyboardButton[][] = [
|
|
2149
|
+
[
|
|
2150
|
+
{ text: '✅ Approve', callbackData: 'approve:' + approval.id },
|
|
2151
|
+
{ text: '❌ Deny', callbackData: 'deny:' + approval.id },
|
|
2152
|
+
],
|
|
2153
|
+
];
|
|
2154
|
+
|
|
2155
|
+
try {
|
|
2156
|
+
await pending.adapter.sendMessage({
|
|
2157
|
+
chatId: pending.chatId,
|
|
2158
|
+
text: message,
|
|
2159
|
+
parseMode: 'markdown',
|
|
2160
|
+
inlineKeyboard: keyboard,
|
|
2161
|
+
});
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
console.error('Error sending approval request:', error);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/**
|
|
2169
|
+
* Handle /approve command
|
|
2170
|
+
*/
|
|
2171
|
+
private async handleApproveCommand(
|
|
2172
|
+
adapter: ChannelAdapter,
|
|
2173
|
+
message: IncomingMessage,
|
|
2174
|
+
sessionId: string
|
|
2175
|
+
): Promise<void> {
|
|
2176
|
+
// Find pending approval for this session
|
|
2177
|
+
const approvalEntry = Array.from(this.pendingApprovals.entries())
|
|
2178
|
+
.find(([, data]) => data.sessionId === sessionId);
|
|
2179
|
+
|
|
2180
|
+
if (!approvalEntry) {
|
|
2181
|
+
await adapter.sendMessage({
|
|
2182
|
+
chatId: message.chatId,
|
|
2183
|
+
text: '❌ No pending approval request.',
|
|
2184
|
+
});
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
const [approvalId, data] = approvalEntry;
|
|
2189
|
+
this.pendingApprovals.delete(approvalId);
|
|
2190
|
+
|
|
2191
|
+
try {
|
|
2192
|
+
await this.agentDaemon?.respondToApproval(approvalId, true);
|
|
2193
|
+
await adapter.sendMessage({
|
|
2194
|
+
chatId: message.chatId,
|
|
2195
|
+
text: '✅ Approved! Executing...',
|
|
2196
|
+
});
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
console.error('Error responding to approval:', error);
|
|
2199
|
+
await adapter.sendMessage({
|
|
2200
|
+
chatId: message.chatId,
|
|
2201
|
+
text: '❌ Failed to process approval.',
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* Handle /deny command
|
|
2208
|
+
*/
|
|
2209
|
+
private async handleDenyCommand(
|
|
2210
|
+
adapter: ChannelAdapter,
|
|
2211
|
+
message: IncomingMessage,
|
|
2212
|
+
sessionId: string
|
|
2213
|
+
): Promise<void> {
|
|
2214
|
+
// Find pending approval for this session
|
|
2215
|
+
const approvalEntry = Array.from(this.pendingApprovals.entries())
|
|
2216
|
+
.find(([, data]) => data.sessionId === sessionId);
|
|
2217
|
+
|
|
2218
|
+
if (!approvalEntry) {
|
|
2219
|
+
await adapter.sendMessage({
|
|
2220
|
+
chatId: message.chatId,
|
|
2221
|
+
text: '❌ No pending approval request.',
|
|
2222
|
+
});
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
const [approvalId] = approvalEntry;
|
|
2227
|
+
this.pendingApprovals.delete(approvalId);
|
|
2228
|
+
|
|
2229
|
+
try {
|
|
2230
|
+
await this.agentDaemon?.respondToApproval(approvalId, false);
|
|
2231
|
+
await adapter.sendMessage({
|
|
2232
|
+
chatId: message.chatId,
|
|
2233
|
+
text: '🛑 Denied. Action cancelled.',
|
|
2234
|
+
});
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
console.error('Error responding to denial:', error);
|
|
2237
|
+
await adapter.sendMessage({
|
|
2238
|
+
chatId: message.chatId,
|
|
2239
|
+
text: '❌ Failed to process denial.',
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* Handle /queue command - view or clear task queue
|
|
2246
|
+
*/
|
|
2247
|
+
private async handleQueueCommand(
|
|
2248
|
+
adapter: ChannelAdapter,
|
|
2249
|
+
message: IncomingMessage,
|
|
2250
|
+
args: string[]
|
|
2251
|
+
): Promise<void> {
|
|
2252
|
+
if (!this.agentDaemon) {
|
|
2253
|
+
await adapter.sendMessage({
|
|
2254
|
+
chatId: message.chatId,
|
|
2255
|
+
text: '❌ Agent daemon not available.',
|
|
2256
|
+
});
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const subcommand = args[0]?.toLowerCase();
|
|
2261
|
+
|
|
2262
|
+
if (subcommand === 'clear' || subcommand === 'reset') {
|
|
2263
|
+
// Clear stuck tasks (also properly cancels running tasks to clean up browser sessions)
|
|
2264
|
+
const result = await this.agentDaemon.clearStuckTasks();
|
|
2265
|
+
await adapter.sendMessage({
|
|
2266
|
+
chatId: message.chatId,
|
|
2267
|
+
text: `✅ Queue cleared!\n\n• Running tasks cancelled: ${result.clearedRunning}\n• Queued tasks removed: ${result.clearedQueued}\n\nBrowser sessions and other resources have been cleaned up. You can now start new tasks.`,
|
|
2268
|
+
});
|
|
2269
|
+
} else {
|
|
2270
|
+
// Show queue status
|
|
2271
|
+
const status = this.agentDaemon.getQueueStatus();
|
|
2272
|
+
const statusText = `📊 *Queue Status*
|
|
2273
|
+
|
|
2274
|
+
• Running: ${status.runningCount}/${status.maxConcurrent}
|
|
2275
|
+
• Queued: ${status.queuedCount}
|
|
2276
|
+
|
|
2277
|
+
${status.runningCount > 0 ? `Running task IDs: ${status.runningTaskIds.join(', ')}` : ''}
|
|
2278
|
+
${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}` : ''}
|
|
2279
|
+
|
|
2280
|
+
*Commands:*
|
|
2281
|
+
• \`/queue\` - Show this status
|
|
2282
|
+
• \`/queue clear\` - Clear stuck tasks`;
|
|
2283
|
+
|
|
2284
|
+
await adapter.sendMessage({
|
|
2285
|
+
chatId: message.chatId,
|
|
2286
|
+
text: statusText,
|
|
2287
|
+
parseMode: 'markdown',
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Split a message into chunks for Telegram's character limit
|
|
2294
|
+
*/
|
|
2295
|
+
private splitMessage(text: string, maxLength: number): string[] {
|
|
2296
|
+
if (text.length <= maxLength) {
|
|
2297
|
+
return [text];
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
const chunks: string[] = [];
|
|
2301
|
+
let remaining = text;
|
|
2302
|
+
|
|
2303
|
+
while (remaining.length > 0) {
|
|
2304
|
+
if (remaining.length <= maxLength) {
|
|
2305
|
+
chunks.push(remaining);
|
|
2306
|
+
break;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// Try to split at newline
|
|
2310
|
+
let splitIndex = remaining.lastIndexOf('\n', maxLength);
|
|
2311
|
+
if (splitIndex === -1 || splitIndex < maxLength / 2) {
|
|
2312
|
+
// Try to split at space
|
|
2313
|
+
splitIndex = remaining.lastIndexOf(' ', maxLength);
|
|
2314
|
+
}
|
|
2315
|
+
if (splitIndex === -1 || splitIndex < maxLength / 2) {
|
|
2316
|
+
// Force split
|
|
2317
|
+
splitIndex = maxLength;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
chunks.push(remaining.substring(0, splitIndex));
|
|
2321
|
+
remaining = remaining.substring(splitIndex).trimStart();
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
return chunks;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* Handle cancel command
|
|
2329
|
+
*/
|
|
2330
|
+
private async handleCancelCommand(
|
|
2331
|
+
adapter: ChannelAdapter,
|
|
2332
|
+
message: IncomingMessage,
|
|
2333
|
+
sessionId: string
|
|
2334
|
+
): Promise<void> {
|
|
2335
|
+
const session = this.sessionRepo.findById(sessionId);
|
|
2336
|
+
|
|
2337
|
+
if (session?.taskId) {
|
|
2338
|
+
// Notify desktop app to cancel the task
|
|
2339
|
+
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
2340
|
+
this.mainWindow.webContents.send('gateway:cancel-task', {
|
|
2341
|
+
taskId: session.taskId,
|
|
2342
|
+
sessionId,
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// Update session state
|
|
2347
|
+
this.sessionRepo.update(sessionId, { state: 'idle', taskId: undefined });
|
|
2348
|
+
|
|
2349
|
+
await adapter.sendMessage({
|
|
2350
|
+
chatId: message.chatId,
|
|
2351
|
+
text: '🛑 Task cancelled.',
|
|
2352
|
+
});
|
|
2353
|
+
} else {
|
|
2354
|
+
await adapter.sendMessage({
|
|
2355
|
+
chatId: message.chatId,
|
|
2356
|
+
text: 'No active task to cancel.',
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/**
|
|
2362
|
+
* Handle newtask command - start a fresh task session
|
|
2363
|
+
*/
|
|
2364
|
+
private async handleNewTaskCommand(
|
|
2365
|
+
adapter: ChannelAdapter,
|
|
2366
|
+
message: IncomingMessage,
|
|
2367
|
+
sessionId: string
|
|
2368
|
+
): Promise<void> {
|
|
2369
|
+
const session = this.sessionRepo.findById(sessionId);
|
|
2370
|
+
|
|
2371
|
+
if (session?.taskId) {
|
|
2372
|
+
// Unlink current task from session
|
|
2373
|
+
this.sessionManager.unlinkSessionFromTask(sessionId);
|
|
2374
|
+
this.pendingTaskResponses.delete(session.taskId);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
await adapter.sendMessage({
|
|
2378
|
+
chatId: message.chatId,
|
|
2379
|
+
text: '🆕 Ready for a new task!\n\nSend me a message describing what you want to do.',
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Handle /removeworkspace command
|
|
2385
|
+
*/
|
|
2386
|
+
private async handleRemoveWorkspaceCommand(
|
|
2387
|
+
adapter: ChannelAdapter,
|
|
2388
|
+
message: IncomingMessage,
|
|
2389
|
+
sessionId: string,
|
|
2390
|
+
args: string[]
|
|
2391
|
+
): Promise<void> {
|
|
2392
|
+
if (args.length === 0) {
|
|
2393
|
+
await adapter.sendMessage({
|
|
2394
|
+
chatId: message.chatId,
|
|
2395
|
+
text: '❌ Please specify a workspace name to remove.\n\nUsage: `/removeworkspace <name>`',
|
|
2396
|
+
parseMode: 'markdown',
|
|
2397
|
+
});
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
const workspaceName = args.join(' ');
|
|
2402
|
+
const workspaces = this.workspaceRepo.findAll();
|
|
2403
|
+
const workspace = workspaces.find(
|
|
2404
|
+
(w) => w.name.toLowerCase() === workspaceName.toLowerCase()
|
|
2405
|
+
);
|
|
2406
|
+
|
|
2407
|
+
if (!workspace) {
|
|
2408
|
+
await adapter.sendMessage({
|
|
2409
|
+
chatId: message.chatId,
|
|
2410
|
+
text: `❌ Workspace "${workspaceName}" not found.\n\nUse /workspaces to see available workspaces.`,
|
|
2411
|
+
});
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// Check if this is the current workspace for the session
|
|
2416
|
+
const session = this.sessionRepo.findById(sessionId);
|
|
2417
|
+
if (session?.workspaceId === workspace.id) {
|
|
2418
|
+
// Clear the workspace from session
|
|
2419
|
+
this.sessionRepo.update(sessionId, { workspaceId: undefined });
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// Remove the workspace
|
|
2423
|
+
this.workspaceRepo.delete(workspace.id);
|
|
2424
|
+
|
|
2425
|
+
await adapter.sendMessage({
|
|
2426
|
+
chatId: message.chatId,
|
|
2427
|
+
text: `✅ Workspace "${workspace.name}" removed successfully.`,
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
/**
|
|
2432
|
+
* Handle /retry command - retry the last failed task
|
|
2433
|
+
*/
|
|
2434
|
+
private async handleRetryCommand(
|
|
2435
|
+
adapter: ChannelAdapter,
|
|
2436
|
+
message: IncomingMessage,
|
|
2437
|
+
sessionId: string
|
|
2438
|
+
): Promise<void> {
|
|
2439
|
+
let session = this.sessionRepo.findById(sessionId);
|
|
2440
|
+
|
|
2441
|
+
// Auto-assign temp workspace if none selected
|
|
2442
|
+
if (!session?.workspaceId) {
|
|
2443
|
+
const tempWorkspace = this.getOrCreateTempWorkspace();
|
|
2444
|
+
this.sessionRepo.update(sessionId, { workspaceId: tempWorkspace.id });
|
|
2445
|
+
session = this.sessionRepo.findById(sessionId);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// Find the last task for this session's workspace that failed or was cancelled
|
|
2449
|
+
const tasks = this.taskRepo.findByWorkspace(session!.workspaceId!);
|
|
2450
|
+
const lastFailedTask = tasks
|
|
2451
|
+
.filter((t: Task) => t.status === 'failed' || t.status === 'cancelled')
|
|
2452
|
+
.sort((a: Task, b: Task) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
|
|
2453
|
+
|
|
2454
|
+
if (!lastFailedTask) {
|
|
2455
|
+
await adapter.sendMessage({
|
|
2456
|
+
chatId: message.chatId,
|
|
2457
|
+
text: '❌ No failed task found to retry.\n\nStart a new task by sending a message.',
|
|
2458
|
+
});
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// Re-submit the task by sending the original prompt as a new message
|
|
2463
|
+
await adapter.sendMessage({
|
|
2464
|
+
chatId: message.chatId,
|
|
2465
|
+
text: `🔄 Retrying task...\n\nOriginal prompt: "${lastFailedTask.title}"`,
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
// Create a synthetic message with the original prompt
|
|
2469
|
+
const retryMessage: IncomingMessage = {
|
|
2470
|
+
...message,
|
|
2471
|
+
text: lastFailedTask.title,
|
|
2472
|
+
};
|
|
2473
|
+
|
|
2474
|
+
// Route as a regular task message
|
|
2475
|
+
await this.routeMessage(adapter, retryMessage, sessionId);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* Handle /history command - show recent task history
|
|
2480
|
+
*/
|
|
2481
|
+
private async handleHistoryCommand(
|
|
2482
|
+
adapter: ChannelAdapter,
|
|
2483
|
+
message: IncomingMessage,
|
|
2484
|
+
sessionId: string
|
|
2485
|
+
): Promise<void> {
|
|
2486
|
+
let session = this.sessionRepo.findById(sessionId);
|
|
2487
|
+
|
|
2488
|
+
// Auto-assign temp workspace if none selected
|
|
2489
|
+
if (!session?.workspaceId) {
|
|
2490
|
+
const tempWorkspace = this.getOrCreateTempWorkspace();
|
|
2491
|
+
this.sessionRepo.update(sessionId, { workspaceId: tempWorkspace.id });
|
|
2492
|
+
session = this.sessionRepo.findById(sessionId);
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
const tasks = this.taskRepo.findByWorkspace(session!.workspaceId!);
|
|
2496
|
+
const recentTasks = tasks
|
|
2497
|
+
.sort((a: Task, b: Task) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
2498
|
+
.slice(0, 10);
|
|
2499
|
+
|
|
2500
|
+
if (recentTasks.length === 0) {
|
|
2501
|
+
await adapter.sendMessage({
|
|
2502
|
+
chatId: message.chatId,
|
|
2503
|
+
text: '📋 No task history found.\n\nStart a new task by sending a message.',
|
|
2504
|
+
});
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
const statusEmoji: Record<string, string> = {
|
|
2509
|
+
completed: '✅',
|
|
2510
|
+
running: '⏳',
|
|
2511
|
+
pending: '⏸️',
|
|
2512
|
+
error: '❌',
|
|
2513
|
+
cancelled: '🚫',
|
|
2514
|
+
};
|
|
2515
|
+
|
|
2516
|
+
const historyText = recentTasks
|
|
2517
|
+
.map((t: Task, i: number) => {
|
|
2518
|
+
const emoji = statusEmoji[t.status] || '❓';
|
|
2519
|
+
const date = new Date(t.createdAt).toLocaleDateString();
|
|
2520
|
+
const title = t.title.length > 40 ? t.title.substring(0, 40) + '...' : t.title;
|
|
2521
|
+
return `${i + 1}. ${emoji} ${title}\n ${date} • ${t.status}`;
|
|
2522
|
+
})
|
|
2523
|
+
.join('\n\n');
|
|
2524
|
+
|
|
2525
|
+
await adapter.sendMessage({
|
|
2526
|
+
chatId: message.chatId,
|
|
2527
|
+
text: `📋 *Recent Tasks*\n\n${historyText}`,
|
|
2528
|
+
parseMode: 'markdown',
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
/**
|
|
2533
|
+
* Handle /skills command - list available skills
|
|
2534
|
+
*/
|
|
2535
|
+
private async handleSkillsCommand(
|
|
2536
|
+
adapter: ChannelAdapter,
|
|
2537
|
+
message: IncomingMessage,
|
|
2538
|
+
_sessionId: string
|
|
2539
|
+
): Promise<void> {
|
|
2540
|
+
try {
|
|
2541
|
+
const skillLoader = getCustomSkillLoader();
|
|
2542
|
+
await skillLoader.initialize();
|
|
2543
|
+
const skills = skillLoader.listTaskSkills();
|
|
2544
|
+
|
|
2545
|
+
if (skills.length === 0) {
|
|
2546
|
+
await adapter.sendMessage({
|
|
2547
|
+
chatId: message.chatId,
|
|
2548
|
+
text: '📚 No skills available.\n\nSkills are stored in:\n`~/Library/Application Support/cowork-os/skills/`',
|
|
2549
|
+
parseMode: 'markdown',
|
|
2550
|
+
});
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// Group skills by category
|
|
2555
|
+
const byCategory = new Map<string, typeof skills>();
|
|
2556
|
+
for (const skill of skills) {
|
|
2557
|
+
const category = skill.category || 'Uncategorized';
|
|
2558
|
+
if (!byCategory.has(category)) {
|
|
2559
|
+
byCategory.set(category, []);
|
|
2560
|
+
}
|
|
2561
|
+
byCategory.get(category)!.push(skill);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
let text = '📚 *Available Skills*\n\n';
|
|
2565
|
+
for (const [category, categorySkills] of byCategory) {
|
|
2566
|
+
text += `*${category}*\n`;
|
|
2567
|
+
for (const skill of categorySkills) {
|
|
2568
|
+
const status = skill.enabled !== false ? '✅' : '❌';
|
|
2569
|
+
text += `${skill.icon || '⚡'} ${skill.name} ${status}\n`;
|
|
2570
|
+
text += ` \`/skill ${skill.id}\` to toggle\n`;
|
|
2571
|
+
}
|
|
2572
|
+
text += '\n';
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
text += '_Use `/skill <name>` to toggle a skill on/off_';
|
|
2576
|
+
|
|
2577
|
+
await adapter.sendMessage({
|
|
2578
|
+
chatId: message.chatId,
|
|
2579
|
+
text,
|
|
2580
|
+
parseMode: 'markdown',
|
|
2581
|
+
});
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
await adapter.sendMessage({
|
|
2584
|
+
chatId: message.chatId,
|
|
2585
|
+
text: '❌ Failed to load skills.',
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* Handle /skill command - toggle a skill on/off
|
|
2592
|
+
*/
|
|
2593
|
+
private async handleSkillCommand(
|
|
2594
|
+
adapter: ChannelAdapter,
|
|
2595
|
+
message: IncomingMessage,
|
|
2596
|
+
_sessionId: string,
|
|
2597
|
+
args: string[]
|
|
2598
|
+
): Promise<void> {
|
|
2599
|
+
if (args.length === 0) {
|
|
2600
|
+
await adapter.sendMessage({
|
|
2601
|
+
chatId: message.chatId,
|
|
2602
|
+
text: '❌ Please specify a skill ID.\n\nUsage: `/skill <id>`\n\nUse /skills to see available skills.',
|
|
2603
|
+
parseMode: 'markdown',
|
|
2604
|
+
});
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
try {
|
|
2609
|
+
const skillLoader = getCustomSkillLoader();
|
|
2610
|
+
await skillLoader.initialize();
|
|
2611
|
+
const skillId = args[0].toLowerCase();
|
|
2612
|
+
const skill = skillLoader.getSkill(skillId);
|
|
2613
|
+
|
|
2614
|
+
if (!skill) {
|
|
2615
|
+
await adapter.sendMessage({
|
|
2616
|
+
chatId: message.chatId,
|
|
2617
|
+
text: `❌ Skill "${skillId}" not found.\n\nUse /skills to see available skills.`,
|
|
2618
|
+
});
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// Toggle the enabled state
|
|
2623
|
+
const newState = skill.enabled === false;
|
|
2624
|
+
await skillLoader.updateSkill(skillId, { enabled: newState });
|
|
2625
|
+
|
|
2626
|
+
const statusText = newState ? '✅ enabled' : '❌ disabled';
|
|
2627
|
+
await adapter.sendMessage({
|
|
2628
|
+
chatId: message.chatId,
|
|
2629
|
+
text: `${skill.icon || '⚡'} *${skill.name}* is now ${statusText}`,
|
|
2630
|
+
parseMode: 'markdown',
|
|
2631
|
+
});
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
await adapter.sendMessage({
|
|
2634
|
+
chatId: message.chatId,
|
|
2635
|
+
text: '❌ Failed to toggle skill.',
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/**
|
|
2641
|
+
* Handle /providers command - list available LLM providers
|
|
2642
|
+
*/
|
|
2643
|
+
private async handleProvidersCommand(
|
|
2644
|
+
adapter: ChannelAdapter,
|
|
2645
|
+
message: IncomingMessage
|
|
2646
|
+
): Promise<void> {
|
|
2647
|
+
const status = LLMProviderFactory.getConfigStatus();
|
|
2648
|
+
const current = status.currentProvider;
|
|
2649
|
+
|
|
2650
|
+
const providerEmoji: Record<string, string> = {
|
|
2651
|
+
anthropic: '🟠',
|
|
2652
|
+
openai: '🟢',
|
|
2653
|
+
gemini: '🔵',
|
|
2654
|
+
bedrock: '🟡',
|
|
2655
|
+
ollama: '⚪',
|
|
2656
|
+
openrouter: '🟣',
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
// Build inline keyboard with provider buttons
|
|
2660
|
+
const keyboard: InlineKeyboardButton[][] = [];
|
|
2661
|
+
const row1: InlineKeyboardButton[] = [];
|
|
2662
|
+
const row2: InlineKeyboardButton[] = [];
|
|
2663
|
+
|
|
2664
|
+
// Get configured providers for the keyboard
|
|
2665
|
+
const providerOrder: LLMProviderType[] = ['anthropic', 'openai', 'gemini', 'bedrock', 'openrouter', 'ollama'];
|
|
2666
|
+
|
|
2667
|
+
for (let i = 0; i < providerOrder.length; i++) {
|
|
2668
|
+
const provider = providerOrder[i];
|
|
2669
|
+
const emoji = providerEmoji[provider] || '⚡';
|
|
2670
|
+
const isCurrent = provider === current ? ' ✓' : '';
|
|
2671
|
+
const providerInfo = status.providers.find(p => p.type === provider);
|
|
2672
|
+
const name = providerInfo?.name || provider;
|
|
2673
|
+
|
|
2674
|
+
const button: InlineKeyboardButton = {
|
|
2675
|
+
text: `${emoji} ${name}${isCurrent}`,
|
|
2676
|
+
callbackData: `provider:${provider}`,
|
|
2677
|
+
};
|
|
2678
|
+
|
|
2679
|
+
// Split into two rows
|
|
2680
|
+
if (i < 3) {
|
|
2681
|
+
row1.push(button);
|
|
2682
|
+
} else {
|
|
2683
|
+
row2.push(button);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
keyboard.push(row1);
|
|
2688
|
+
keyboard.push(row2);
|
|
2689
|
+
|
|
2690
|
+
const currentProviderInfo = status.providers.find(p => p.type === current);
|
|
2691
|
+
|
|
2692
|
+
// WhatsApp/iMessage don't support inline keyboards - use text-based selection
|
|
2693
|
+
if (adapter.type === 'whatsapp' || adapter.type === 'imessage') {
|
|
2694
|
+
let text = `🤖 *AI Providers*\n\nCurrent: *${currentProviderInfo?.name || current}*\n\n`;
|
|
2695
|
+
providerOrder.forEach((provider, index) => {
|
|
2696
|
+
const emoji = providerEmoji[provider] || '⚡';
|
|
2697
|
+
const providerInfo = status.providers.find(p => p.type === provider);
|
|
2698
|
+
const name = providerInfo?.name || provider;
|
|
2699
|
+
const isCurrent = provider === current ? ' ✓' : '';
|
|
2700
|
+
text += `${index + 1}. ${emoji} *${name}*${isCurrent}\n`;
|
|
2701
|
+
});
|
|
2702
|
+
text += '\n━━━━━━━━━━━━━━━\n';
|
|
2703
|
+
text += 'Reply with number to switch.\nExample: `1` for Anthropic';
|
|
2704
|
+
|
|
2705
|
+
await adapter.sendMessage({
|
|
2706
|
+
chatId: message.chatId,
|
|
2707
|
+
text,
|
|
2708
|
+
parseMode: 'markdown',
|
|
2709
|
+
threadId: message.threadId,
|
|
2710
|
+
});
|
|
2711
|
+
} else {
|
|
2712
|
+
let text = `🤖 *AI Providers*\n\nCurrent: ${currentProviderInfo?.name || current}\n\nTap to switch:`;
|
|
2713
|
+
|
|
2714
|
+
await adapter.sendMessage({
|
|
2715
|
+
chatId: message.chatId,
|
|
2716
|
+
text,
|
|
2717
|
+
parseMode: 'markdown',
|
|
2718
|
+
inlineKeyboard: keyboard,
|
|
2719
|
+
threadId: message.threadId,
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
/**
|
|
2725
|
+
* Handle /settings command - view current settings
|
|
2726
|
+
*/
|
|
2727
|
+
private async handleSettingsCommand(
|
|
2728
|
+
adapter: ChannelAdapter,
|
|
2729
|
+
message: IncomingMessage,
|
|
2730
|
+
sessionId: string
|
|
2731
|
+
): Promise<void> {
|
|
2732
|
+
const session = this.sessionRepo.findById(sessionId);
|
|
2733
|
+
const workspace = session?.workspaceId
|
|
2734
|
+
? this.workspaceRepo.findById(session.workspaceId)
|
|
2735
|
+
: null;
|
|
2736
|
+
|
|
2737
|
+
const provider = LLMProviderFactory.getSelectedProvider();
|
|
2738
|
+
const model = LLMProviderFactory.getSelectedModel();
|
|
2739
|
+
const settings = LLMProviderFactory.getSettings();
|
|
2740
|
+
|
|
2741
|
+
let text = '⚙️ *Current Settings*\n\n';
|
|
2742
|
+
|
|
2743
|
+
text += '*Workspace*\n';
|
|
2744
|
+
text += workspace ? `📁 ${workspace.name}\n` : '❌ None selected\n';
|
|
2745
|
+
text += '\n';
|
|
2746
|
+
|
|
2747
|
+
text += '*AI Configuration*\n';
|
|
2748
|
+
text += `🤖 Provider: \`${provider}\`\n`;
|
|
2749
|
+
text += `🧠 Model: \`${model}\`\n`;
|
|
2750
|
+
text += '\n';
|
|
2751
|
+
|
|
2752
|
+
text += '*Session*\n';
|
|
2753
|
+
text += `🔧 Shell commands: ${session?.shellEnabled ? '✅' : '❌'}\n`;
|
|
2754
|
+
text += `📝 Debug mode: ${session?.debugMode ? '✅' : '❌'}\n`;
|
|
2755
|
+
|
|
2756
|
+
await adapter.sendMessage({
|
|
2757
|
+
chatId: message.chatId,
|
|
2758
|
+
text,
|
|
2759
|
+
parseMode: 'markdown',
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/**
|
|
2764
|
+
* Handle /debug command - toggle debug mode
|
|
2765
|
+
*/
|
|
2766
|
+
private async handleDebugCommand(
|
|
2767
|
+
adapter: ChannelAdapter,
|
|
2768
|
+
message: IncomingMessage,
|
|
2769
|
+
sessionId: string
|
|
2770
|
+
): Promise<void> {
|
|
2771
|
+
const session = this.sessionRepo.findById(sessionId);
|
|
2772
|
+
const currentDebug = session?.debugMode || false;
|
|
2773
|
+
const newDebug = !currentDebug;
|
|
2774
|
+
|
|
2775
|
+
this.sessionRepo.update(sessionId, { debugMode: newDebug });
|
|
2776
|
+
|
|
2777
|
+
const statusText = newDebug ? '✅ enabled' : '❌ disabled';
|
|
2778
|
+
await adapter.sendMessage({
|
|
2779
|
+
chatId: message.chatId,
|
|
2780
|
+
text: `🐛 Debug mode is now ${statusText}`,
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
/**
|
|
2785
|
+
* Handle /version command - show version info
|
|
2786
|
+
*/
|
|
2787
|
+
private async handleVersionCommand(
|
|
2788
|
+
adapter: ChannelAdapter,
|
|
2789
|
+
message: IncomingMessage
|
|
2790
|
+
): Promise<void> {
|
|
2791
|
+
const version = app.getVersion();
|
|
2792
|
+
const electronVersion = process.versions.electron;
|
|
2793
|
+
const nodeVersion = process.versions.node;
|
|
2794
|
+
const platform = process.platform;
|
|
2795
|
+
const arch = process.arch;
|
|
2796
|
+
|
|
2797
|
+
const text = `📦 *CoWork OS*
|
|
2798
|
+
|
|
2799
|
+
Version: \`${version}\`
|
|
2800
|
+
Platform: \`${platform}\` (${arch})
|
|
2801
|
+
Electron: \`${electronVersion}\`
|
|
2802
|
+
Node.js: \`${nodeVersion}\`
|
|
2803
|
+
|
|
2804
|
+
🔗 [GitHub](https://github.com/CoWork-OS/cowork-os)`;
|
|
2805
|
+
|
|
2806
|
+
await adapter.sendMessage({
|
|
2807
|
+
chatId: message.chatId,
|
|
2808
|
+
text,
|
|
2809
|
+
parseMode: 'markdown',
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2814
|
+
* Handle /start command with smart onboarding
|
|
2815
|
+
*/
|
|
2816
|
+
private async handleStartCommand(
|
|
2817
|
+
adapter: ChannelAdapter,
|
|
2818
|
+
message: IncomingMessage,
|
|
2819
|
+
sessionId: string
|
|
2820
|
+
): Promise<void> {
|
|
2821
|
+
const session = this.sessionRepo.findById(sessionId);
|
|
2822
|
+
const workspaces = this.workspaceRepo.findAll();
|
|
2823
|
+
|
|
2824
|
+
// WhatsApp/iMessage-optimized welcome flow (no inline keyboards)
|
|
2825
|
+
if (adapter.type === 'whatsapp' || adapter.type === 'imessage') {
|
|
2826
|
+
if (session?.workspaceId) {
|
|
2827
|
+
const workspace = this.workspaceRepo.findById(session.workspaceId);
|
|
2828
|
+
await adapter.sendMessage({
|
|
2829
|
+
chatId: message.chatId,
|
|
2830
|
+
text: `👋 *Welcome back!*\n\nWorkspace: *${workspace?.name || 'Unknown'}*\n\nJust send me what you'd like me to do.\n\nType /help for commands.`,
|
|
2831
|
+
parseMode: 'markdown',
|
|
2832
|
+
});
|
|
2833
|
+
} else if (workspaces.length === 0) {
|
|
2834
|
+
await adapter.sendMessage({
|
|
2835
|
+
chatId: message.chatId,
|
|
2836
|
+
text: `👋 *Welcome to CoWork!*\n\nI'm your AI coding assistant.\n\nFirst, add a workspace:\n\`/addworkspace /path/to/project\`\n\nOr add one from the desktop app.`,
|
|
2837
|
+
parseMode: 'markdown',
|
|
2838
|
+
});
|
|
2839
|
+
} else if (workspaces.length === 1) {
|
|
2840
|
+
// Auto-select the only workspace
|
|
2841
|
+
const workspace = workspaces[0];
|
|
2842
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
|
|
2843
|
+
await adapter.sendMessage({
|
|
2844
|
+
chatId: message.chatId,
|
|
2845
|
+
text: `👋 *Welcome to CoWork!*\n\n✅ Workspace: *${workspace.name}*\n\nJust tell me what you'd like me to do!\n\nExamples:\n• "Add dark mode support"\n• "Fix the login bug"\n• "Create a new API endpoint"`,
|
|
2846
|
+
parseMode: 'markdown',
|
|
2847
|
+
});
|
|
2848
|
+
} else {
|
|
2849
|
+
// Multiple workspaces - show selection
|
|
2850
|
+
let text = `👋 *Welcome to CoWork!*\n\nSelect a workspace to start:\n\n`;
|
|
2851
|
+
workspaces.forEach((ws, index) => {
|
|
2852
|
+
text += `${index + 1}. *${ws.name}*\n`;
|
|
2853
|
+
});
|
|
2854
|
+
text += `\nReply with a number (e.g., \`1\`)`;
|
|
2855
|
+
|
|
2856
|
+
await adapter.sendMessage({
|
|
2857
|
+
chatId: message.chatId,
|
|
2858
|
+
text,
|
|
2859
|
+
parseMode: 'markdown',
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// Standard welcome for Telegram/Discord
|
|
2866
|
+
await adapter.sendMessage({
|
|
2867
|
+
chatId: message.chatId,
|
|
2868
|
+
text: this.config.welcomeMessage!,
|
|
2869
|
+
});
|
|
2870
|
+
|
|
2871
|
+
// Show workspaces if none selected
|
|
2872
|
+
if (!session?.workspaceId && workspaces.length > 0) {
|
|
2873
|
+
await this.handleWorkspacesCommand(adapter, message);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
/**
|
|
2878
|
+
* Get help text - channel-specific for better UX
|
|
2879
|
+
*/
|
|
2880
|
+
private getHelpText(channelType?: ChannelType): string {
|
|
2881
|
+
// Compact help for WhatsApp (mobile-friendly)
|
|
2882
|
+
if (channelType === 'whatsapp') {
|
|
2883
|
+
return `📚 *Commands*
|
|
2884
|
+
|
|
2885
|
+
*Basics*
|
|
2886
|
+
/workspaces - Select workspace
|
|
2887
|
+
/status - Current status
|
|
2888
|
+
/newtask - Fresh start
|
|
2889
|
+
|
|
2890
|
+
*Tasks*
|
|
2891
|
+
/cancel - Stop task
|
|
2892
|
+
/approve or /yes - Approve action
|
|
2893
|
+
/deny or /no - Reject action
|
|
2894
|
+
|
|
2895
|
+
*Settings*
|
|
2896
|
+
/shell on|off - Shell access
|
|
2897
|
+
/models - Change AI model
|
|
2898
|
+
|
|
2899
|
+
━━━━━━━━━━━━━━━
|
|
2900
|
+
💡 Just send your task directly!
|
|
2901
|
+
Example: "Add a login form"`;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// Full help for other channels
|
|
2905
|
+
return `📚 *Available Commands*
|
|
2906
|
+
|
|
2907
|
+
*Core*
|
|
2908
|
+
/start - Start the bot
|
|
2909
|
+
/help - Show this help message
|
|
2910
|
+
/status - Check bot status and workspace
|
|
2911
|
+
/version - Show version information
|
|
2912
|
+
|
|
2913
|
+
*Workspaces*
|
|
2914
|
+
/workspaces - List available workspaces
|
|
2915
|
+
/workspace <name> - Select a workspace
|
|
2916
|
+
/addworkspace <path> - Add a new workspace
|
|
2917
|
+
/removeworkspace <name> - Remove a workspace
|
|
2918
|
+
|
|
2919
|
+
*Tasks*
|
|
2920
|
+
/newtask - Start a fresh task/conversation
|
|
2921
|
+
/cancel - Cancel current task
|
|
2922
|
+
/retry - Retry the last failed task
|
|
2923
|
+
/history - Show recent task history
|
|
2924
|
+
/approve - Approve pending action (or /yes, /y)
|
|
2925
|
+
/deny - Reject pending action (or /no, /n)
|
|
2926
|
+
/queue - View/clear task queue
|
|
2927
|
+
|
|
2928
|
+
*Models*
|
|
2929
|
+
/providers - List available AI providers
|
|
2930
|
+
/provider <name> - Show or change provider
|
|
2931
|
+
/models - List available AI models
|
|
2932
|
+
/model <name> - Show or change model
|
|
2933
|
+
|
|
2934
|
+
*Skills*
|
|
2935
|
+
/skills - List available skills
|
|
2936
|
+
/skill <name> - Toggle a skill on/off
|
|
2937
|
+
|
|
2938
|
+
*Settings*
|
|
2939
|
+
/settings - View current settings
|
|
2940
|
+
/shell - Enable/disable shell commands
|
|
2941
|
+
/debug - Toggle debug mode
|
|
2942
|
+
|
|
2943
|
+
💬 *Quick Start*
|
|
2944
|
+
1. \`/workspaces\` → \`/workspace <name>\`
|
|
2945
|
+
2. \`/shell on\` (if needed)
|
|
2946
|
+
3. Send your task message
|
|
2947
|
+
4. \`/newtask\` to start fresh`;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
/**
|
|
2951
|
+
* Handle callback query from inline keyboard button press
|
|
2952
|
+
*/
|
|
2953
|
+
private async handleCallbackQuery(adapter: ChannelAdapter, query: CallbackQuery): Promise<void> {
|
|
2954
|
+
const { data, chatId } = query;
|
|
2955
|
+
|
|
2956
|
+
// Parse callback data (format: action:param)
|
|
2957
|
+
const [action, ...params] = data.split(':');
|
|
2958
|
+
const param = params.join(':');
|
|
2959
|
+
|
|
2960
|
+
try {
|
|
2961
|
+
// Get or create session for this chat
|
|
2962
|
+
const channel = this.channelRepo.findByType(adapter.type);
|
|
2963
|
+
if (!channel) {
|
|
2964
|
+
console.error(`No channel configuration found for ${adapter.type}`);
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// Find existing session or create one
|
|
2969
|
+
let session = this.sessionRepo.findByChatId(channel.id, chatId);
|
|
2970
|
+
if (!session) {
|
|
2971
|
+
// Create a minimal session for handling callback
|
|
2972
|
+
session = this.sessionRepo.create({
|
|
2973
|
+
channelId: channel.id,
|
|
2974
|
+
chatId,
|
|
2975
|
+
state: 'idle',
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// Answer the callback to remove loading indicator
|
|
2980
|
+
if (adapter.answerCallbackQuery) {
|
|
2981
|
+
await adapter.answerCallbackQuery(query.id);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
switch (action) {
|
|
2985
|
+
case 'workspace':
|
|
2986
|
+
await this.handleWorkspaceCallback(adapter, query, session.id, param);
|
|
2987
|
+
break;
|
|
2988
|
+
|
|
2989
|
+
case 'provider':
|
|
2990
|
+
await this.handleProviderCallback(adapter, query, param);
|
|
2991
|
+
break;
|
|
2992
|
+
|
|
2993
|
+
case 'model':
|
|
2994
|
+
await this.handleModelCallback(adapter, query, param);
|
|
2995
|
+
break;
|
|
2996
|
+
|
|
2997
|
+
case 'approve':
|
|
2998
|
+
await this.handleApprovalCallback(adapter, query, session.id, true);
|
|
2999
|
+
break;
|
|
3000
|
+
|
|
3001
|
+
case 'deny':
|
|
3002
|
+
await this.handleApprovalCallback(adapter, query, session.id, false);
|
|
3003
|
+
break;
|
|
3004
|
+
|
|
3005
|
+
default:
|
|
3006
|
+
console.log(`Unknown callback action: ${action}`);
|
|
3007
|
+
}
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
console.error('Error handling callback query:', error);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* Handle workspace selection callback
|
|
3015
|
+
*/
|
|
3016
|
+
private async handleWorkspaceCallback(
|
|
3017
|
+
adapter: ChannelAdapter,
|
|
3018
|
+
query: CallbackQuery,
|
|
3019
|
+
sessionId: string,
|
|
3020
|
+
workspaceId: string
|
|
3021
|
+
): Promise<void> {
|
|
3022
|
+
const workspace = this.workspaceRepo.findById(workspaceId);
|
|
3023
|
+
if (!workspace) {
|
|
3024
|
+
await adapter.sendMessage({
|
|
3025
|
+
chatId: query.chatId,
|
|
3026
|
+
text: '❌ Workspace not found.',
|
|
3027
|
+
});
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// Update session workspace
|
|
3032
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
|
|
3033
|
+
|
|
3034
|
+
// Update the original message with the selection
|
|
3035
|
+
if (adapter.editMessageWithKeyboard) {
|
|
3036
|
+
await adapter.editMessageWithKeyboard(
|
|
3037
|
+
query.chatId,
|
|
3038
|
+
query.messageId,
|
|
3039
|
+
`✅ Workspace selected: *${workspace.name}*\n\`${workspace.path}\`\n\nYou can now send messages to create tasks.`
|
|
3040
|
+
);
|
|
3041
|
+
} else {
|
|
3042
|
+
await adapter.sendMessage({
|
|
3043
|
+
chatId: query.chatId,
|
|
3044
|
+
text: `✅ Workspace set to: *${workspace.name}*\n\`${workspace.path}\``,
|
|
3045
|
+
parseMode: 'markdown',
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
/**
|
|
3051
|
+
* Handle provider selection callback
|
|
3052
|
+
*/
|
|
3053
|
+
private async handleProviderCallback(
|
|
3054
|
+
adapter: ChannelAdapter,
|
|
3055
|
+
query: CallbackQuery,
|
|
3056
|
+
providerType: string
|
|
3057
|
+
): Promise<void> {
|
|
3058
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
3059
|
+
const status = LLMProviderFactory.getConfigStatus();
|
|
3060
|
+
|
|
3061
|
+
// Update provider
|
|
3062
|
+
const newSettings: LLMSettings = {
|
|
3063
|
+
...settings,
|
|
3064
|
+
providerType: providerType as LLMProviderType,
|
|
3065
|
+
};
|
|
3066
|
+
|
|
3067
|
+
LLMProviderFactory.saveSettings(newSettings);
|
|
3068
|
+
LLMProviderFactory.clearCache();
|
|
3069
|
+
|
|
3070
|
+
const providerInfo = status.providers.find(p => p.type === providerType);
|
|
3071
|
+
|
|
3072
|
+
// Update the original message
|
|
3073
|
+
if (adapter.editMessageWithKeyboard) {
|
|
3074
|
+
await adapter.editMessageWithKeyboard(
|
|
3075
|
+
query.chatId,
|
|
3076
|
+
query.messageId,
|
|
3077
|
+
`✅ Provider changed to: *${providerInfo?.name || providerType}*\n\nUse /models to see available models.`
|
|
3078
|
+
);
|
|
3079
|
+
} else {
|
|
3080
|
+
await adapter.sendMessage({
|
|
3081
|
+
chatId: query.chatId,
|
|
3082
|
+
text: `✅ Provider changed to: *${providerInfo?.name || providerType}*`,
|
|
3083
|
+
parseMode: 'markdown',
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
/**
|
|
3089
|
+
* Handle model selection callback
|
|
3090
|
+
*/
|
|
3091
|
+
private async handleModelCallback(
|
|
3092
|
+
adapter: ChannelAdapter,
|
|
3093
|
+
query: CallbackQuery,
|
|
3094
|
+
modelKey: string
|
|
3095
|
+
): Promise<void> {
|
|
3096
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
3097
|
+
const status = LLMProviderFactory.getConfigStatus();
|
|
3098
|
+
const providerType = status.currentProvider;
|
|
3099
|
+
|
|
3100
|
+
// Save to the appropriate provider-specific setting
|
|
3101
|
+
let newSettings: LLMSettings = { ...settings };
|
|
3102
|
+
let displayName = modelKey;
|
|
3103
|
+
|
|
3104
|
+
switch (providerType) {
|
|
3105
|
+
case 'openai':
|
|
3106
|
+
newSettings.openai = { ...settings.openai, model: modelKey };
|
|
3107
|
+
break;
|
|
3108
|
+
case 'gemini':
|
|
3109
|
+
newSettings.gemini = { ...settings.gemini, model: modelKey };
|
|
3110
|
+
break;
|
|
3111
|
+
case 'openrouter':
|
|
3112
|
+
newSettings.openrouter = { ...settings.openrouter, model: modelKey };
|
|
3113
|
+
break;
|
|
3114
|
+
case 'ollama':
|
|
3115
|
+
newSettings.ollama = { ...settings.ollama, model: modelKey };
|
|
3116
|
+
break;
|
|
3117
|
+
default:
|
|
3118
|
+
newSettings.modelKey = modelKey as ModelKey;
|
|
3119
|
+
const modelInfo = status.models.find(m => m.key === modelKey);
|
|
3120
|
+
displayName = modelInfo?.displayName || modelKey;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
LLMProviderFactory.saveSettings(newSettings);
|
|
3124
|
+
LLMProviderFactory.clearCache();
|
|
3125
|
+
|
|
3126
|
+
// Update the original message
|
|
3127
|
+
if (adapter.editMessageWithKeyboard) {
|
|
3128
|
+
await adapter.editMessageWithKeyboard(
|
|
3129
|
+
query.chatId,
|
|
3130
|
+
query.messageId,
|
|
3131
|
+
`✅ Model changed to: *${displayName}*`
|
|
3132
|
+
);
|
|
3133
|
+
} else {
|
|
3134
|
+
await adapter.sendMessage({
|
|
3135
|
+
chatId: query.chatId,
|
|
3136
|
+
text: `✅ Model changed to: *${displayName}*`,
|
|
3137
|
+
parseMode: 'markdown',
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
/**
|
|
3143
|
+
* Handle approval/deny callback from inline buttons
|
|
3144
|
+
*/
|
|
3145
|
+
private async handleApprovalCallback(
|
|
3146
|
+
adapter: ChannelAdapter,
|
|
3147
|
+
query: CallbackQuery,
|
|
3148
|
+
sessionId: string,
|
|
3149
|
+
approved: boolean
|
|
3150
|
+
): Promise<void> {
|
|
3151
|
+
// Find pending approval for this session
|
|
3152
|
+
const approvalEntry = Array.from(this.pendingApprovals.entries())
|
|
3153
|
+
.find(([, data]) => data.sessionId === sessionId);
|
|
3154
|
+
|
|
3155
|
+
if (!approvalEntry) {
|
|
3156
|
+
if (adapter.editMessageWithKeyboard) {
|
|
3157
|
+
await adapter.editMessageWithKeyboard(
|
|
3158
|
+
query.chatId,
|
|
3159
|
+
query.messageId,
|
|
3160
|
+
'❌ No pending approval request (may have expired).'
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
const [approvalId] = approvalEntry;
|
|
3167
|
+
this.pendingApprovals.delete(approvalId);
|
|
3168
|
+
|
|
3169
|
+
try {
|
|
3170
|
+
await this.agentDaemon?.respondToApproval(approvalId, approved);
|
|
3171
|
+
|
|
3172
|
+
const statusText = approved ? '✅ Approved! Executing...' : '🛑 Denied. Action cancelled.';
|
|
3173
|
+
if (adapter.editMessageWithKeyboard) {
|
|
3174
|
+
await adapter.editMessageWithKeyboard(
|
|
3175
|
+
query.chatId,
|
|
3176
|
+
query.messageId,
|
|
3177
|
+
statusText
|
|
3178
|
+
);
|
|
3179
|
+
} else {
|
|
3180
|
+
await adapter.sendMessage({
|
|
3181
|
+
chatId: query.chatId,
|
|
3182
|
+
text: statusText,
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
} catch (error) {
|
|
3186
|
+
console.error('Error responding to approval:', error);
|
|
3187
|
+
await adapter.sendMessage({
|
|
3188
|
+
chatId: query.chatId,
|
|
3189
|
+
text: '❌ Failed to process response.',
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
/**
|
|
3195
|
+
* Emit an event to all handlers
|
|
3196
|
+
*/
|
|
3197
|
+
private emitEvent(event: GatewayEvent): void {
|
|
3198
|
+
for (const handler of this.eventHandlers) {
|
|
3199
|
+
try {
|
|
3200
|
+
handler(event);
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
console.error('Error in event handler:', error);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
}
|