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,3661 @@
|
|
|
1
|
+
import { ipcMain, shell, BrowserWindow, app } from 'electron';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as fsSync from 'fs';
|
|
5
|
+
import mammoth from 'mammoth';
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
8
|
+
const pdfParseModule = require('pdf-parse');
|
|
9
|
+
// Handle both ESM default export and CommonJS module.exports
|
|
10
|
+
const pdfParse = (typeof pdfParseModule === 'function' ? pdfParseModule : pdfParseModule.default) as (dataBuffer: Buffer) => Promise<{
|
|
11
|
+
text: string;
|
|
12
|
+
numpages: number;
|
|
13
|
+
info: { Title?: string; Author?: string };
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
import { DatabaseManager } from '../database/schema';
|
|
17
|
+
import {
|
|
18
|
+
WorkspaceRepository,
|
|
19
|
+
TaskRepository,
|
|
20
|
+
TaskEventRepository,
|
|
21
|
+
ArtifactRepository,
|
|
22
|
+
SkillRepository,
|
|
23
|
+
LLMModelRepository,
|
|
24
|
+
} from '../database/repositories';
|
|
25
|
+
import { AgentRoleRepository } from '../agents/AgentRoleRepository';
|
|
26
|
+
import { ActivityRepository } from '../activity/ActivityRepository';
|
|
27
|
+
import { MentionRepository } from '../agents/MentionRepository';
|
|
28
|
+
import { TaskLabelRepository } from '../database/TaskLabelRepository';
|
|
29
|
+
import { WorkingStateRepository } from '../agents/WorkingStateRepository';
|
|
30
|
+
import { ContextPolicyManager } from '../gateway/context-policy';
|
|
31
|
+
import { IPC_CHANNELS, LLMSettingsData, AddChannelRequest, UpdateChannelRequest, SecurityMode, UpdateInfo, TEMP_WORKSPACE_ID, TEMP_WORKSPACE_NAME, Workspace, AgentRole, Task, BoardColumn, XSettingsData } from '../../shared/types';
|
|
32
|
+
import * as os from 'os';
|
|
33
|
+
import { AgentDaemon } from '../agent/daemon';
|
|
34
|
+
import { LLMProviderFactory, LLMProviderConfig, ModelKey, MODELS, GEMINI_MODELS, OPENROUTER_MODELS, OLLAMA_MODELS, OpenAIOAuth } from '../agent/llm';
|
|
35
|
+
import { SearchProviderFactory, SearchSettings, SearchProviderType } from '../agent/search';
|
|
36
|
+
import { ChannelGateway } from '../gateway';
|
|
37
|
+
import { updateManager } from '../updater';
|
|
38
|
+
import { rateLimiter, RATE_LIMIT_CONFIGS } from '../utils/rate-limiter';
|
|
39
|
+
import {
|
|
40
|
+
validateInput,
|
|
41
|
+
WorkspaceCreateSchema,
|
|
42
|
+
TaskCreateSchema,
|
|
43
|
+
TaskRenameSchema,
|
|
44
|
+
TaskMessageSchema,
|
|
45
|
+
ApprovalResponseSchema,
|
|
46
|
+
LLMSettingsSchema,
|
|
47
|
+
SearchSettingsSchema,
|
|
48
|
+
XSettingsSchema,
|
|
49
|
+
AddChannelSchema,
|
|
50
|
+
UpdateChannelSchema,
|
|
51
|
+
GrantAccessSchema,
|
|
52
|
+
RevokeAccessSchema,
|
|
53
|
+
GeneratePairingSchema,
|
|
54
|
+
GuardrailSettingsSchema,
|
|
55
|
+
UUIDSchema,
|
|
56
|
+
StringIdSchema,
|
|
57
|
+
} from '../utils/validation';
|
|
58
|
+
import { GuardrailManager } from '../guardrails/guardrail-manager';
|
|
59
|
+
import { AppearanceManager } from '../settings/appearance-manager';
|
|
60
|
+
import { PersonalityManager } from '../settings/personality-manager';
|
|
61
|
+
|
|
62
|
+
const normalizeMentionToken = (value: string): string =>
|
|
63
|
+
value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
64
|
+
|
|
65
|
+
const buildAgentMentionIndex = (roles: AgentRole[]) => {
|
|
66
|
+
const index = new Map<string, AgentRole>();
|
|
67
|
+
roles.forEach((role) => {
|
|
68
|
+
const baseTokens = [
|
|
69
|
+
role.name,
|
|
70
|
+
role.displayName,
|
|
71
|
+
role.name.replace(/[_-]+/g, ''),
|
|
72
|
+
role.displayName.replace(/\s+/g, ''),
|
|
73
|
+
role.displayName.replace(/\s+/g, '_'),
|
|
74
|
+
role.displayName.replace(/\s+/g, '-'),
|
|
75
|
+
];
|
|
76
|
+
baseTokens.forEach((token) => {
|
|
77
|
+
const normalized = normalizeMentionToken(token);
|
|
78
|
+
if (normalized) {
|
|
79
|
+
index.set(normalized, role);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
return index;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const CAPABILITY_KEYWORDS: Record<string, string[]> = {
|
|
87
|
+
code: ['code', 'implement', 'build', 'develop', 'feature', 'api', 'backend', 'frontend', 'refactor', 'bug', 'fix'],
|
|
88
|
+
review: ['review', 'audit', 'best practices', 'quality', 'lint'],
|
|
89
|
+
test: ['test', 'testing', 'qa', 'unit', 'integration', 'e2e', 'regression', 'coverage'],
|
|
90
|
+
design: ['design', 'ui', 'ux', 'wireframe', 'mockup', 'figma', 'layout', 'visual', 'brand'],
|
|
91
|
+
ops: ['deploy', 'ci', 'cd', 'devops', 'infra', 'infrastructure', 'docker', 'kubernetes', 'pipeline', 'monitor'],
|
|
92
|
+
security: ['security', 'vulnerability', 'threat', 'audit', 'compliance', 'encryption'],
|
|
93
|
+
research: ['research', 'investigate', 'compare', 'comparison', 'competitive', 'competitor', 'benchmark', 'study'],
|
|
94
|
+
analyze: ['analyze', 'analysis', 'data', 'metrics', 'insights', 'report', 'trend', 'dashboard'],
|
|
95
|
+
plan: ['plan', 'strategy', 'roadmap', 'architecture', 'outline', 'spec'],
|
|
96
|
+
document: ['document', 'documentation', 'docs', 'guide', 'manual', 'readme', 'spec'],
|
|
97
|
+
write: ['write', 'draft', 'copy', 'blog', 'post', 'article', 'content', 'summary'],
|
|
98
|
+
communicate: ['email', 'support', 'customer', 'communication', 'outreach', 'reply', 'respond'],
|
|
99
|
+
market: ['marketing', 'growth', 'campaign', 'social', 'seo', 'launch', 'newsletter', 'ads'],
|
|
100
|
+
manage: ['manage', 'project', 'timeline', 'milestone', 'coordination', 'sprint', 'backlog'],
|
|
101
|
+
product: ['product', 'feature', 'user story', 'requirements', 'prioritize', 'mvp'],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const scoreAgentForTask = (role: AgentRole, text: string) => {
|
|
105
|
+
const lowerText = text.toLowerCase();
|
|
106
|
+
let score = 0;
|
|
107
|
+
const roleText = `${role.name} ${role.displayName} ${role.description ?? ''}`.toLowerCase();
|
|
108
|
+
const tokens = roleText.split(/[^a-z0-9]+/).filter((token) => token.length > 2);
|
|
109
|
+
tokens.forEach((token) => {
|
|
110
|
+
if (lowerText.includes(token)) {
|
|
111
|
+
score += 1;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (role.capabilities) {
|
|
116
|
+
role.capabilities.forEach((capability) => {
|
|
117
|
+
const keywords = CAPABILITY_KEYWORDS[capability];
|
|
118
|
+
if (keywords && keywords.some((keyword) => lowerText.includes(keyword))) {
|
|
119
|
+
score += 3;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return score;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const selectBestAgentsForTask = (text: string, roles: AgentRole[]) => {
|
|
128
|
+
if (roles.length === 0) return roles;
|
|
129
|
+
const scored = roles
|
|
130
|
+
.map((role) => ({ role, score: scoreAgentForTask(role, text) }))
|
|
131
|
+
.sort((a, b) => {
|
|
132
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
133
|
+
return (a.role.sortOrder ?? 0) - (b.role.sortOrder ?? 0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const withScore = scored.filter((entry) => entry.score > 0);
|
|
137
|
+
if (withScore.length > 0) {
|
|
138
|
+
const maxScore = withScore[0].score;
|
|
139
|
+
const threshold = Math.max(1, maxScore - 2);
|
|
140
|
+
const selected = withScore
|
|
141
|
+
.filter((entry) => entry.score >= threshold)
|
|
142
|
+
.slice(0, 4)
|
|
143
|
+
.map((entry) => entry.role);
|
|
144
|
+
return selected.length > 0 ? selected : withScore.slice(0, 3).map((entry) => entry.role);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const leads = roles
|
|
148
|
+
.filter((role) => role.autonomyLevel === 'lead')
|
|
149
|
+
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
150
|
+
if (leads.length > 0) {
|
|
151
|
+
return leads.slice(0, 3);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return roles.slice(0, Math.min(3, roles.length));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const extractMentionedRoles = (
|
|
158
|
+
text: string,
|
|
159
|
+
roles: AgentRole[]
|
|
160
|
+
) => {
|
|
161
|
+
const normalizedText = text.toLowerCase();
|
|
162
|
+
const useSmartSelection = /\B@everybody\b/.test(normalizedText);
|
|
163
|
+
if (/\B@all\b/.test(normalizedText) || /\B@everyone\b/.test(normalizedText)) {
|
|
164
|
+
return roles;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const index = buildAgentMentionIndex(roles);
|
|
168
|
+
const matches = new Map<string, AgentRole>();
|
|
169
|
+
|
|
170
|
+
const regex = /@([a-zA-Z0-9][a-zA-Z0-9 _-]{0,50})/g;
|
|
171
|
+
let match: RegExpExecArray | null;
|
|
172
|
+
while ((match = regex.exec(text)) !== null) {
|
|
173
|
+
const raw = match[1].replace(/[.,:;!?)]*$/, '').trim();
|
|
174
|
+
const token = normalizeMentionToken(raw);
|
|
175
|
+
const role = index.get(token);
|
|
176
|
+
if (role) {
|
|
177
|
+
matches.set(role.id, role);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (matches.size > 0) {
|
|
182
|
+
if (useSmartSelection) {
|
|
183
|
+
const selected = selectBestAgentsForTask(text, roles);
|
|
184
|
+
const merged = new Map<string, AgentRole>();
|
|
185
|
+
selected.forEach((role) => merged.set(role.id, role));
|
|
186
|
+
matches.forEach((role) => merged.set(role.id, role));
|
|
187
|
+
return Array.from(merged.values());
|
|
188
|
+
}
|
|
189
|
+
return Array.from(matches.values());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const normalizedWithAt = text
|
|
193
|
+
.toLowerCase()
|
|
194
|
+
.replace(/[^a-z0-9@]/g, '');
|
|
195
|
+
|
|
196
|
+
index.forEach((role, token) => {
|
|
197
|
+
if (normalizedWithAt.includes(`@${token}`)) {
|
|
198
|
+
matches.set(role.id, role);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (useSmartSelection) {
|
|
203
|
+
return selectBestAgentsForTask(text, roles);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return Array.from(matches.values());
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const buildSoulSummary = (soul?: string): string | null => {
|
|
210
|
+
if (!soul) return null;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(soul) as Record<string, unknown>;
|
|
213
|
+
const parts: string[] = [];
|
|
214
|
+
if (typeof parsed.name === 'string') parts.push(`Name: ${parsed.name}`);
|
|
215
|
+
if (typeof parsed.role === 'string') parts.push(`Role: ${parsed.role}`);
|
|
216
|
+
if (typeof parsed.personality === 'string') parts.push(`Personality: ${parsed.personality}`);
|
|
217
|
+
if (typeof parsed.communicationStyle === 'string') parts.push(`Style: ${parsed.communicationStyle}`);
|
|
218
|
+
if (Array.isArray(parsed.focusAreas)) parts.push(`Focus: ${parsed.focusAreas.join(', ')}`);
|
|
219
|
+
if (Array.isArray(parsed.strengths)) parts.push(`Strengths: ${parsed.strengths.join(', ')}`);
|
|
220
|
+
if (parts.length === 0) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return parts.join('\n');
|
|
224
|
+
} catch {
|
|
225
|
+
return soul;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const buildAgentDispatchPrompt = (
|
|
230
|
+
role: {
|
|
231
|
+
displayName: string;
|
|
232
|
+
description?: string | null;
|
|
233
|
+
capabilities?: string[];
|
|
234
|
+
systemPrompt?: string | null;
|
|
235
|
+
soul?: string | null;
|
|
236
|
+
},
|
|
237
|
+
parentTask: { title: string; prompt: string }
|
|
238
|
+
) => {
|
|
239
|
+
const lines: string[] = [
|
|
240
|
+
`You are ${role.displayName}${role.description ? ` — ${role.description}` : ''}.`,
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
if (role.capabilities && role.capabilities.length > 0) {
|
|
244
|
+
lines.push(`Capabilities: ${role.capabilities.join(', ')}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (role.systemPrompt) {
|
|
248
|
+
lines.push('System guidance:');
|
|
249
|
+
lines.push(role.systemPrompt);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const soulSummary = buildSoulSummary(role.soul || undefined);
|
|
253
|
+
if (soulSummary) {
|
|
254
|
+
lines.push('Role notes:');
|
|
255
|
+
lines.push(soulSummary);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
lines.push('');
|
|
259
|
+
lines.push(`Parent task: ${parentTask.title}`);
|
|
260
|
+
lines.push('Request:');
|
|
261
|
+
lines.push(parentTask.prompt);
|
|
262
|
+
lines.push('');
|
|
263
|
+
lines.push('Deliverables:');
|
|
264
|
+
lines.push('- Provide a concise summary of your findings.');
|
|
265
|
+
lines.push('- Call out risks or open questions.');
|
|
266
|
+
lines.push('- Recommend next steps.');
|
|
267
|
+
|
|
268
|
+
return lines.join('\n');
|
|
269
|
+
};
|
|
270
|
+
import { XSettingsManager } from '../settings/x-manager';
|
|
271
|
+
import { testXConnection, checkBirdInstalled } from '../utils/x-cli';
|
|
272
|
+
import { getCustomSkillLoader } from '../agent/custom-skill-loader';
|
|
273
|
+
import { CustomSkill } from '../../shared/types';
|
|
274
|
+
import { MCPSettingsManager } from '../mcp/settings';
|
|
275
|
+
import { MCPClientManager } from '../mcp/client/MCPClientManager';
|
|
276
|
+
import { MCPRegistryManager } from '../mcp/registry/MCPRegistryManager';
|
|
277
|
+
import type { MCPSettings, MCPServerConfig } from '../mcp/types';
|
|
278
|
+
import { MCPHostServer } from '../mcp/host/MCPHostServer';
|
|
279
|
+
import { BuiltinToolsSettingsManager } from '../agent/tools/builtin-settings';
|
|
280
|
+
import {
|
|
281
|
+
MCPServerConfigSchema,
|
|
282
|
+
MCPServerUpdateSchema,
|
|
283
|
+
MCPSettingsSchema,
|
|
284
|
+
MCPRegistrySearchSchema,
|
|
285
|
+
HookMappingSchema,
|
|
286
|
+
} from '../utils/validation';
|
|
287
|
+
import { NotificationService } from '../notifications';
|
|
288
|
+
import type { NotificationType, HooksSettingsData, HookMappingData, GmailHooksSettingsData, HooksStatus } from '../../shared/types';
|
|
289
|
+
import {
|
|
290
|
+
HooksSettingsManager,
|
|
291
|
+
HooksServer,
|
|
292
|
+
startGmailWatcher,
|
|
293
|
+
stopGmailWatcher,
|
|
294
|
+
isGmailWatcherRunning,
|
|
295
|
+
isGogAvailable,
|
|
296
|
+
generateHookToken,
|
|
297
|
+
DEFAULT_HOOKS_PORT,
|
|
298
|
+
} from '../hooks';
|
|
299
|
+
import { MemoryService } from '../memory/MemoryService';
|
|
300
|
+
import type { MemorySettings } from '../database/repositories';
|
|
301
|
+
import { VoiceSettingsManager } from '../voice/voice-settings-manager';
|
|
302
|
+
import { getVoiceService } from '../voice/VoiceService';
|
|
303
|
+
|
|
304
|
+
// Global notification service instance
|
|
305
|
+
let notificationService: NotificationService | null = null;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get the notification service instance
|
|
309
|
+
*/
|
|
310
|
+
export function getNotificationService(): NotificationService | null {
|
|
311
|
+
return notificationService;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Helper to check rate limit and throw if exceeded
|
|
315
|
+
function checkRateLimit(channel: string, config: { maxRequests: number; windowMs: number } = RATE_LIMIT_CONFIGS.standard): void {
|
|
316
|
+
if (!rateLimiter.check(channel)) {
|
|
317
|
+
const resetMs = rateLimiter.getResetTime(channel);
|
|
318
|
+
const resetSec = Math.ceil(resetMs / 1000);
|
|
319
|
+
throw new Error(`Rate limit exceeded. Try again in ${resetSec} seconds.`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Configure rate limits for sensitive channels
|
|
324
|
+
rateLimiter.configure(IPC_CHANNELS.TASK_CREATE, RATE_LIMIT_CONFIGS.expensive);
|
|
325
|
+
rateLimiter.configure(IPC_CHANNELS.TASK_SEND_MESSAGE, RATE_LIMIT_CONFIGS.expensive);
|
|
326
|
+
rateLimiter.configure(IPC_CHANNELS.LLM_SAVE_SETTINGS, RATE_LIMIT_CONFIGS.limited);
|
|
327
|
+
rateLimiter.configure(IPC_CHANNELS.LLM_TEST_PROVIDER, RATE_LIMIT_CONFIGS.expensive);
|
|
328
|
+
rateLimiter.configure(IPC_CHANNELS.LLM_GET_OLLAMA_MODELS, RATE_LIMIT_CONFIGS.standard);
|
|
329
|
+
rateLimiter.configure(IPC_CHANNELS.LLM_GET_GEMINI_MODELS, RATE_LIMIT_CONFIGS.standard);
|
|
330
|
+
rateLimiter.configure(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS, RATE_LIMIT_CONFIGS.standard);
|
|
331
|
+
rateLimiter.configure(IPC_CHANNELS.LLM_GET_BEDROCK_MODELS, RATE_LIMIT_CONFIGS.standard);
|
|
332
|
+
rateLimiter.configure(IPC_CHANNELS.SEARCH_SAVE_SETTINGS, RATE_LIMIT_CONFIGS.limited);
|
|
333
|
+
rateLimiter.configure(IPC_CHANNELS.SEARCH_TEST_PROVIDER, RATE_LIMIT_CONFIGS.expensive);
|
|
334
|
+
rateLimiter.configure(IPC_CHANNELS.GATEWAY_ADD_CHANNEL, RATE_LIMIT_CONFIGS.limited);
|
|
335
|
+
rateLimiter.configure(IPC_CHANNELS.GATEWAY_TEST_CHANNEL, RATE_LIMIT_CONFIGS.expensive);
|
|
336
|
+
rateLimiter.configure(IPC_CHANNELS.GUARDRAIL_SAVE_SETTINGS, RATE_LIMIT_CONFIGS.limited);
|
|
337
|
+
|
|
338
|
+
// Helper function to get the main window
|
|
339
|
+
function getMainWindow(): BrowserWindow | null {
|
|
340
|
+
const windows = BrowserWindow.getAllWindows();
|
|
341
|
+
return windows.length > 0 ? windows[0] : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function setupIpcHandlers(
|
|
345
|
+
dbManager: DatabaseManager,
|
|
346
|
+
agentDaemon: AgentDaemon,
|
|
347
|
+
gateway?: ChannelGateway
|
|
348
|
+
) {
|
|
349
|
+
const db = dbManager.getDatabase();
|
|
350
|
+
const workspaceRepo = new WorkspaceRepository(db);
|
|
351
|
+
const taskRepo = new TaskRepository(db);
|
|
352
|
+
const taskEventRepo = new TaskEventRepository(db);
|
|
353
|
+
const artifactRepo = new ArtifactRepository(db);
|
|
354
|
+
const skillRepo = new SkillRepository(db);
|
|
355
|
+
const llmModelRepo = new LLMModelRepository(db);
|
|
356
|
+
const agentRoleRepo = new AgentRoleRepository(db);
|
|
357
|
+
const activityRepo = new ActivityRepository(db);
|
|
358
|
+
const mentionRepo = new MentionRepository(db);
|
|
359
|
+
const taskLabelRepo = new TaskLabelRepository(db);
|
|
360
|
+
const workingStateRepo = new WorkingStateRepository(db);
|
|
361
|
+
const contextPolicyManager = new ContextPolicyManager(db);
|
|
362
|
+
|
|
363
|
+
// Seed default agent roles if none exist
|
|
364
|
+
agentRoleRepo.seedDefaults();
|
|
365
|
+
|
|
366
|
+
// Helper to validate path is within workspace (prevent path traversal attacks)
|
|
367
|
+
const isPathWithinWorkspace = (filePath: string, workspacePath: string): boolean => {
|
|
368
|
+
const normalizedWorkspace = path.resolve(workspacePath);
|
|
369
|
+
const normalizedFile = path.resolve(normalizedWorkspace, filePath);
|
|
370
|
+
const relative = path.relative(normalizedWorkspace, normalizedFile);
|
|
371
|
+
// If relative path starts with '..' or is absolute, it's outside workspace
|
|
372
|
+
return !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Temp workspace management
|
|
376
|
+
// The temp workspace is created on-demand and stored in the database with a special ID
|
|
377
|
+
// It uses the system's temp directory and is filtered from the workspace list shown to users
|
|
378
|
+
const getOrCreateTempWorkspace = async (): Promise<Workspace> => {
|
|
379
|
+
// Check if temp workspace already exists in database
|
|
380
|
+
const existing = workspaceRepo.findById(TEMP_WORKSPACE_ID);
|
|
381
|
+
if (existing) {
|
|
382
|
+
const updatedPermissions = {
|
|
383
|
+
...existing.permissions,
|
|
384
|
+
read: true,
|
|
385
|
+
write: true,
|
|
386
|
+
delete: true,
|
|
387
|
+
network: true,
|
|
388
|
+
shell: existing.permissions.shell ?? false,
|
|
389
|
+
unrestrictedFileAccess: true,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
if (!existing.permissions.unrestrictedFileAccess) {
|
|
393
|
+
workspaceRepo.updatePermissions(existing.id, updatedPermissions);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Verify the temp directory still exists, recreate if not
|
|
397
|
+
try {
|
|
398
|
+
await fs.access(existing.path);
|
|
399
|
+
return { ...existing, permissions: updatedPermissions, isTemp: true };
|
|
400
|
+
} catch {
|
|
401
|
+
// Directory was deleted, delete the workspace record and recreate
|
|
402
|
+
workspaceRepo.delete(TEMP_WORKSPACE_ID);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Create temp directory
|
|
407
|
+
const tempDir = path.join(os.tmpdir(), 'cowork-os-temp');
|
|
408
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
409
|
+
|
|
410
|
+
// Create the temp workspace with a known ID
|
|
411
|
+
const tempWorkspace: Workspace = {
|
|
412
|
+
id: TEMP_WORKSPACE_ID,
|
|
413
|
+
name: TEMP_WORKSPACE_NAME,
|
|
414
|
+
path: tempDir,
|
|
415
|
+
createdAt: Date.now(),
|
|
416
|
+
permissions: {
|
|
417
|
+
read: true,
|
|
418
|
+
write: true,
|
|
419
|
+
delete: true,
|
|
420
|
+
network: true,
|
|
421
|
+
shell: false,
|
|
422
|
+
unrestrictedFileAccess: true,
|
|
423
|
+
},
|
|
424
|
+
isTemp: true,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// Insert directly using raw SQL to use our specific ID
|
|
428
|
+
const stmt = db.prepare(`
|
|
429
|
+
INSERT OR REPLACE INTO workspaces (id, name, path, created_at, permissions)
|
|
430
|
+
VALUES (?, ?, ?, ?, ?)
|
|
431
|
+
`);
|
|
432
|
+
stmt.run(
|
|
433
|
+
tempWorkspace.id,
|
|
434
|
+
tempWorkspace.name,
|
|
435
|
+
tempWorkspace.path,
|
|
436
|
+
tempWorkspace.createdAt,
|
|
437
|
+
JSON.stringify(tempWorkspace.permissions)
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
return tempWorkspace;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// File handlers - open files and show in Finder
|
|
444
|
+
ipcMain.handle('file:open', async (_, filePath: string, workspacePath?: string) => {
|
|
445
|
+
// Security: require workspacePath and validate path is within it
|
|
446
|
+
if (!workspacePath) {
|
|
447
|
+
throw new Error('Workspace path is required for file operations');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Resolve the path relative to workspace
|
|
451
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
452
|
+
? filePath
|
|
453
|
+
: path.resolve(workspacePath, filePath);
|
|
454
|
+
|
|
455
|
+
// Validate path is within workspace (prevent path traversal)
|
|
456
|
+
if (!isPathWithinWorkspace(resolvedPath, workspacePath)) {
|
|
457
|
+
throw new Error('Access denied: file path is outside the workspace');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return shell.openPath(resolvedPath);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
ipcMain.handle('file:showInFinder', async (_, filePath: string, workspacePath?: string) => {
|
|
464
|
+
// Security: require workspacePath and validate path is within it
|
|
465
|
+
if (!workspacePath) {
|
|
466
|
+
throw new Error('Workspace path is required for file operations');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Resolve the path relative to workspace
|
|
470
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
471
|
+
? filePath
|
|
472
|
+
: path.resolve(workspacePath, filePath);
|
|
473
|
+
|
|
474
|
+
// Validate path is within workspace (prevent path traversal)
|
|
475
|
+
if (!isPathWithinWorkspace(resolvedPath, workspacePath)) {
|
|
476
|
+
throw new Error('Access denied: file path is outside the workspace');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
shell.showItemInFolder(resolvedPath);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Open external URL in system browser
|
|
483
|
+
ipcMain.handle('shell:openExternal', async (_, url: string) => {
|
|
484
|
+
// Validate URL to prevent security issues
|
|
485
|
+
try {
|
|
486
|
+
const parsedUrl = new URL(url);
|
|
487
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
488
|
+
throw new Error('Only http and https URLs are allowed');
|
|
489
|
+
}
|
|
490
|
+
await shell.openExternal(url);
|
|
491
|
+
} catch (error: any) {
|
|
492
|
+
throw new Error(`Failed to open URL: ${error.message}`);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// File viewer handler - read file content for in-app preview
|
|
497
|
+
// Note: This handler allows viewing any file on the system for convenience.
|
|
498
|
+
// File operations like open/showInFinder remain workspace-restricted.
|
|
499
|
+
ipcMain.handle('file:readForViewer', async (_, data: { filePath: string; workspacePath?: string }) => {
|
|
500
|
+
const { filePath, workspacePath } = data;
|
|
501
|
+
|
|
502
|
+
// Resolve the path - if absolute use directly, otherwise resolve relative to workspace or cwd
|
|
503
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
504
|
+
? filePath
|
|
505
|
+
: workspacePath
|
|
506
|
+
? path.resolve(workspacePath, filePath)
|
|
507
|
+
: path.resolve(filePath);
|
|
508
|
+
|
|
509
|
+
// Check if file exists
|
|
510
|
+
try {
|
|
511
|
+
await fs.access(resolvedPath);
|
|
512
|
+
} catch {
|
|
513
|
+
return { success: false, error: 'File not found' };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Get file stats
|
|
517
|
+
const stats = await fs.stat(resolvedPath);
|
|
518
|
+
const extension = path.extname(resolvedPath).toLowerCase();
|
|
519
|
+
const fileName = path.basename(resolvedPath);
|
|
520
|
+
|
|
521
|
+
// Determine file type
|
|
522
|
+
const getFileType = (ext: string): 'markdown' | 'code' | 'text' | 'docx' | 'pdf' | 'image' | 'pptx' | 'html' | 'unsupported' => {
|
|
523
|
+
const codeExtensions = ['.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.go', '.rs', '.c', '.cpp', '.h', '.css', '.scss', '.xml', '.json', '.yaml', '.yml', '.toml', '.sh', '.bash', '.zsh', '.sql', '.graphql', '.vue', '.svelte', '.rb', '.php', '.swift', '.kt', '.scala'];
|
|
524
|
+
const textExtensions = ['.txt', '.log', '.csv', '.env', '.gitignore', '.dockerignore', '.editorconfig', '.prettierrc', '.eslintrc'];
|
|
525
|
+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico'];
|
|
526
|
+
|
|
527
|
+
if (ext === '.md' || ext === '.markdown') return 'markdown';
|
|
528
|
+
if (ext === '.html' || ext === '.htm') return 'html';
|
|
529
|
+
if (ext === '.docx') return 'docx';
|
|
530
|
+
if (ext === '.pdf') return 'pdf';
|
|
531
|
+
if (ext === '.pptx') return 'pptx';
|
|
532
|
+
if (imageExtensions.includes(ext)) return 'image';
|
|
533
|
+
if (codeExtensions.includes(ext)) return 'code';
|
|
534
|
+
if (textExtensions.includes(ext)) return 'text';
|
|
535
|
+
|
|
536
|
+
return 'unsupported';
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const fileType = getFileType(extension);
|
|
540
|
+
|
|
541
|
+
// Size limits
|
|
542
|
+
const MAX_TEXT_SIZE = 5 * 1024 * 1024; // 5MB
|
|
543
|
+
const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
544
|
+
|
|
545
|
+
if (fileType === 'image' && stats.size > MAX_IMAGE_SIZE) {
|
|
546
|
+
return { success: false, error: 'File too large for preview (max 10MB for images)' };
|
|
547
|
+
}
|
|
548
|
+
if (fileType !== 'image' && fileType !== 'unsupported' && stats.size > MAX_TEXT_SIZE) {
|
|
549
|
+
return { success: false, error: 'File too large for preview (max 5MB for text files)' };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
let content: string | null = null;
|
|
554
|
+
let htmlContent: string | undefined;
|
|
555
|
+
|
|
556
|
+
switch (fileType) {
|
|
557
|
+
case 'markdown':
|
|
558
|
+
case 'code':
|
|
559
|
+
case 'text': {
|
|
560
|
+
content = await fs.readFile(resolvedPath, 'utf-8');
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
case 'docx': {
|
|
565
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
566
|
+
const result = await mammoth.convertToHtml({ buffer });
|
|
567
|
+
htmlContent = result.value;
|
|
568
|
+
content = null; // HTML content is in htmlContent
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
case 'pdf': {
|
|
573
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
574
|
+
const pdfData = await pdfParse(buffer);
|
|
575
|
+
content = pdfData.text;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
case 'image': {
|
|
580
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
581
|
+
const mimeTypes: Record<string, string> = {
|
|
582
|
+
'.png': 'image/png',
|
|
583
|
+
'.jpg': 'image/jpeg',
|
|
584
|
+
'.jpeg': 'image/jpeg',
|
|
585
|
+
'.gif': 'image/gif',
|
|
586
|
+
'.webp': 'image/webp',
|
|
587
|
+
'.svg': 'image/svg+xml',
|
|
588
|
+
'.bmp': 'image/bmp',
|
|
589
|
+
'.ico': 'image/x-icon',
|
|
590
|
+
};
|
|
591
|
+
const mimeType = mimeTypes[extension] || 'image/png';
|
|
592
|
+
content = `data:${mimeType};base64,${buffer.toString('base64')}`;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
case 'html': {
|
|
597
|
+
htmlContent = await fs.readFile(resolvedPath, 'utf-8');
|
|
598
|
+
content = null; // HTML content is in htmlContent
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
case 'pptx':
|
|
603
|
+
// PowerPoint files are complex to render, return placeholder
|
|
604
|
+
content = null;
|
|
605
|
+
break;
|
|
606
|
+
|
|
607
|
+
default:
|
|
608
|
+
return { success: false, error: 'Unsupported file type', fileType: 'unsupported' };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
success: true,
|
|
613
|
+
data: {
|
|
614
|
+
path: resolvedPath,
|
|
615
|
+
fileName,
|
|
616
|
+
fileType,
|
|
617
|
+
content,
|
|
618
|
+
htmlContent,
|
|
619
|
+
size: stats.size,
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
} catch (error: any) {
|
|
623
|
+
return { success: false, error: `Failed to read file: ${error.message}` };
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Workspace handlers
|
|
628
|
+
ipcMain.handle(IPC_CHANNELS.WORKSPACE_CREATE, async (_, data) => {
|
|
629
|
+
const validated = validateInput(WorkspaceCreateSchema, data, 'workspace');
|
|
630
|
+
const { name, path, permissions } = validated;
|
|
631
|
+
|
|
632
|
+
// Check if workspace with this path already exists
|
|
633
|
+
if (workspaceRepo.existsByPath(path)) {
|
|
634
|
+
throw new Error(`A workspace with path "${path}" already exists. Please choose a different folder.`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Provide default permissions if not specified
|
|
638
|
+
// Note: network is enabled by default for browser tools (web access)
|
|
639
|
+
const defaultPermissions = {
|
|
640
|
+
read: true,
|
|
641
|
+
write: true,
|
|
642
|
+
delete: false,
|
|
643
|
+
network: true,
|
|
644
|
+
shell: false,
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
return workspaceRepo.create(name, path, permissions ?? defaultPermissions);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
ipcMain.handle(IPC_CHANNELS.WORKSPACE_LIST, async () => {
|
|
651
|
+
// Filter out the temp workspace from the list - users shouldn't see it in their workspaces
|
|
652
|
+
const allWorkspaces = workspaceRepo.findAll();
|
|
653
|
+
return allWorkspaces.filter(w => w.id !== TEMP_WORKSPACE_ID);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Get or create the temp workspace (used when no workspace is selected)
|
|
657
|
+
ipcMain.handle(IPC_CHANNELS.WORKSPACE_GET_TEMP, async () => {
|
|
658
|
+
return getOrCreateTempWorkspace();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
ipcMain.handle(IPC_CHANNELS.WORKSPACE_SELECT, async (_, id: string) => {
|
|
662
|
+
return workspaceRepo.findById(id);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
ipcMain.handle(IPC_CHANNELS.WORKSPACE_UPDATE_PERMISSIONS, async (_, id: string, permissions: { shell?: boolean; network?: boolean; read?: boolean; write?: boolean; delete?: boolean }) => {
|
|
666
|
+
const workspace = workspaceRepo.findById(id);
|
|
667
|
+
if (!workspace) {
|
|
668
|
+
throw new Error(`Workspace not found: ${id}`);
|
|
669
|
+
}
|
|
670
|
+
const updatedPermissions = { ...workspace.permissions, ...permissions };
|
|
671
|
+
workspaceRepo.updatePermissions(id, updatedPermissions);
|
|
672
|
+
return workspaceRepo.findById(id);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Task handlers
|
|
676
|
+
ipcMain.handle(IPC_CHANNELS.TASK_CREATE, async (_, data) => {
|
|
677
|
+
checkRateLimit(IPC_CHANNELS.TASK_CREATE);
|
|
678
|
+
const validated = validateInput(TaskCreateSchema, data, 'task');
|
|
679
|
+
const { title, prompt, workspaceId, budgetTokens, budgetCost } = validated;
|
|
680
|
+
const task = taskRepo.create({
|
|
681
|
+
title,
|
|
682
|
+
prompt,
|
|
683
|
+
status: 'pending',
|
|
684
|
+
workspaceId,
|
|
685
|
+
budgetTokens,
|
|
686
|
+
budgetCost,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Start task execution in agent daemon
|
|
690
|
+
try {
|
|
691
|
+
await agentDaemon.startTask(task);
|
|
692
|
+
} catch (error: any) {
|
|
693
|
+
// Update task status to failed if we can't start it
|
|
694
|
+
taskRepo.update(task.id, {
|
|
695
|
+
status: 'failed',
|
|
696
|
+
error: error.message || 'Failed to start task',
|
|
697
|
+
});
|
|
698
|
+
throw new Error(error.message || 'Failed to start task. Please check your LLM provider settings.');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Dispatch to mentioned agents (e.g., "Please review @Vision @Loki")
|
|
702
|
+
try {
|
|
703
|
+
const activeRoles = agentRoleRepo.findAll(false).filter((role) => role.isActive);
|
|
704
|
+
const mentionedRoles = extractMentionedRoles(`${title}\n${prompt}`, activeRoles);
|
|
705
|
+
const dispatchRoles = mentionedRoles.length > 0 ? mentionedRoles : activeRoles;
|
|
706
|
+
|
|
707
|
+
if (dispatchRoles.length > 0) {
|
|
708
|
+
const taskUpdate: Partial<Task> = {
|
|
709
|
+
mentionedAgentRoleIds: dispatchRoles.map((role) => role.id),
|
|
710
|
+
};
|
|
711
|
+
taskRepo.update(task.id, taskUpdate);
|
|
712
|
+
|
|
713
|
+
// Parallelize child task creation for better performance
|
|
714
|
+
const dispatchPromises = dispatchRoles.map(async (role) => {
|
|
715
|
+
const childPrompt = buildAgentDispatchPrompt(role, task);
|
|
716
|
+
const childTask = await agentDaemon.createChildTask({
|
|
717
|
+
title: `@${role.displayName}: ${task.title}`,
|
|
718
|
+
prompt: childPrompt,
|
|
719
|
+
workspaceId: task.workspaceId,
|
|
720
|
+
parentTaskId: task.id,
|
|
721
|
+
agentType: 'sub',
|
|
722
|
+
agentConfig: {
|
|
723
|
+
...(role.modelKey ? { modelKey: role.modelKey } : {}),
|
|
724
|
+
...(role.personalityId ? { personalityId: role.personalityId } : {}),
|
|
725
|
+
retainMemory: false,
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const childUpdate: Partial<Task> = {
|
|
730
|
+
assignedAgentRoleId: role.id,
|
|
731
|
+
boardColumn: 'todo' as BoardColumn,
|
|
732
|
+
};
|
|
733
|
+
taskRepo.update(childTask.id, childUpdate);
|
|
734
|
+
|
|
735
|
+
const dispatchActivity = activityRepo.create({
|
|
736
|
+
workspaceId: task.workspaceId,
|
|
737
|
+
taskId: task.id,
|
|
738
|
+
agentRoleId: role.id,
|
|
739
|
+
actorType: 'system',
|
|
740
|
+
activityType: 'agent_assigned',
|
|
741
|
+
title: `Dispatched to ${role.displayName}`,
|
|
742
|
+
description: childTask.title,
|
|
743
|
+
});
|
|
744
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity: dispatchActivity });
|
|
745
|
+
|
|
746
|
+
const mention = mentionRepo.create({
|
|
747
|
+
workspaceId: task.workspaceId,
|
|
748
|
+
taskId: task.id,
|
|
749
|
+
toAgentRoleId: role.id,
|
|
750
|
+
mentionType: 'request',
|
|
751
|
+
context: `New task: ${task.title}`,
|
|
752
|
+
});
|
|
753
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.MENTION_EVENT, { type: 'created', mention });
|
|
754
|
+
|
|
755
|
+
const mentionActivity = activityRepo.create({
|
|
756
|
+
workspaceId: task.workspaceId,
|
|
757
|
+
taskId: task.id,
|
|
758
|
+
agentRoleId: role.id,
|
|
759
|
+
actorType: 'user',
|
|
760
|
+
activityType: 'mention',
|
|
761
|
+
title: `@${role.displayName} mentioned`,
|
|
762
|
+
description: mention.context,
|
|
763
|
+
metadata: { mentionId: mention.id, mentionType: mention.mentionType },
|
|
764
|
+
});
|
|
765
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity: mentionActivity });
|
|
766
|
+
|
|
767
|
+
return { role, childTask };
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
await Promise.all(dispatchPromises);
|
|
771
|
+
}
|
|
772
|
+
} catch (error: unknown) {
|
|
773
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
774
|
+
console.error('Failed to dispatch to mentioned agents:', error);
|
|
775
|
+
// Notify user of dispatch failure via activity feed
|
|
776
|
+
const errorActivity = activityRepo.create({
|
|
777
|
+
workspaceId: task.workspaceId,
|
|
778
|
+
taskId: task.id,
|
|
779
|
+
actorType: 'system',
|
|
780
|
+
activityType: 'error',
|
|
781
|
+
title: 'Agent dispatch failed',
|
|
782
|
+
description: `Failed to dispatch task to mentioned agents: ${errorMessage}`,
|
|
783
|
+
});
|
|
784
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity: errorActivity });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return task;
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
ipcMain.handle(IPC_CHANNELS.TASK_GET, async (_, id: string) => {
|
|
791
|
+
return taskRepo.findById(id);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
ipcMain.handle(IPC_CHANNELS.TASK_LIST, async () => {
|
|
795
|
+
return taskRepo.findAll();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
ipcMain.handle(IPC_CHANNELS.TASK_CANCEL, async (_, id: string) => {
|
|
799
|
+
try {
|
|
800
|
+
await agentDaemon.cancelTask(id);
|
|
801
|
+
} finally {
|
|
802
|
+
// Always update status even if daemon cancel fails
|
|
803
|
+
taskRepo.update(id, { status: 'cancelled' });
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
ipcMain.handle(IPC_CHANNELS.TASK_PAUSE, async (_, id: string) => {
|
|
808
|
+
// Pause daemon first - if it fails, exception propagates and status won't be updated
|
|
809
|
+
await agentDaemon.pauseTask(id);
|
|
810
|
+
taskRepo.update(id, { status: 'paused' });
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
ipcMain.handle(IPC_CHANNELS.TASK_RESUME, async (_, id: string) => {
|
|
814
|
+
// Resume daemon first - if it fails, exception propagates and status won't be updated
|
|
815
|
+
await agentDaemon.resumeTask(id);
|
|
816
|
+
taskRepo.update(id, { status: 'executing' });
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
ipcMain.handle(IPC_CHANNELS.TASK_SEND_STDIN, async (_, data: { taskId: string; input: string }) => {
|
|
820
|
+
return agentDaemon.sendStdinToTask(data.taskId, data.input);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
ipcMain.handle(IPC_CHANNELS.TASK_KILL_COMMAND, async (_, data: { taskId: string; force?: boolean }) => {
|
|
824
|
+
return agentDaemon.killCommandInTask(data.taskId, data.force);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
ipcMain.handle(IPC_CHANNELS.TASK_RENAME, async (_, data) => {
|
|
828
|
+
const validated = validateInput(TaskRenameSchema, data, 'task rename');
|
|
829
|
+
taskRepo.update(validated.id, { title: validated.title });
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
ipcMain.handle(IPC_CHANNELS.TASK_DELETE, async (_, id: string) => {
|
|
833
|
+
// Cancel the task if it's running
|
|
834
|
+
await agentDaemon.cancelTask(id);
|
|
835
|
+
// Delete from database
|
|
836
|
+
taskRepo.delete(id);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// ============ Sub-Agent / Parallel Agent Handlers ============
|
|
840
|
+
|
|
841
|
+
// Get child tasks for a parent task
|
|
842
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_GET_CHILDREN, async (_, parentTaskId: string) => {
|
|
843
|
+
return agentDaemon.getChildTasks(parentTaskId);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// Get status of specific agents
|
|
847
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_GET_STATUS, async (_, taskIds: string[]) => {
|
|
848
|
+
const tasks = [];
|
|
849
|
+
for (const id of taskIds) {
|
|
850
|
+
const task = await agentDaemon.getTaskById(id);
|
|
851
|
+
if (task) {
|
|
852
|
+
tasks.push({
|
|
853
|
+
taskId: id,
|
|
854
|
+
status: task.status,
|
|
855
|
+
title: task.title,
|
|
856
|
+
agentType: task.agentType,
|
|
857
|
+
resultSummary: task.resultSummary,
|
|
858
|
+
error: task.error,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return tasks;
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Task events handler - get historical events from database
|
|
866
|
+
ipcMain.handle(IPC_CHANNELS.TASK_EVENTS, async (_, taskId: string) => {
|
|
867
|
+
return taskEventRepo.findByTaskId(taskId);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// Send follow-up message to a task
|
|
871
|
+
ipcMain.handle(IPC_CHANNELS.TASK_SEND_MESSAGE, async (_, data) => {
|
|
872
|
+
checkRateLimit(IPC_CHANNELS.TASK_SEND_MESSAGE);
|
|
873
|
+
const validated = validateInput(TaskMessageSchema, data, 'task message');
|
|
874
|
+
await agentDaemon.sendMessage(validated.taskId, validated.message);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// Approval handlers
|
|
878
|
+
ipcMain.handle(IPC_CHANNELS.APPROVAL_RESPOND, async (_, data) => {
|
|
879
|
+
const validated = validateInput(ApprovalResponseSchema, data, 'approval response');
|
|
880
|
+
await agentDaemon.respondToApproval(validated.approvalId, validated.approved);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Artifact handlers
|
|
884
|
+
ipcMain.handle(IPC_CHANNELS.ARTIFACT_LIST, async (_, taskId: string) => {
|
|
885
|
+
return artifactRepo.findByTaskId(taskId);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
ipcMain.handle(IPC_CHANNELS.ARTIFACT_PREVIEW, async (_, id: string) => {
|
|
889
|
+
// TODO: Implement artifact preview
|
|
890
|
+
return null;
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// Skill handlers
|
|
894
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_LIST, async () => {
|
|
895
|
+
return skillRepo.findAll();
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_GET, async (_, id: string) => {
|
|
899
|
+
return skillRepo.findById(id);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
// Custom User Skills handlers
|
|
903
|
+
const customSkillLoader = getCustomSkillLoader();
|
|
904
|
+
|
|
905
|
+
// Initialize custom skill loader
|
|
906
|
+
customSkillLoader.initialize().catch(error => {
|
|
907
|
+
console.error('[IPC] Failed to initialize custom skill loader:', error);
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_LIST, async () => {
|
|
911
|
+
return customSkillLoader.listSkills();
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_LIST_TASKS, async () => {
|
|
915
|
+
return customSkillLoader.listTaskSkills();
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_LIST_GUIDELINES, async () => {
|
|
919
|
+
return customSkillLoader.listGuidelineSkills();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_GET, async (_, id: string) => {
|
|
923
|
+
return customSkillLoader.getSkill(id);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_CREATE, async (_, skillData: Omit<CustomSkill, 'filePath'>) => {
|
|
927
|
+
return customSkillLoader.createSkill(skillData);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_UPDATE, async (_, id: string, updates: Partial<CustomSkill>) => {
|
|
931
|
+
return customSkillLoader.updateSkill(id, updates);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_DELETE, async (_, id: string) => {
|
|
935
|
+
return customSkillLoader.deleteSkill(id);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_RELOAD, async () => {
|
|
939
|
+
return customSkillLoader.reloadSkills();
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
ipcMain.handle(IPC_CHANNELS.CUSTOM_SKILL_OPEN_FOLDER, async () => {
|
|
943
|
+
return customSkillLoader.openSkillsFolder();
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// Skill Registry (SkillHub) handlers
|
|
947
|
+
const { getSkillRegistry } = await import('../agent/skill-registry');
|
|
948
|
+
const skillRegistry = getSkillRegistry();
|
|
949
|
+
|
|
950
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_SEARCH, async (_, query: string, options?: { page?: number; pageSize?: number }) => {
|
|
951
|
+
return skillRegistry.search(query, options);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_GET_DETAILS, async (_, skillId: string) => {
|
|
955
|
+
return skillRegistry.getSkillDetails(skillId);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_INSTALL, async (_, skillId: string, version?: string) => {
|
|
959
|
+
const result = await skillRegistry.install(skillId, version);
|
|
960
|
+
if (result.success) {
|
|
961
|
+
// Reload skills to pick up the new one
|
|
962
|
+
await customSkillLoader.reloadSkills();
|
|
963
|
+
// Clear eligibility cache in case new dependencies were installed
|
|
964
|
+
customSkillLoader.clearEligibilityCache();
|
|
965
|
+
}
|
|
966
|
+
return result;
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_UPDATE, async (_, skillId: string, version?: string) => {
|
|
970
|
+
const result = await skillRegistry.update(skillId, version);
|
|
971
|
+
if (result.success) {
|
|
972
|
+
await customSkillLoader.reloadSkills();
|
|
973
|
+
customSkillLoader.clearEligibilityCache();
|
|
974
|
+
}
|
|
975
|
+
return result;
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_UPDATE_ALL, async () => {
|
|
979
|
+
const result = await skillRegistry.updateAll();
|
|
980
|
+
await customSkillLoader.reloadSkills();
|
|
981
|
+
customSkillLoader.clearEligibilityCache();
|
|
982
|
+
return result;
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_UNINSTALL, async (_, skillId: string) => {
|
|
986
|
+
const result = skillRegistry.uninstall(skillId);
|
|
987
|
+
if (result.success) {
|
|
988
|
+
await customSkillLoader.reloadSkills();
|
|
989
|
+
}
|
|
990
|
+
return result;
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_LIST_MANAGED, async () => {
|
|
994
|
+
return skillRegistry.listManagedSkills();
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_CHECK_UPDATES, async (_, skillId: string) => {
|
|
998
|
+
return skillRegistry.checkForUpdates(skillId);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_GET_STATUS, async () => {
|
|
1002
|
+
return customSkillLoader.getSkillStatus();
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
ipcMain.handle(IPC_CHANNELS.SKILL_REGISTRY_GET_ELIGIBLE, async () => {
|
|
1006
|
+
return customSkillLoader.getEligibleSkills();
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// LLM Settings handlers
|
|
1010
|
+
ipcMain.handle(IPC_CHANNELS.LLM_GET_SETTINGS, async () => {
|
|
1011
|
+
return LLMProviderFactory.loadSettings();
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
ipcMain.handle(IPC_CHANNELS.LLM_SAVE_SETTINGS, async (_, settings) => {
|
|
1015
|
+
checkRateLimit(IPC_CHANNELS.LLM_SAVE_SETTINGS);
|
|
1016
|
+
const validated = validateInput(LLMSettingsSchema, settings, 'LLM settings');
|
|
1017
|
+
|
|
1018
|
+
// Load existing settings to preserve cached models and OAuth tokens
|
|
1019
|
+
const existingSettings = LLMProviderFactory.loadSettings();
|
|
1020
|
+
|
|
1021
|
+
// Build OpenAI settings, preserving OAuth tokens from existing settings
|
|
1022
|
+
let openaiSettings = validated.openai;
|
|
1023
|
+
if (existingSettings.openai?.authMethod === 'oauth') {
|
|
1024
|
+
// Preserve OAuth tokens when saving settings
|
|
1025
|
+
openaiSettings = {
|
|
1026
|
+
...validated.openai,
|
|
1027
|
+
accessToken: existingSettings.openai.accessToken,
|
|
1028
|
+
refreshToken: existingSettings.openai.refreshToken,
|
|
1029
|
+
tokenExpiresAt: existingSettings.openai.tokenExpiresAt,
|
|
1030
|
+
authMethod: existingSettings.openai.authMethod,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
LLMProviderFactory.saveSettings({
|
|
1035
|
+
providerType: validated.providerType,
|
|
1036
|
+
modelKey: validated.modelKey as ModelKey,
|
|
1037
|
+
anthropic: validated.anthropic,
|
|
1038
|
+
bedrock: validated.bedrock,
|
|
1039
|
+
ollama: validated.ollama,
|
|
1040
|
+
gemini: validated.gemini,
|
|
1041
|
+
openrouter: validated.openrouter,
|
|
1042
|
+
openai: openaiSettings,
|
|
1043
|
+
// Preserve cached models from existing settings
|
|
1044
|
+
cachedGeminiModels: existingSettings.cachedGeminiModels,
|
|
1045
|
+
cachedOpenRouterModels: existingSettings.cachedOpenRouterModels,
|
|
1046
|
+
cachedOllamaModels: existingSettings.cachedOllamaModels,
|
|
1047
|
+
cachedBedrockModels: existingSettings.cachedBedrockModels,
|
|
1048
|
+
cachedOpenAIModels: existingSettings.cachedOpenAIModels,
|
|
1049
|
+
});
|
|
1050
|
+
// Clear cache so next task uses new settings
|
|
1051
|
+
LLMProviderFactory.clearCache();
|
|
1052
|
+
return { success: true };
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
ipcMain.handle(IPC_CHANNELS.LLM_TEST_PROVIDER, async (_, config: any) => {
|
|
1056
|
+
checkRateLimit(IPC_CHANNELS.LLM_TEST_PROVIDER);
|
|
1057
|
+
// For OpenAI OAuth, get tokens from stored settings if authMethod is 'oauth'
|
|
1058
|
+
let openaiAccessToken: string | undefined;
|
|
1059
|
+
let openaiRefreshToken: string | undefined;
|
|
1060
|
+
if (config.providerType === 'openai' && config.openai?.authMethod === 'oauth') {
|
|
1061
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
1062
|
+
openaiAccessToken = settings.openai?.accessToken;
|
|
1063
|
+
openaiRefreshToken = settings.openai?.refreshToken;
|
|
1064
|
+
}
|
|
1065
|
+
const providerConfig: LLMProviderConfig = {
|
|
1066
|
+
type: config.providerType,
|
|
1067
|
+
model: LLMProviderFactory.getModelId(
|
|
1068
|
+
config.modelKey as ModelKey,
|
|
1069
|
+
config.providerType,
|
|
1070
|
+
config.ollama?.model,
|
|
1071
|
+
config.gemini?.model,
|
|
1072
|
+
config.openrouter?.model,
|
|
1073
|
+
config.openai?.model
|
|
1074
|
+
),
|
|
1075
|
+
anthropicApiKey: config.anthropic?.apiKey,
|
|
1076
|
+
awsRegion: config.bedrock?.region,
|
|
1077
|
+
awsAccessKeyId: config.bedrock?.accessKeyId,
|
|
1078
|
+
awsSecretAccessKey: config.bedrock?.secretAccessKey,
|
|
1079
|
+
awsSessionToken: config.bedrock?.sessionToken,
|
|
1080
|
+
awsProfile: config.bedrock?.profile,
|
|
1081
|
+
ollamaBaseUrl: config.ollama?.baseUrl,
|
|
1082
|
+
ollamaApiKey: config.ollama?.apiKey,
|
|
1083
|
+
geminiApiKey: config.gemini?.apiKey,
|
|
1084
|
+
openrouterApiKey: config.openrouter?.apiKey,
|
|
1085
|
+
openaiApiKey: config.openai?.apiKey,
|
|
1086
|
+
openaiAccessToken: openaiAccessToken,
|
|
1087
|
+
openaiRefreshToken: openaiRefreshToken,
|
|
1088
|
+
};
|
|
1089
|
+
return LLMProviderFactory.testProvider(providerConfig);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
ipcMain.handle(IPC_CHANNELS.LLM_GET_MODELS, async () => {
|
|
1093
|
+
// Get models from database
|
|
1094
|
+
const dbModels = llmModelRepo.findAll();
|
|
1095
|
+
return dbModels.map(m => ({
|
|
1096
|
+
key: m.key,
|
|
1097
|
+
displayName: m.displayName,
|
|
1098
|
+
description: m.description,
|
|
1099
|
+
}));
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
ipcMain.handle(IPC_CHANNELS.LLM_GET_CONFIG_STATUS, async () => {
|
|
1103
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
1104
|
+
const providers = LLMProviderFactory.getAvailableProviders();
|
|
1105
|
+
|
|
1106
|
+
// Get models based on the current provider type
|
|
1107
|
+
let models: Array<{ key: string; displayName: string; description: string }> = [];
|
|
1108
|
+
let currentModel = settings.modelKey;
|
|
1109
|
+
|
|
1110
|
+
switch (settings.providerType) {
|
|
1111
|
+
case 'anthropic':
|
|
1112
|
+
case 'bedrock':
|
|
1113
|
+
// Use Anthropic/Bedrock models from MODELS
|
|
1114
|
+
models = Object.entries(MODELS).map(([key, value]) => ({
|
|
1115
|
+
key,
|
|
1116
|
+
displayName: value.displayName,
|
|
1117
|
+
description: key.includes('opus') ? 'Most capable for complex work' :
|
|
1118
|
+
key.includes('sonnet') ? 'Balanced performance and speed' :
|
|
1119
|
+
'Fast and efficient',
|
|
1120
|
+
}));
|
|
1121
|
+
break;
|
|
1122
|
+
|
|
1123
|
+
case 'gemini': {
|
|
1124
|
+
// For Gemini, use the specific model from settings (full model ID)
|
|
1125
|
+
currentModel = settings.gemini?.model || 'gemini-2.0-flash';
|
|
1126
|
+
// Use cached models if available, otherwise fall back to static list
|
|
1127
|
+
const cachedGemini = LLMProviderFactory.getCachedModels('gemini');
|
|
1128
|
+
if (cachedGemini && cachedGemini.length > 0) {
|
|
1129
|
+
models = cachedGemini;
|
|
1130
|
+
} else {
|
|
1131
|
+
// Fall back to static models
|
|
1132
|
+
models = Object.values(GEMINI_MODELS).map((value) => ({
|
|
1133
|
+
key: value.id,
|
|
1134
|
+
displayName: value.displayName,
|
|
1135
|
+
description: value.description,
|
|
1136
|
+
}));
|
|
1137
|
+
}
|
|
1138
|
+
// Ensure the currently selected model is in the list
|
|
1139
|
+
if (currentModel && !models.some(m => m.key === currentModel)) {
|
|
1140
|
+
models.unshift({
|
|
1141
|
+
key: currentModel,
|
|
1142
|
+
displayName: currentModel,
|
|
1143
|
+
description: 'Selected model',
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
case 'openrouter': {
|
|
1150
|
+
// For OpenRouter, use the specific model from settings (full model ID)
|
|
1151
|
+
currentModel = settings.openrouter?.model || 'anthropic/claude-3.5-sonnet';
|
|
1152
|
+
// Use cached models if available, otherwise fall back to static list
|
|
1153
|
+
const cachedOpenRouter = LLMProviderFactory.getCachedModels('openrouter');
|
|
1154
|
+
if (cachedOpenRouter && cachedOpenRouter.length > 0) {
|
|
1155
|
+
models = cachedOpenRouter;
|
|
1156
|
+
} else {
|
|
1157
|
+
// Fall back to static models
|
|
1158
|
+
models = Object.values(OPENROUTER_MODELS).map((value) => ({
|
|
1159
|
+
key: value.id,
|
|
1160
|
+
displayName: value.displayName,
|
|
1161
|
+
description: value.description,
|
|
1162
|
+
}));
|
|
1163
|
+
}
|
|
1164
|
+
// Ensure the currently selected model is in the list
|
|
1165
|
+
if (currentModel && !models.some(m => m.key === currentModel)) {
|
|
1166
|
+
models.unshift({
|
|
1167
|
+
key: currentModel,
|
|
1168
|
+
displayName: currentModel,
|
|
1169
|
+
description: 'Selected model',
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
case 'ollama': {
|
|
1176
|
+
// For Ollama, use the specific model from settings
|
|
1177
|
+
currentModel = settings.ollama?.model || 'llama3.2';
|
|
1178
|
+
// Use cached models if available, otherwise fall back to static list
|
|
1179
|
+
const cachedOllama = LLMProviderFactory.getCachedModels('ollama');
|
|
1180
|
+
if (cachedOllama && cachedOllama.length > 0) {
|
|
1181
|
+
models = cachedOllama;
|
|
1182
|
+
} else {
|
|
1183
|
+
// Fall back to static models
|
|
1184
|
+
models = Object.entries(OLLAMA_MODELS).map(([key, value]) => ({
|
|
1185
|
+
key,
|
|
1186
|
+
displayName: value.displayName,
|
|
1187
|
+
description: `${value.size} parameter model`,
|
|
1188
|
+
}));
|
|
1189
|
+
}
|
|
1190
|
+
// Ensure the currently selected model is in the list
|
|
1191
|
+
if (currentModel && !models.some(m => m.key === currentModel)) {
|
|
1192
|
+
models.unshift({
|
|
1193
|
+
key: currentModel,
|
|
1194
|
+
displayName: currentModel,
|
|
1195
|
+
description: 'Selected model',
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
case 'openai': {
|
|
1202
|
+
// For OpenAI, use the specific model from settings
|
|
1203
|
+
currentModel = settings.openai?.model || 'gpt-4o-mini';
|
|
1204
|
+
// Use cached models if available, otherwise fall back to static list
|
|
1205
|
+
const cachedOpenAI = LLMProviderFactory.getCachedModels('openai');
|
|
1206
|
+
if (cachedOpenAI && cachedOpenAI.length > 0) {
|
|
1207
|
+
models = cachedOpenAI;
|
|
1208
|
+
} else {
|
|
1209
|
+
// Fall back to static models
|
|
1210
|
+
models = [
|
|
1211
|
+
{ key: 'gpt-4o', displayName: 'GPT-4o', description: 'Most capable model for complex tasks' },
|
|
1212
|
+
{ key: 'gpt-4o-mini', displayName: 'GPT-4o Mini', description: 'Fast and affordable for most tasks' },
|
|
1213
|
+
{ key: 'gpt-4-turbo', displayName: 'GPT-4 Turbo', description: 'Previous generation flagship' },
|
|
1214
|
+
{ key: 'gpt-3.5-turbo', displayName: 'GPT-3.5 Turbo', description: 'Fast and cost-effective' },
|
|
1215
|
+
{ key: 'o1', displayName: 'o1', description: 'Advanced reasoning model' },
|
|
1216
|
+
{ key: 'o1-mini', displayName: 'o1 Mini', description: 'Fast reasoning model' },
|
|
1217
|
+
];
|
|
1218
|
+
}
|
|
1219
|
+
// Ensure the currently selected model is in the list
|
|
1220
|
+
if (currentModel && !models.some(m => m.key === currentModel)) {
|
|
1221
|
+
models.unshift({
|
|
1222
|
+
key: currentModel,
|
|
1223
|
+
displayName: currentModel,
|
|
1224
|
+
description: 'Selected model',
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
default:
|
|
1231
|
+
// Fallback to Anthropic models
|
|
1232
|
+
models = Object.entries(MODELS).map(([key, value]) => ({
|
|
1233
|
+
key,
|
|
1234
|
+
displayName: value.displayName,
|
|
1235
|
+
description: 'Claude model',
|
|
1236
|
+
}));
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return {
|
|
1240
|
+
currentProvider: settings.providerType,
|
|
1241
|
+
currentModel,
|
|
1242
|
+
providers,
|
|
1243
|
+
models,
|
|
1244
|
+
};
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
// Set the current model (persists selection across sessions)
|
|
1248
|
+
ipcMain.handle(IPC_CHANNELS.LLM_SET_MODEL, async (_, modelKey: string) => {
|
|
1249
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
1250
|
+
|
|
1251
|
+
// Update the model key based on the current provider
|
|
1252
|
+
switch (settings.providerType) {
|
|
1253
|
+
case 'gemini':
|
|
1254
|
+
settings.gemini = { ...settings.gemini, model: modelKey };
|
|
1255
|
+
break;
|
|
1256
|
+
case 'openrouter':
|
|
1257
|
+
settings.openrouter = { ...settings.openrouter, model: modelKey };
|
|
1258
|
+
break;
|
|
1259
|
+
case 'ollama':
|
|
1260
|
+
settings.ollama = { ...settings.ollama, model: modelKey };
|
|
1261
|
+
break;
|
|
1262
|
+
case 'openai':
|
|
1263
|
+
settings.openai = { ...settings.openai, model: modelKey };
|
|
1264
|
+
break;
|
|
1265
|
+
case 'anthropic':
|
|
1266
|
+
case 'bedrock':
|
|
1267
|
+
default:
|
|
1268
|
+
// For Anthropic/Bedrock, use the modelKey field
|
|
1269
|
+
settings.modelKey = modelKey as ModelKey;
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
LLMProviderFactory.saveSettings(settings);
|
|
1274
|
+
return { success: true };
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
ipcMain.handle(IPC_CHANNELS.LLM_GET_OLLAMA_MODELS, async (_, baseUrl?: string) => {
|
|
1278
|
+
checkRateLimit(IPC_CHANNELS.LLM_GET_OLLAMA_MODELS);
|
|
1279
|
+
console.log('[IPC] Handling LLM_GET_OLLAMA_MODELS request');
|
|
1280
|
+
const models = await LLMProviderFactory.getOllamaModels(baseUrl);
|
|
1281
|
+
// Cache the models for use in config status
|
|
1282
|
+
const cachedModels = models.map(m => ({
|
|
1283
|
+
key: m.name,
|
|
1284
|
+
displayName: m.name,
|
|
1285
|
+
description: `${Math.round(m.size / 1e9)}B parameter model`,
|
|
1286
|
+
size: m.size,
|
|
1287
|
+
}));
|
|
1288
|
+
LLMProviderFactory.saveCachedModels('ollama', cachedModels);
|
|
1289
|
+
return models;
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
ipcMain.handle(IPC_CHANNELS.LLM_GET_GEMINI_MODELS, async (_, apiKey?: string) => {
|
|
1293
|
+
checkRateLimit(IPC_CHANNELS.LLM_GET_GEMINI_MODELS);
|
|
1294
|
+
const models = await LLMProviderFactory.getGeminiModels(apiKey);
|
|
1295
|
+
// Cache the models for use in config status
|
|
1296
|
+
const cachedModels = models.map(m => ({
|
|
1297
|
+
key: m.name,
|
|
1298
|
+
displayName: m.displayName,
|
|
1299
|
+
description: m.description,
|
|
1300
|
+
}));
|
|
1301
|
+
LLMProviderFactory.saveCachedModels('gemini', cachedModels);
|
|
1302
|
+
return models;
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
ipcMain.handle(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS, async (_, apiKey?: string) => {
|
|
1306
|
+
checkRateLimit(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS);
|
|
1307
|
+
const models = await LLMProviderFactory.getOpenRouterModels(apiKey);
|
|
1308
|
+
// Cache the models for use in config status
|
|
1309
|
+
const cachedModels = models.map(m => ({
|
|
1310
|
+
key: m.id,
|
|
1311
|
+
displayName: m.name,
|
|
1312
|
+
description: `Context: ${Math.round(m.context_length / 1000)}k tokens`,
|
|
1313
|
+
contextLength: m.context_length,
|
|
1314
|
+
}));
|
|
1315
|
+
LLMProviderFactory.saveCachedModels('openrouter', cachedModels);
|
|
1316
|
+
return models;
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
ipcMain.handle(IPC_CHANNELS.LLM_GET_OPENAI_MODELS, async (_, apiKey?: string) => {
|
|
1320
|
+
checkRateLimit(IPC_CHANNELS.LLM_GET_OPENAI_MODELS);
|
|
1321
|
+
const models = await LLMProviderFactory.getOpenAIModels(apiKey);
|
|
1322
|
+
// Cache the models for use in config status
|
|
1323
|
+
const cachedModels = models.map(m => ({
|
|
1324
|
+
key: m.id,
|
|
1325
|
+
displayName: m.name,
|
|
1326
|
+
description: m.description,
|
|
1327
|
+
}));
|
|
1328
|
+
LLMProviderFactory.saveCachedModels('openai', cachedModels);
|
|
1329
|
+
return models;
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
// OpenAI OAuth handlers
|
|
1333
|
+
ipcMain.handle(IPC_CHANNELS.LLM_OPENAI_OAUTH_START, async () => {
|
|
1334
|
+
checkRateLimit(IPC_CHANNELS.LLM_OPENAI_OAUTH_START);
|
|
1335
|
+
console.log('[IPC] Starting OpenAI OAuth flow with pi-ai SDK...');
|
|
1336
|
+
|
|
1337
|
+
try {
|
|
1338
|
+
const oauth = new OpenAIOAuth();
|
|
1339
|
+
const tokens = await oauth.authenticate();
|
|
1340
|
+
|
|
1341
|
+
// Save tokens to settings
|
|
1342
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
1343
|
+
settings.openai = {
|
|
1344
|
+
...settings.openai,
|
|
1345
|
+
accessToken: tokens.access_token,
|
|
1346
|
+
refreshToken: tokens.refresh_token,
|
|
1347
|
+
tokenExpiresAt: tokens.expires_at,
|
|
1348
|
+
authMethod: 'oauth',
|
|
1349
|
+
// Clear API key when using OAuth
|
|
1350
|
+
apiKey: undefined,
|
|
1351
|
+
};
|
|
1352
|
+
LLMProviderFactory.saveSettings(settings);
|
|
1353
|
+
LLMProviderFactory.clearCache();
|
|
1354
|
+
|
|
1355
|
+
console.log('[IPC] OpenAI OAuth successful!');
|
|
1356
|
+
if (tokens.email) {
|
|
1357
|
+
console.log('[IPC] Logged in as:', tokens.email);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return { success: true, email: tokens.email };
|
|
1361
|
+
} catch (error: any) {
|
|
1362
|
+
console.error('[IPC] OpenAI OAuth failed:', error.message);
|
|
1363
|
+
return { success: false, error: error.message };
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
ipcMain.handle(IPC_CHANNELS.LLM_OPENAI_OAUTH_LOGOUT, async () => {
|
|
1368
|
+
checkRateLimit(IPC_CHANNELS.LLM_OPENAI_OAUTH_LOGOUT);
|
|
1369
|
+
console.log('[IPC] Logging out of OpenAI OAuth...');
|
|
1370
|
+
|
|
1371
|
+
// Clear OAuth tokens from settings
|
|
1372
|
+
const settings = LLMProviderFactory.loadSettings();
|
|
1373
|
+
if (settings.openai) {
|
|
1374
|
+
settings.openai = {
|
|
1375
|
+
...settings.openai,
|
|
1376
|
+
accessToken: undefined,
|
|
1377
|
+
refreshToken: undefined,
|
|
1378
|
+
tokenExpiresAt: undefined,
|
|
1379
|
+
authMethod: undefined,
|
|
1380
|
+
};
|
|
1381
|
+
LLMProviderFactory.saveSettings(settings);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return { success: true };
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
ipcMain.handle(IPC_CHANNELS.LLM_GET_BEDROCK_MODELS, async (_, config?: {
|
|
1388
|
+
region?: string;
|
|
1389
|
+
accessKeyId?: string;
|
|
1390
|
+
secretAccessKey?: string;
|
|
1391
|
+
profile?: string;
|
|
1392
|
+
}) => {
|
|
1393
|
+
checkRateLimit(IPC_CHANNELS.LLM_GET_BEDROCK_MODELS);
|
|
1394
|
+
const models = await LLMProviderFactory.getBedrockModels(config);
|
|
1395
|
+
// Cache the models for use in config status
|
|
1396
|
+
const cachedModels = models.map(m => ({
|
|
1397
|
+
key: m.id,
|
|
1398
|
+
displayName: m.name,
|
|
1399
|
+
description: m.description,
|
|
1400
|
+
}));
|
|
1401
|
+
LLMProviderFactory.saveCachedModels('bedrock', cachedModels);
|
|
1402
|
+
return models;
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
// Search Settings handlers
|
|
1406
|
+
ipcMain.handle(IPC_CHANNELS.SEARCH_GET_SETTINGS, async () => {
|
|
1407
|
+
return SearchProviderFactory.loadSettings();
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
ipcMain.handle(IPC_CHANNELS.SEARCH_SAVE_SETTINGS, async (_, settings) => {
|
|
1411
|
+
checkRateLimit(IPC_CHANNELS.SEARCH_SAVE_SETTINGS);
|
|
1412
|
+
const validated = validateInput(SearchSettingsSchema, settings, 'search settings');
|
|
1413
|
+
SearchProviderFactory.saveSettings(validated as SearchSettings);
|
|
1414
|
+
SearchProviderFactory.clearCache();
|
|
1415
|
+
return { success: true };
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
ipcMain.handle(IPC_CHANNELS.SEARCH_GET_CONFIG_STATUS, async () => {
|
|
1419
|
+
return SearchProviderFactory.getConfigStatus();
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
ipcMain.handle(IPC_CHANNELS.SEARCH_TEST_PROVIDER, async (_, providerType: SearchProviderType) => {
|
|
1423
|
+
checkRateLimit(IPC_CHANNELS.SEARCH_TEST_PROVIDER);
|
|
1424
|
+
return SearchProviderFactory.testProvider(providerType);
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
// X/Twitter Settings handlers
|
|
1428
|
+
ipcMain.handle(IPC_CHANNELS.X_GET_SETTINGS, async () => {
|
|
1429
|
+
return XSettingsManager.loadSettings();
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
ipcMain.handle(IPC_CHANNELS.X_SAVE_SETTINGS, async (_, settings) => {
|
|
1433
|
+
checkRateLimit(IPC_CHANNELS.X_SAVE_SETTINGS);
|
|
1434
|
+
const validated = validateInput(XSettingsSchema, settings, 'x settings') as XSettingsData;
|
|
1435
|
+
XSettingsManager.saveSettings(validated);
|
|
1436
|
+
XSettingsManager.clearCache();
|
|
1437
|
+
return { success: true };
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
ipcMain.handle(IPC_CHANNELS.X_TEST_CONNECTION, async () => {
|
|
1441
|
+
checkRateLimit(IPC_CHANNELS.X_TEST_CONNECTION);
|
|
1442
|
+
const settings = XSettingsManager.loadSettings();
|
|
1443
|
+
return testXConnection(settings);
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
ipcMain.handle(IPC_CHANNELS.X_GET_STATUS, async () => {
|
|
1447
|
+
checkRateLimit(IPC_CHANNELS.X_GET_STATUS);
|
|
1448
|
+
const installStatus = await checkBirdInstalled();
|
|
1449
|
+
if (!installStatus.installed) {
|
|
1450
|
+
return { installed: false, connected: false };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const settings = XSettingsManager.loadSettings();
|
|
1454
|
+
if (!settings.enabled) {
|
|
1455
|
+
return { installed: true, connected: false };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const result = await testXConnection(settings);
|
|
1459
|
+
return {
|
|
1460
|
+
installed: true,
|
|
1461
|
+
connected: result.success,
|
|
1462
|
+
username: result.username,
|
|
1463
|
+
error: result.success ? undefined : result.error,
|
|
1464
|
+
};
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// Gateway / Channel handlers
|
|
1468
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_GET_CHANNELS, async () => {
|
|
1469
|
+
if (!gateway) return [];
|
|
1470
|
+
return gateway.getChannels().map(ch => ({
|
|
1471
|
+
id: ch.id,
|
|
1472
|
+
type: ch.type,
|
|
1473
|
+
name: ch.name,
|
|
1474
|
+
enabled: ch.enabled,
|
|
1475
|
+
status: ch.status,
|
|
1476
|
+
botUsername: ch.botUsername,
|
|
1477
|
+
securityMode: ch.securityConfig.mode,
|
|
1478
|
+
createdAt: ch.createdAt,
|
|
1479
|
+
config: ch.config,
|
|
1480
|
+
}));
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_ADD_CHANNEL, async (_, data) => {
|
|
1484
|
+
checkRateLimit(IPC_CHANNELS.GATEWAY_ADD_CHANNEL);
|
|
1485
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1486
|
+
|
|
1487
|
+
const validated = validateInput(AddChannelSchema, data, 'channel');
|
|
1488
|
+
|
|
1489
|
+
if (validated.type === 'telegram') {
|
|
1490
|
+
const channel = await gateway.addTelegramChannel(
|
|
1491
|
+
validated.name,
|
|
1492
|
+
validated.botToken,
|
|
1493
|
+
validated.securityMode || 'pairing'
|
|
1494
|
+
);
|
|
1495
|
+
return {
|
|
1496
|
+
id: channel.id,
|
|
1497
|
+
type: channel.type,
|
|
1498
|
+
name: channel.name,
|
|
1499
|
+
enabled: channel.enabled,
|
|
1500
|
+
status: channel.status,
|
|
1501
|
+
securityMode: channel.securityConfig.mode,
|
|
1502
|
+
createdAt: channel.createdAt,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (validated.type === 'discord') {
|
|
1507
|
+
const channel = await gateway.addDiscordChannel(
|
|
1508
|
+
validated.name,
|
|
1509
|
+
validated.botToken,
|
|
1510
|
+
validated.applicationId,
|
|
1511
|
+
validated.guildIds,
|
|
1512
|
+
validated.securityMode || 'pairing'
|
|
1513
|
+
);
|
|
1514
|
+
return {
|
|
1515
|
+
id: channel.id,
|
|
1516
|
+
type: channel.type,
|
|
1517
|
+
name: channel.name,
|
|
1518
|
+
enabled: channel.enabled,
|
|
1519
|
+
status: channel.status,
|
|
1520
|
+
securityMode: channel.securityConfig.mode,
|
|
1521
|
+
createdAt: channel.createdAt,
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (validated.type === 'slack') {
|
|
1526
|
+
const channel = await gateway.addSlackChannel(
|
|
1527
|
+
validated.name,
|
|
1528
|
+
validated.botToken,
|
|
1529
|
+
validated.appToken,
|
|
1530
|
+
validated.signingSecret,
|
|
1531
|
+
validated.securityMode || 'pairing'
|
|
1532
|
+
);
|
|
1533
|
+
return {
|
|
1534
|
+
id: channel.id,
|
|
1535
|
+
type: channel.type,
|
|
1536
|
+
name: channel.name,
|
|
1537
|
+
enabled: channel.enabled,
|
|
1538
|
+
status: channel.status,
|
|
1539
|
+
securityMode: channel.securityConfig.mode,
|
|
1540
|
+
createdAt: channel.createdAt,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (validated.type === 'whatsapp') {
|
|
1545
|
+
const channel = await gateway.addWhatsAppChannel(
|
|
1546
|
+
validated.name,
|
|
1547
|
+
validated.allowedNumbers,
|
|
1548
|
+
validated.securityMode || 'pairing',
|
|
1549
|
+
validated.selfChatMode ?? true,
|
|
1550
|
+
validated.responsePrefix ?? '🤖'
|
|
1551
|
+
);
|
|
1552
|
+
|
|
1553
|
+
// Automatically enable and connect WhatsApp to start QR code generation
|
|
1554
|
+
// This is done asynchronously to not block the response
|
|
1555
|
+
gateway.enableWhatsAppWithQRForwarding(channel.id).catch((err) => {
|
|
1556
|
+
console.error('Failed to enable WhatsApp channel:', err);
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
return {
|
|
1560
|
+
id: channel.id,
|
|
1561
|
+
type: channel.type,
|
|
1562
|
+
name: channel.name,
|
|
1563
|
+
enabled: channel.enabled,
|
|
1564
|
+
status: 'connecting', // Indicate we're connecting
|
|
1565
|
+
securityMode: channel.securityConfig.mode,
|
|
1566
|
+
createdAt: channel.createdAt,
|
|
1567
|
+
config: channel.config,
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (validated.type === 'imessage') {
|
|
1572
|
+
const channel = await gateway.addImessageChannel(
|
|
1573
|
+
validated.name,
|
|
1574
|
+
validated.cliPath,
|
|
1575
|
+
validated.dbPath,
|
|
1576
|
+
validated.allowedContacts,
|
|
1577
|
+
validated.securityMode || 'pairing',
|
|
1578
|
+
validated.dmPolicy || 'pairing',
|
|
1579
|
+
validated.groupPolicy || 'allowlist'
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1582
|
+
// Automatically enable and connect iMessage
|
|
1583
|
+
gateway.enableChannel(channel.id).catch((err) => {
|
|
1584
|
+
console.error('Failed to enable iMessage channel:', err);
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
return {
|
|
1588
|
+
id: channel.id,
|
|
1589
|
+
type: channel.type,
|
|
1590
|
+
name: channel.name,
|
|
1591
|
+
enabled: channel.enabled,
|
|
1592
|
+
status: 'connecting', // Indicate we're connecting
|
|
1593
|
+
securityMode: channel.securityConfig.mode,
|
|
1594
|
+
createdAt: channel.createdAt,
|
|
1595
|
+
config: channel.config,
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (validated.type === 'signal') {
|
|
1600
|
+
const channel = await gateway.addSignalChannel(
|
|
1601
|
+
validated.name,
|
|
1602
|
+
validated.phoneNumber,
|
|
1603
|
+
validated.dataDir,
|
|
1604
|
+
validated.securityMode || 'pairing',
|
|
1605
|
+
validated.mode || 'native',
|
|
1606
|
+
validated.trustMode || 'tofu',
|
|
1607
|
+
validated.dmPolicy || 'pairing',
|
|
1608
|
+
validated.groupPolicy || 'allowlist',
|
|
1609
|
+
validated.sendReadReceipts ?? true,
|
|
1610
|
+
validated.sendTypingIndicators ?? true
|
|
1611
|
+
);
|
|
1612
|
+
|
|
1613
|
+
// Automatically enable and connect Signal
|
|
1614
|
+
gateway.enableChannel(channel.id).catch((err) => {
|
|
1615
|
+
console.error('Failed to enable Signal channel:', err);
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
return {
|
|
1619
|
+
id: channel.id,
|
|
1620
|
+
type: channel.type,
|
|
1621
|
+
name: channel.name,
|
|
1622
|
+
enabled: channel.enabled,
|
|
1623
|
+
status: 'connecting', // Indicate we're connecting
|
|
1624
|
+
securityMode: channel.securityConfig.mode,
|
|
1625
|
+
createdAt: channel.createdAt,
|
|
1626
|
+
config: channel.config,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (validated.type === 'mattermost') {
|
|
1631
|
+
const channel = await gateway.addMattermostChannel(
|
|
1632
|
+
validated.name,
|
|
1633
|
+
validated.mattermostServerUrl!,
|
|
1634
|
+
validated.mattermostToken!,
|
|
1635
|
+
validated.mattermostTeamId,
|
|
1636
|
+
validated.securityMode || 'pairing'
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1639
|
+
// Automatically enable and connect Mattermost
|
|
1640
|
+
gateway.enableChannel(channel.id).catch((err) => {
|
|
1641
|
+
console.error('Failed to enable Mattermost channel:', err);
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
return {
|
|
1645
|
+
id: channel.id,
|
|
1646
|
+
type: channel.type,
|
|
1647
|
+
name: channel.name,
|
|
1648
|
+
enabled: channel.enabled,
|
|
1649
|
+
status: 'connecting',
|
|
1650
|
+
securityMode: channel.securityConfig.mode,
|
|
1651
|
+
createdAt: channel.createdAt,
|
|
1652
|
+
config: channel.config,
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (validated.type === 'matrix') {
|
|
1657
|
+
const channel = await gateway.addMatrixChannel(
|
|
1658
|
+
validated.name,
|
|
1659
|
+
validated.matrixHomeserver!,
|
|
1660
|
+
validated.matrixUserId!,
|
|
1661
|
+
validated.matrixAccessToken!,
|
|
1662
|
+
validated.matrixDeviceId,
|
|
1663
|
+
validated.matrixRoomIds,
|
|
1664
|
+
validated.securityMode || 'pairing'
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
// Automatically enable and connect Matrix
|
|
1668
|
+
gateway.enableChannel(channel.id).catch((err) => {
|
|
1669
|
+
console.error('Failed to enable Matrix channel:', err);
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
return {
|
|
1673
|
+
id: channel.id,
|
|
1674
|
+
type: channel.type,
|
|
1675
|
+
name: channel.name,
|
|
1676
|
+
enabled: channel.enabled,
|
|
1677
|
+
status: 'connecting',
|
|
1678
|
+
securityMode: channel.securityConfig.mode,
|
|
1679
|
+
createdAt: channel.createdAt,
|
|
1680
|
+
config: channel.config,
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (validated.type === 'twitch') {
|
|
1685
|
+
const channel = await gateway.addTwitchChannel(
|
|
1686
|
+
validated.name,
|
|
1687
|
+
validated.twitchUsername!,
|
|
1688
|
+
validated.twitchOauthToken!,
|
|
1689
|
+
validated.twitchChannels || [],
|
|
1690
|
+
validated.twitchAllowWhispers ?? false,
|
|
1691
|
+
validated.securityMode || 'pairing'
|
|
1692
|
+
);
|
|
1693
|
+
|
|
1694
|
+
// Automatically enable and connect Twitch
|
|
1695
|
+
gateway.enableChannel(channel.id).catch((err) => {
|
|
1696
|
+
console.error('Failed to enable Twitch channel:', err);
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
return {
|
|
1700
|
+
id: channel.id,
|
|
1701
|
+
type: channel.type,
|
|
1702
|
+
name: channel.name,
|
|
1703
|
+
enabled: channel.enabled,
|
|
1704
|
+
status: 'connecting',
|
|
1705
|
+
securityMode: channel.securityConfig.mode,
|
|
1706
|
+
createdAt: channel.createdAt,
|
|
1707
|
+
config: channel.config,
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
if (validated.type === 'line') {
|
|
1712
|
+
const channel = await gateway.addLineChannel(
|
|
1713
|
+
validated.name,
|
|
1714
|
+
validated.lineChannelAccessToken!,
|
|
1715
|
+
validated.lineChannelSecret!,
|
|
1716
|
+
validated.lineWebhookPort ?? 3100,
|
|
1717
|
+
validated.securityMode || 'pairing'
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
// Automatically enable and connect LINE
|
|
1721
|
+
gateway.enableChannel(channel.id).catch((err) => {
|
|
1722
|
+
console.error('Failed to enable LINE channel:', err);
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
return {
|
|
1726
|
+
id: channel.id,
|
|
1727
|
+
type: channel.type,
|
|
1728
|
+
name: channel.name,
|
|
1729
|
+
enabled: channel.enabled,
|
|
1730
|
+
status: 'connecting',
|
|
1731
|
+
securityMode: channel.securityConfig.mode,
|
|
1732
|
+
createdAt: channel.createdAt,
|
|
1733
|
+
config: channel.config,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
if (validated.type === 'bluebubbles') {
|
|
1738
|
+
const channel = await gateway.addBlueBubblesChannel(
|
|
1739
|
+
validated.name,
|
|
1740
|
+
validated.blueBubblesServerUrl!,
|
|
1741
|
+
validated.blueBubblesPassword!,
|
|
1742
|
+
validated.blueBubblesWebhookPort ?? 3101,
|
|
1743
|
+
validated.blueBubblesAllowedContacts,
|
|
1744
|
+
validated.securityMode || 'pairing'
|
|
1745
|
+
);
|
|
1746
|
+
|
|
1747
|
+
// Automatically enable and connect BlueBubbles
|
|
1748
|
+
gateway.enableChannel(channel.id).catch((err) => {
|
|
1749
|
+
console.error('Failed to enable BlueBubbles channel:', err);
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
return {
|
|
1753
|
+
id: channel.id,
|
|
1754
|
+
type: channel.type,
|
|
1755
|
+
name: channel.name,
|
|
1756
|
+
enabled: channel.enabled,
|
|
1757
|
+
status: 'connecting',
|
|
1758
|
+
securityMode: channel.securityConfig.mode,
|
|
1759
|
+
createdAt: channel.createdAt,
|
|
1760
|
+
config: channel.config,
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (validated.type === 'email') {
|
|
1765
|
+
const channel = await gateway.addEmailChannel(
|
|
1766
|
+
validated.name,
|
|
1767
|
+
validated.emailAddress!,
|
|
1768
|
+
validated.emailPassword!,
|
|
1769
|
+
validated.emailImapHost!,
|
|
1770
|
+
validated.emailSmtpHost!,
|
|
1771
|
+
validated.emailDisplayName,
|
|
1772
|
+
validated.emailAllowedSenders,
|
|
1773
|
+
validated.emailSubjectFilter,
|
|
1774
|
+
validated.securityMode || 'pairing'
|
|
1775
|
+
);
|
|
1776
|
+
|
|
1777
|
+
// Automatically enable and connect Email
|
|
1778
|
+
gateway.enableChannel(channel.id).catch((err) => {
|
|
1779
|
+
console.error('Failed to enable Email channel:', err);
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
return {
|
|
1783
|
+
id: channel.id,
|
|
1784
|
+
type: channel.type,
|
|
1785
|
+
name: channel.name,
|
|
1786
|
+
enabled: channel.enabled,
|
|
1787
|
+
status: 'connecting',
|
|
1788
|
+
securityMode: channel.securityConfig.mode,
|
|
1789
|
+
createdAt: channel.createdAt,
|
|
1790
|
+
config: channel.config,
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// TypeScript exhaustiveness check - should never reach here due to discriminated union
|
|
1795
|
+
throw new Error(`Unsupported channel type`);
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_UPDATE_CHANNEL, async (_, data) => {
|
|
1799
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1800
|
+
|
|
1801
|
+
const validated = validateInput(UpdateChannelSchema, data, 'channel update');
|
|
1802
|
+
const channel = gateway.getChannel(validated.id);
|
|
1803
|
+
if (!channel) throw new Error('Channel not found');
|
|
1804
|
+
|
|
1805
|
+
const updates: Record<string, unknown> = {};
|
|
1806
|
+
if (validated.name !== undefined) updates.name = validated.name;
|
|
1807
|
+
if (validated.securityMode !== undefined) {
|
|
1808
|
+
updates.securityConfig = { ...channel.securityConfig, mode: validated.securityMode };
|
|
1809
|
+
}
|
|
1810
|
+
if (validated.config !== undefined) {
|
|
1811
|
+
updates.config = { ...channel.config, ...validated.config };
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
gateway.updateChannel(validated.id, updates);
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_REMOVE_CHANNEL, async (_, id: string) => {
|
|
1818
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1819
|
+
await gateway.removeChannel(id);
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_ENABLE_CHANNEL, async (_, id: string) => {
|
|
1823
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1824
|
+
await gateway.enableChannel(id);
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_DISABLE_CHANNEL, async (_, id: string) => {
|
|
1828
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1829
|
+
await gateway.disableChannel(id);
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_TEST_CHANNEL, async (_, id: string) => {
|
|
1833
|
+
checkRateLimit(IPC_CHANNELS.GATEWAY_TEST_CHANNEL);
|
|
1834
|
+
if (!gateway) return { success: false, error: 'Gateway not initialized' };
|
|
1835
|
+
return gateway.testChannel(id);
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_GET_USERS, async (_, channelId: string) => {
|
|
1839
|
+
if (!gateway) return [];
|
|
1840
|
+
return gateway.getChannelUsers(channelId).map(u => ({
|
|
1841
|
+
id: u.id,
|
|
1842
|
+
channelId: u.channelId,
|
|
1843
|
+
channelUserId: u.channelUserId,
|
|
1844
|
+
displayName: u.displayName,
|
|
1845
|
+
username: u.username,
|
|
1846
|
+
allowed: u.allowed,
|
|
1847
|
+
lastSeenAt: u.lastSeenAt,
|
|
1848
|
+
}));
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_GRANT_ACCESS, async (_, data) => {
|
|
1852
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1853
|
+
const validated = validateInput(GrantAccessSchema, data, 'grant access');
|
|
1854
|
+
gateway.grantUserAccess(validated.channelId, validated.userId, validated.displayName);
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_REVOKE_ACCESS, async (_, data) => {
|
|
1858
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1859
|
+
const validated = validateInput(RevokeAccessSchema, data, 'revoke access');
|
|
1860
|
+
gateway.revokeUserAccess(validated.channelId, validated.userId);
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
ipcMain.handle(IPC_CHANNELS.GATEWAY_GENERATE_PAIRING, async (_, data) => {
|
|
1864
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1865
|
+
const validated = validateInput(GeneratePairingSchema, data, 'generate pairing');
|
|
1866
|
+
return gateway.generatePairingCode(validated.channelId, validated.userId, validated.displayName);
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
// WhatsApp-specific handlers
|
|
1870
|
+
ipcMain.handle('whatsapp:get-info', async () => {
|
|
1871
|
+
if (!gateway) return {};
|
|
1872
|
+
return gateway.getWhatsAppInfo();
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
ipcMain.handle('whatsapp:logout', async () => {
|
|
1876
|
+
if (!gateway) throw new Error('Gateway not initialized');
|
|
1877
|
+
await gateway.whatsAppLogout();
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
// App Update handlers
|
|
1881
|
+
ipcMain.handle(IPC_CHANNELS.APP_GET_VERSION, async () => {
|
|
1882
|
+
return updateManager.getVersionInfo();
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
ipcMain.handle(IPC_CHANNELS.APP_CHECK_UPDATES, async () => {
|
|
1886
|
+
return updateManager.checkForUpdates();
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
ipcMain.handle(IPC_CHANNELS.APP_DOWNLOAD_UPDATE, async (_, updateInfo: UpdateInfo) => {
|
|
1890
|
+
await updateManager.downloadAndInstallUpdate(updateInfo);
|
|
1891
|
+
return { success: true };
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
ipcMain.handle(IPC_CHANNELS.APP_INSTALL_UPDATE, async () => {
|
|
1895
|
+
await updateManager.installUpdateAndRestart();
|
|
1896
|
+
return { success: true };
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
// Guardrail Settings handlers
|
|
1900
|
+
ipcMain.handle(IPC_CHANNELS.GUARDRAIL_GET_SETTINGS, async () => {
|
|
1901
|
+
return GuardrailManager.loadSettings();
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
ipcMain.handle(IPC_CHANNELS.GUARDRAIL_SAVE_SETTINGS, async (_, settings) => {
|
|
1905
|
+
checkRateLimit(IPC_CHANNELS.GUARDRAIL_SAVE_SETTINGS);
|
|
1906
|
+
const validated = validateInput(GuardrailSettingsSchema, settings, 'guardrail settings');
|
|
1907
|
+
GuardrailManager.saveSettings(validated);
|
|
1908
|
+
GuardrailManager.clearCache();
|
|
1909
|
+
return { success: true };
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
ipcMain.handle(IPC_CHANNELS.GUARDRAIL_GET_DEFAULTS, async () => {
|
|
1913
|
+
return GuardrailManager.getDefaults();
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
// Appearance Settings handlers
|
|
1917
|
+
ipcMain.handle(IPC_CHANNELS.APPEARANCE_GET_SETTINGS, async () => {
|
|
1918
|
+
return AppearanceManager.loadSettings();
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
ipcMain.handle(IPC_CHANNELS.APPEARANCE_SAVE_SETTINGS, async (_, settings) => {
|
|
1922
|
+
AppearanceManager.saveSettings(settings);
|
|
1923
|
+
return { success: true };
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
// Personality Settings handlers
|
|
1927
|
+
// Subscribe to PersonalityManager events to broadcast changes to UI
|
|
1928
|
+
// This handles both IPC changes and tool-based changes
|
|
1929
|
+
PersonalityManager.onSettingsChanged((settings) => {
|
|
1930
|
+
broadcastPersonalitySettingsChanged(settings);
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
ipcMain.handle(IPC_CHANNELS.PERSONALITY_GET_SETTINGS, async () => {
|
|
1934
|
+
return PersonalityManager.loadSettings();
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
ipcMain.handle(IPC_CHANNELS.PERSONALITY_SAVE_SETTINGS, async (_, settings) => {
|
|
1938
|
+
PersonalityManager.saveSettings(settings);
|
|
1939
|
+
// Event emission is handled by PersonalityManager.saveSettings()
|
|
1940
|
+
return { success: true };
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
ipcMain.handle(IPC_CHANNELS.PERSONALITY_GET_DEFINITIONS, async () => {
|
|
1944
|
+
return PersonalityManager.getDefinitions();
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
ipcMain.handle(IPC_CHANNELS.PERSONALITY_GET_PERSONAS, async () => {
|
|
1948
|
+
return PersonalityManager.getPersonaDefinitions();
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
ipcMain.handle(IPC_CHANNELS.PERSONALITY_GET_RELATIONSHIP_STATS, async () => {
|
|
1952
|
+
return PersonalityManager.getRelationshipStats();
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
ipcMain.handle(IPC_CHANNELS.PERSONALITY_SET_ACTIVE, async (_, personalityId) => {
|
|
1956
|
+
PersonalityManager.setActivePersonality(personalityId);
|
|
1957
|
+
// Event emission is handled by PersonalityManager.saveSettings()
|
|
1958
|
+
return { success: true };
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
ipcMain.handle(IPC_CHANNELS.PERSONALITY_SET_PERSONA, async (_, personaId) => {
|
|
1962
|
+
PersonalityManager.setActivePersona(personaId);
|
|
1963
|
+
// Event emission is handled by PersonalityManager.saveSettings()
|
|
1964
|
+
return { success: true };
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
ipcMain.handle(IPC_CHANNELS.PERSONALITY_RESET, async (_, preserveRelationship?: boolean) => {
|
|
1968
|
+
checkRateLimit(IPC_CHANNELS.PERSONALITY_RESET);
|
|
1969
|
+
PersonalityManager.resetToDefaults(preserveRelationship);
|
|
1970
|
+
// Event emission is handled by PersonalityManager.resetToDefaults()
|
|
1971
|
+
return { success: true };
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
// Agent Role / Squad handlers
|
|
1975
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_LIST, async (_, includeInactive?: boolean) => {
|
|
1976
|
+
return agentRoleRepo.findAll(includeInactive ?? false);
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_GET, async (_, id: string) => {
|
|
1980
|
+
const validated = validateInput(UUIDSchema, id, 'agent role ID');
|
|
1981
|
+
return agentRoleRepo.findById(validated);
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_CREATE, async (_, request) => {
|
|
1985
|
+
checkRateLimit(IPC_CHANNELS.AGENT_ROLE_CREATE);
|
|
1986
|
+
// Validate name format (lowercase, alphanumeric, hyphens)
|
|
1987
|
+
if (!/^[a-z0-9-]+$/.test(request.name)) {
|
|
1988
|
+
throw new Error('Agent role name must be lowercase alphanumeric with hyphens only');
|
|
1989
|
+
}
|
|
1990
|
+
// Check for duplicate name
|
|
1991
|
+
if (agentRoleRepo.findByName(request.name)) {
|
|
1992
|
+
throw new Error(`Agent role with name "${request.name}" already exists`);
|
|
1993
|
+
}
|
|
1994
|
+
return agentRoleRepo.create(request);
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_UPDATE, async (_, request) => {
|
|
1998
|
+
checkRateLimit(IPC_CHANNELS.AGENT_ROLE_UPDATE);
|
|
1999
|
+
const validated = validateInput(UUIDSchema, request.id, 'agent role ID');
|
|
2000
|
+
const result = agentRoleRepo.update({ ...request, id: validated });
|
|
2001
|
+
if (!result) {
|
|
2002
|
+
throw new Error('Agent role not found');
|
|
2003
|
+
}
|
|
2004
|
+
return result;
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_DELETE, async (_, id: string) => {
|
|
2008
|
+
checkRateLimit(IPC_CHANNELS.AGENT_ROLE_DELETE);
|
|
2009
|
+
const validated = validateInput(UUIDSchema, id, 'agent role ID');
|
|
2010
|
+
const success = agentRoleRepo.delete(validated);
|
|
2011
|
+
if (!success) {
|
|
2012
|
+
throw new Error('Agent role not found or cannot be deleted');
|
|
2013
|
+
}
|
|
2014
|
+
return { success: true };
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_ASSIGN_TO_TASK, async (_, taskId: string, agentRoleId: string | null) => {
|
|
2018
|
+
checkRateLimit(IPC_CHANNELS.AGENT_ROLE_ASSIGN_TO_TASK);
|
|
2019
|
+
const validatedTaskId = validateInput(UUIDSchema, taskId, 'task ID');
|
|
2020
|
+
if (agentRoleId !== null) {
|
|
2021
|
+
const validatedRoleId = validateInput(UUIDSchema, agentRoleId, 'agent role ID');
|
|
2022
|
+
const role = agentRoleRepo.findById(validatedRoleId);
|
|
2023
|
+
if (!role) {
|
|
2024
|
+
throw new Error('Agent role not found');
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
const taskUpdate: Partial<Task> = { assignedAgentRoleId: agentRoleId ?? undefined };
|
|
2028
|
+
taskRepo.update(validatedTaskId, taskUpdate);
|
|
2029
|
+
const task = taskRepo.findById(validatedTaskId);
|
|
2030
|
+
if (task) {
|
|
2031
|
+
if (agentRoleId) {
|
|
2032
|
+
const role = agentRoleRepo.findById(agentRoleId);
|
|
2033
|
+
const activity = activityRepo.create({
|
|
2034
|
+
workspaceId: task.workspaceId,
|
|
2035
|
+
taskId: task.id,
|
|
2036
|
+
agentRoleId,
|
|
2037
|
+
actorType: 'system',
|
|
2038
|
+
activityType: 'agent_assigned',
|
|
2039
|
+
title: `Assigned to ${role?.displayName || 'agent'}`,
|
|
2040
|
+
description: task.title,
|
|
2041
|
+
});
|
|
2042
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity });
|
|
2043
|
+
} else {
|
|
2044
|
+
const activity = activityRepo.create({
|
|
2045
|
+
workspaceId: task.workspaceId,
|
|
2046
|
+
taskId: task.id,
|
|
2047
|
+
actorType: 'system',
|
|
2048
|
+
activityType: 'info',
|
|
2049
|
+
title: 'Task unassigned',
|
|
2050
|
+
description: task.title,
|
|
2051
|
+
});
|
|
2052
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity });
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return { success: true };
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_GET_DEFAULTS, async () => {
|
|
2059
|
+
const { DEFAULT_AGENT_ROLES } = await import('../../shared/types');
|
|
2060
|
+
return DEFAULT_AGENT_ROLES;
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_SEED_DEFAULTS, async () => {
|
|
2064
|
+
checkRateLimit(IPC_CHANNELS.AGENT_ROLE_SEED_DEFAULTS);
|
|
2065
|
+
return agentRoleRepo.seedDefaults();
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
ipcMain.handle(IPC_CHANNELS.AGENT_ROLE_SYNC_DEFAULTS, async () => {
|
|
2069
|
+
checkRateLimit(IPC_CHANNELS.AGENT_ROLE_SYNC_DEFAULTS);
|
|
2070
|
+
return agentRoleRepo.syncNewDefaults();
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
// Activity Feed handlers
|
|
2074
|
+
ipcMain.handle(IPC_CHANNELS.ACTIVITY_LIST, async (_, query: any) => {
|
|
2075
|
+
const validated = validateInput(UUIDSchema, query.workspaceId, 'workspace ID');
|
|
2076
|
+
return activityRepo.list({ ...query, workspaceId: validated });
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
ipcMain.handle(IPC_CHANNELS.ACTIVITY_CREATE, async (_, request: any) => {
|
|
2080
|
+
checkRateLimit(IPC_CHANNELS.ACTIVITY_CREATE);
|
|
2081
|
+
const validatedWorkspaceId = validateInput(UUIDSchema, request.workspaceId, 'workspace ID');
|
|
2082
|
+
const activity = activityRepo.create({ ...request, workspaceId: validatedWorkspaceId });
|
|
2083
|
+
// Emit activity event for real-time updates
|
|
2084
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity });
|
|
2085
|
+
return activity;
|
|
2086
|
+
});
|
|
2087
|
+
|
|
2088
|
+
ipcMain.handle(IPC_CHANNELS.ACTIVITY_MARK_READ, async (_, id: string) => {
|
|
2089
|
+
checkRateLimit(IPC_CHANNELS.ACTIVITY_MARK_READ);
|
|
2090
|
+
const validated = validateInput(UUIDSchema, id, 'activity ID');
|
|
2091
|
+
const success = activityRepo.markRead(validated);
|
|
2092
|
+
if (success) {
|
|
2093
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'read', id: validated });
|
|
2094
|
+
}
|
|
2095
|
+
return { success };
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
ipcMain.handle(IPC_CHANNELS.ACTIVITY_MARK_ALL_READ, async (_, workspaceId: string) => {
|
|
2099
|
+
checkRateLimit(IPC_CHANNELS.ACTIVITY_MARK_ALL_READ);
|
|
2100
|
+
const validated = validateInput(UUIDSchema, workspaceId, 'workspace ID');
|
|
2101
|
+
const count = activityRepo.markAllRead(validated);
|
|
2102
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'all_read', workspaceId: validated });
|
|
2103
|
+
return { count };
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
ipcMain.handle(IPC_CHANNELS.ACTIVITY_PIN, async (_, id: string) => {
|
|
2107
|
+
checkRateLimit(IPC_CHANNELS.ACTIVITY_PIN);
|
|
2108
|
+
const validated = validateInput(UUIDSchema, id, 'activity ID');
|
|
2109
|
+
const activity = activityRepo.togglePin(validated);
|
|
2110
|
+
if (activity) {
|
|
2111
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'pinned', activity });
|
|
2112
|
+
}
|
|
2113
|
+
return activity;
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2116
|
+
ipcMain.handle(IPC_CHANNELS.ACTIVITY_DELETE, async (_, id: string) => {
|
|
2117
|
+
checkRateLimit(IPC_CHANNELS.ACTIVITY_DELETE);
|
|
2118
|
+
const validated = validateInput(UUIDSchema, id, 'activity ID');
|
|
2119
|
+
const success = activityRepo.delete(validated);
|
|
2120
|
+
if (success) {
|
|
2121
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'deleted', id: validated });
|
|
2122
|
+
}
|
|
2123
|
+
return { success };
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
// @Mention handlers
|
|
2127
|
+
ipcMain.handle(IPC_CHANNELS.MENTION_LIST, async (_, query: any) => {
|
|
2128
|
+
return mentionRepo.list(query);
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
ipcMain.handle(IPC_CHANNELS.MENTION_CREATE, async (_, request: any) => {
|
|
2132
|
+
checkRateLimit(IPC_CHANNELS.MENTION_CREATE);
|
|
2133
|
+
const validatedWorkspaceId = validateInput(UUIDSchema, request.workspaceId, 'workspace ID');
|
|
2134
|
+
const mention = mentionRepo.create({ ...request, workspaceId: validatedWorkspaceId });
|
|
2135
|
+
// Emit mention event for real-time updates
|
|
2136
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.MENTION_EVENT, { type: 'created', mention });
|
|
2137
|
+
// Also create an activity entry for the mention
|
|
2138
|
+
const fromAgent = request.fromAgentRoleId ? agentRoleRepo.findById(request.fromAgentRoleId) : null;
|
|
2139
|
+
const toAgent = agentRoleRepo.findById(request.toAgentRoleId);
|
|
2140
|
+
activityRepo.create({
|
|
2141
|
+
workspaceId: validatedWorkspaceId,
|
|
2142
|
+
taskId: request.taskId,
|
|
2143
|
+
agentRoleId: request.toAgentRoleId,
|
|
2144
|
+
actorType: fromAgent ? 'agent' : 'user',
|
|
2145
|
+
activityType: 'mention',
|
|
2146
|
+
title: `@${toAgent?.displayName || 'Agent'} mentioned`,
|
|
2147
|
+
description: request.context,
|
|
2148
|
+
metadata: { mentionId: mention.id, mentionType: request.mentionType },
|
|
2149
|
+
});
|
|
2150
|
+
return mention;
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
ipcMain.handle(IPC_CHANNELS.MENTION_ACKNOWLEDGE, async (_, id: string) => {
|
|
2154
|
+
checkRateLimit(IPC_CHANNELS.MENTION_ACKNOWLEDGE);
|
|
2155
|
+
const validated = validateInput(UUIDSchema, id, 'mention ID');
|
|
2156
|
+
const mention = mentionRepo.acknowledge(validated);
|
|
2157
|
+
if (mention) {
|
|
2158
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.MENTION_EVENT, { type: 'acknowledged', mention });
|
|
2159
|
+
}
|
|
2160
|
+
return mention;
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
ipcMain.handle(IPC_CHANNELS.MENTION_COMPLETE, async (_, id: string) => {
|
|
2164
|
+
checkRateLimit(IPC_CHANNELS.MENTION_COMPLETE);
|
|
2165
|
+
const validated = validateInput(UUIDSchema, id, 'mention ID');
|
|
2166
|
+
const mention = mentionRepo.complete(validated);
|
|
2167
|
+
if (mention) {
|
|
2168
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.MENTION_EVENT, { type: 'completed', mention });
|
|
2169
|
+
}
|
|
2170
|
+
return mention;
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
ipcMain.handle(IPC_CHANNELS.MENTION_DISMISS, async (_, id: string) => {
|
|
2174
|
+
checkRateLimit(IPC_CHANNELS.MENTION_DISMISS);
|
|
2175
|
+
const validated = validateInput(UUIDSchema, id, 'mention ID');
|
|
2176
|
+
const mention = mentionRepo.dismiss(validated);
|
|
2177
|
+
if (mention) {
|
|
2178
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.MENTION_EVENT, { type: 'dismissed', mention });
|
|
2179
|
+
}
|
|
2180
|
+
return mention;
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
// Task Board handlers
|
|
2184
|
+
ipcMain.handle(IPC_CHANNELS.TASK_MOVE_COLUMN, async (_, taskId: string, column: string) => {
|
|
2185
|
+
checkRateLimit(IPC_CHANNELS.TASK_MOVE_COLUMN);
|
|
2186
|
+
const validatedId = validateInput(UUIDSchema, taskId, 'task ID');
|
|
2187
|
+
const task = taskRepo.moveToColumn(validatedId, column);
|
|
2188
|
+
if (task) {
|
|
2189
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.TASK_BOARD_EVENT, { type: 'moved', task, column });
|
|
2190
|
+
const columnLabels: Record<string, string> = {
|
|
2191
|
+
backlog: 'Inbox',
|
|
2192
|
+
todo: 'Assigned',
|
|
2193
|
+
in_progress: 'In Progress',
|
|
2194
|
+
review: 'Review',
|
|
2195
|
+
done: 'Done',
|
|
2196
|
+
};
|
|
2197
|
+
const activity = activityRepo.create({
|
|
2198
|
+
workspaceId: task.workspaceId,
|
|
2199
|
+
taskId: task.id,
|
|
2200
|
+
agentRoleId: task.assignedAgentRoleId,
|
|
2201
|
+
actorType: 'system',
|
|
2202
|
+
activityType: 'info',
|
|
2203
|
+
title: `Moved to ${columnLabels[column] || column}`,
|
|
2204
|
+
description: task.title,
|
|
2205
|
+
});
|
|
2206
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity });
|
|
2207
|
+
}
|
|
2208
|
+
return task;
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
ipcMain.handle(IPC_CHANNELS.TASK_SET_PRIORITY, async (_, taskId: string, priority: number) => {
|
|
2212
|
+
checkRateLimit(IPC_CHANNELS.TASK_SET_PRIORITY);
|
|
2213
|
+
const validatedId = validateInput(UUIDSchema, taskId, 'task ID');
|
|
2214
|
+
const task = taskRepo.setPriority(validatedId, priority);
|
|
2215
|
+
if (task) {
|
|
2216
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.TASK_BOARD_EVENT, { type: 'priority_changed', task });
|
|
2217
|
+
}
|
|
2218
|
+
return task;
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
ipcMain.handle(IPC_CHANNELS.TASK_SET_DUE_DATE, async (_, taskId: string, dueDate: number | null) => {
|
|
2222
|
+
checkRateLimit(IPC_CHANNELS.TASK_SET_DUE_DATE);
|
|
2223
|
+
const validatedId = validateInput(UUIDSchema, taskId, 'task ID');
|
|
2224
|
+
const task = taskRepo.setDueDate(validatedId, dueDate);
|
|
2225
|
+
if (task) {
|
|
2226
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.TASK_BOARD_EVENT, { type: 'due_date_changed', task });
|
|
2227
|
+
}
|
|
2228
|
+
return task;
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
ipcMain.handle(IPC_CHANNELS.TASK_SET_ESTIMATE, async (_, taskId: string, minutes: number | null) => {
|
|
2232
|
+
checkRateLimit(IPC_CHANNELS.TASK_SET_ESTIMATE);
|
|
2233
|
+
const validatedId = validateInput(UUIDSchema, taskId, 'task ID');
|
|
2234
|
+
const task = taskRepo.setEstimate(validatedId, minutes);
|
|
2235
|
+
if (task) {
|
|
2236
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.TASK_BOARD_EVENT, { type: 'estimate_changed', task });
|
|
2237
|
+
}
|
|
2238
|
+
return task;
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
ipcMain.handle(IPC_CHANNELS.TASK_ADD_LABEL, async (_, taskId: string, labelId: string) => {
|
|
2242
|
+
checkRateLimit(IPC_CHANNELS.TASK_ADD_LABEL);
|
|
2243
|
+
const validatedTaskId = validateInput(UUIDSchema, taskId, 'task ID');
|
|
2244
|
+
const validatedLabelId = validateInput(UUIDSchema, labelId, 'label ID');
|
|
2245
|
+
const task = taskRepo.addLabel(validatedTaskId, validatedLabelId);
|
|
2246
|
+
if (task) {
|
|
2247
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.TASK_BOARD_EVENT, { type: 'label_added', task, labelId: validatedLabelId });
|
|
2248
|
+
}
|
|
2249
|
+
return task;
|
|
2250
|
+
});
|
|
2251
|
+
|
|
2252
|
+
ipcMain.handle(IPC_CHANNELS.TASK_REMOVE_LABEL, async (_, taskId: string, labelId: string) => {
|
|
2253
|
+
checkRateLimit(IPC_CHANNELS.TASK_REMOVE_LABEL);
|
|
2254
|
+
const validatedTaskId = validateInput(UUIDSchema, taskId, 'task ID');
|
|
2255
|
+
const validatedLabelId = validateInput(UUIDSchema, labelId, 'label ID');
|
|
2256
|
+
const task = taskRepo.removeLabel(validatedTaskId, validatedLabelId);
|
|
2257
|
+
if (task) {
|
|
2258
|
+
getMainWindow()?.webContents.send(IPC_CHANNELS.TASK_BOARD_EVENT, { type: 'label_removed', task, labelId: validatedLabelId });
|
|
2259
|
+
}
|
|
2260
|
+
return task;
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
// Task Label handlers
|
|
2264
|
+
ipcMain.handle(IPC_CHANNELS.TASK_LABEL_LIST, async (_, workspaceId: string) => {
|
|
2265
|
+
const validated = validateInput(UUIDSchema, workspaceId, 'workspace ID');
|
|
2266
|
+
return taskLabelRepo.list({ workspaceId: validated });
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
ipcMain.handle(IPC_CHANNELS.TASK_LABEL_CREATE, async (_, request: any) => {
|
|
2270
|
+
checkRateLimit(IPC_CHANNELS.TASK_LABEL_CREATE);
|
|
2271
|
+
const validatedWorkspaceId = validateInput(UUIDSchema, request.workspaceId, 'workspace ID');
|
|
2272
|
+
return taskLabelRepo.create({ ...request, workspaceId: validatedWorkspaceId });
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
ipcMain.handle(IPC_CHANNELS.TASK_LABEL_UPDATE, async (_, id: string, request: any) => {
|
|
2276
|
+
checkRateLimit(IPC_CHANNELS.TASK_LABEL_UPDATE);
|
|
2277
|
+
const validated = validateInput(UUIDSchema, id, 'label ID');
|
|
2278
|
+
return taskLabelRepo.update(validated, request);
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
ipcMain.handle(IPC_CHANNELS.TASK_LABEL_DELETE, async (_, id: string) => {
|
|
2282
|
+
checkRateLimit(IPC_CHANNELS.TASK_LABEL_DELETE);
|
|
2283
|
+
const validated = validateInput(UUIDSchema, id, 'label ID');
|
|
2284
|
+
return { success: taskLabelRepo.delete(validated) };
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
// Working State handlers
|
|
2288
|
+
ipcMain.handle(IPC_CHANNELS.WORKING_STATE_GET, async (_, id: string) => {
|
|
2289
|
+
const validated = validateInput(UUIDSchema, id, 'working state ID');
|
|
2290
|
+
return workingStateRepo.findById(validated);
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2293
|
+
ipcMain.handle(IPC_CHANNELS.WORKING_STATE_GET_CURRENT, async (_, query: any) => {
|
|
2294
|
+
const validatedAgentRoleId = validateInput(UUIDSchema, query.agentRoleId, 'agent role ID');
|
|
2295
|
+
const validatedWorkspaceId = validateInput(UUIDSchema, query.workspaceId, 'workspace ID');
|
|
2296
|
+
return workingStateRepo.getCurrent({
|
|
2297
|
+
agentRoleId: validatedAgentRoleId,
|
|
2298
|
+
workspaceId: validatedWorkspaceId,
|
|
2299
|
+
taskId: query.taskId,
|
|
2300
|
+
stateType: query.stateType,
|
|
2301
|
+
});
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
ipcMain.handle(IPC_CHANNELS.WORKING_STATE_UPDATE, async (_, request: any) => {
|
|
2305
|
+
checkRateLimit(IPC_CHANNELS.WORKING_STATE_UPDATE);
|
|
2306
|
+
const validatedAgentRoleId = validateInput(UUIDSchema, request.agentRoleId, 'agent role ID');
|
|
2307
|
+
const validatedWorkspaceId = validateInput(UUIDSchema, request.workspaceId, 'workspace ID');
|
|
2308
|
+
return workingStateRepo.update({
|
|
2309
|
+
agentRoleId: validatedAgentRoleId,
|
|
2310
|
+
workspaceId: validatedWorkspaceId,
|
|
2311
|
+
taskId: request.taskId,
|
|
2312
|
+
stateType: request.stateType,
|
|
2313
|
+
content: request.content,
|
|
2314
|
+
fileReferences: request.fileReferences,
|
|
2315
|
+
});
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
ipcMain.handle(IPC_CHANNELS.WORKING_STATE_HISTORY, async (_, query: any) => {
|
|
2319
|
+
const validatedAgentRoleId = validateInput(UUIDSchema, query.agentRoleId, 'agent role ID');
|
|
2320
|
+
const validatedWorkspaceId = validateInput(UUIDSchema, query.workspaceId, 'workspace ID');
|
|
2321
|
+
return workingStateRepo.getHistory({
|
|
2322
|
+
agentRoleId: validatedAgentRoleId,
|
|
2323
|
+
workspaceId: validatedWorkspaceId,
|
|
2324
|
+
limit: query.limit,
|
|
2325
|
+
offset: query.offset,
|
|
2326
|
+
});
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
ipcMain.handle(IPC_CHANNELS.WORKING_STATE_RESTORE, async (_, id: string) => {
|
|
2330
|
+
checkRateLimit(IPC_CHANNELS.WORKING_STATE_RESTORE);
|
|
2331
|
+
const validated = validateInput(UUIDSchema, id, 'working state ID');
|
|
2332
|
+
return workingStateRepo.restore(validated);
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
ipcMain.handle(IPC_CHANNELS.WORKING_STATE_DELETE, async (_, id: string) => {
|
|
2336
|
+
checkRateLimit(IPC_CHANNELS.WORKING_STATE_DELETE);
|
|
2337
|
+
const validated = validateInput(UUIDSchema, id, 'working state ID');
|
|
2338
|
+
return { success: workingStateRepo.delete(validated) };
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
ipcMain.handle(IPC_CHANNELS.WORKING_STATE_LIST_FOR_TASK, async (_, taskId: string) => {
|
|
2342
|
+
const validated = validateInput(UUIDSchema, taskId, 'task ID');
|
|
2343
|
+
return workingStateRepo.listForTask(validated);
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
// Context Policy handlers (per-context security DM vs group)
|
|
2347
|
+
ipcMain.handle(IPC_CHANNELS.CONTEXT_POLICY_GET, async (_, channelId: string, contextType: string) => {
|
|
2348
|
+
return contextPolicyManager.getPolicy(channelId, contextType as 'dm' | 'group');
|
|
2349
|
+
});
|
|
2350
|
+
|
|
2351
|
+
ipcMain.handle(IPC_CHANNELS.CONTEXT_POLICY_GET_FOR_CHAT, async (_, channelId: string, chatId: string, isGroup: boolean) => {
|
|
2352
|
+
return contextPolicyManager.getPolicyForChat(channelId, chatId, isGroup);
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
ipcMain.handle(IPC_CHANNELS.CONTEXT_POLICY_LIST, async (_, channelId: string) => {
|
|
2356
|
+
return contextPolicyManager.getPoliciesForChannel(channelId);
|
|
2357
|
+
});
|
|
2358
|
+
|
|
2359
|
+
ipcMain.handle(IPC_CHANNELS.CONTEXT_POLICY_UPDATE, async (_, channelId: string, contextType: string, options: { securityMode?: string; toolRestrictions?: string[] }) => {
|
|
2360
|
+
checkRateLimit(IPC_CHANNELS.CONTEXT_POLICY_UPDATE);
|
|
2361
|
+
return contextPolicyManager.updateByContext(
|
|
2362
|
+
channelId,
|
|
2363
|
+
contextType as 'dm' | 'group',
|
|
2364
|
+
{
|
|
2365
|
+
securityMode: options.securityMode as 'open' | 'allowlist' | 'pairing' | undefined,
|
|
2366
|
+
toolRestrictions: options.toolRestrictions,
|
|
2367
|
+
}
|
|
2368
|
+
);
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
ipcMain.handle(IPC_CHANNELS.CONTEXT_POLICY_DELETE, async (_, channelId: string) => {
|
|
2372
|
+
checkRateLimit(IPC_CHANNELS.CONTEXT_POLICY_DELETE);
|
|
2373
|
+
return { count: contextPolicyManager.deleteByChannel(channelId) };
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
ipcMain.handle(IPC_CHANNELS.CONTEXT_POLICY_CREATE_DEFAULTS, async (_, channelId: string) => {
|
|
2377
|
+
checkRateLimit(IPC_CHANNELS.CONTEXT_POLICY_CREATE_DEFAULTS);
|
|
2378
|
+
contextPolicyManager.createDefaultPolicies(channelId);
|
|
2379
|
+
return { success: true };
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
ipcMain.handle(IPC_CHANNELS.CONTEXT_POLICY_IS_TOOL_ALLOWED, async (_, channelId: string, contextType: string, toolName: string, toolGroups: string[]) => {
|
|
2383
|
+
return { allowed: contextPolicyManager.isToolAllowed(channelId, contextType as 'dm' | 'group', toolName, toolGroups) };
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
// Queue handlers
|
|
2387
|
+
ipcMain.handle(IPC_CHANNELS.QUEUE_GET_STATUS, async () => {
|
|
2388
|
+
return agentDaemon.getQueueStatus();
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
ipcMain.handle(IPC_CHANNELS.QUEUE_GET_SETTINGS, async () => {
|
|
2392
|
+
return agentDaemon.getQueueSettings();
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
ipcMain.handle(IPC_CHANNELS.QUEUE_SAVE_SETTINGS, async (_, settings) => {
|
|
2396
|
+
checkRateLimit(IPC_CHANNELS.QUEUE_SAVE_SETTINGS);
|
|
2397
|
+
agentDaemon.saveQueueSettings(settings);
|
|
2398
|
+
return { success: true };
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
ipcMain.handle(IPC_CHANNELS.QUEUE_CLEAR, async () => {
|
|
2402
|
+
checkRateLimit(IPC_CHANNELS.QUEUE_CLEAR);
|
|
2403
|
+
const result = await agentDaemon.clearStuckTasks();
|
|
2404
|
+
return { success: true, ...result };
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
// MCP handlers
|
|
2408
|
+
setupMCPHandlers();
|
|
2409
|
+
|
|
2410
|
+
// Notification handlers
|
|
2411
|
+
setupNotificationHandlers();
|
|
2412
|
+
|
|
2413
|
+
// Hooks (Webhooks & Gmail Pub/Sub) handlers
|
|
2414
|
+
setupHooksHandlers(agentDaemon);
|
|
2415
|
+
|
|
2416
|
+
// Memory system handlers
|
|
2417
|
+
setupMemoryHandlers();
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
/**
|
|
2421
|
+
* Set up MCP (Model Context Protocol) IPC handlers
|
|
2422
|
+
*/
|
|
2423
|
+
function setupMCPHandlers(): void {
|
|
2424
|
+
// Configure rate limits for MCP channels
|
|
2425
|
+
rateLimiter.configure(IPC_CHANNELS.MCP_SAVE_SETTINGS, RATE_LIMIT_CONFIGS.limited);
|
|
2426
|
+
rateLimiter.configure(IPC_CHANNELS.MCP_CONNECT_SERVER, RATE_LIMIT_CONFIGS.expensive);
|
|
2427
|
+
rateLimiter.configure(IPC_CHANNELS.MCP_TEST_SERVER, RATE_LIMIT_CONFIGS.expensive);
|
|
2428
|
+
rateLimiter.configure(IPC_CHANNELS.MCP_REGISTRY_INSTALL, RATE_LIMIT_CONFIGS.expensive);
|
|
2429
|
+
|
|
2430
|
+
// Initialize MCP settings manager
|
|
2431
|
+
MCPSettingsManager.initialize();
|
|
2432
|
+
|
|
2433
|
+
// Get settings
|
|
2434
|
+
ipcMain.handle(IPC_CHANNELS.MCP_GET_SETTINGS, async () => {
|
|
2435
|
+
return MCPSettingsManager.getSettingsForDisplay();
|
|
2436
|
+
});
|
|
2437
|
+
|
|
2438
|
+
// Save settings
|
|
2439
|
+
ipcMain.handle(IPC_CHANNELS.MCP_SAVE_SETTINGS, async (_, settings) => {
|
|
2440
|
+
checkRateLimit(IPC_CHANNELS.MCP_SAVE_SETTINGS);
|
|
2441
|
+
const validated = validateInput(MCPSettingsSchema, settings, 'MCP settings') as MCPSettings;
|
|
2442
|
+
MCPSettingsManager.saveSettings(validated);
|
|
2443
|
+
MCPSettingsManager.clearCache();
|
|
2444
|
+
return { success: true };
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
// Get all servers
|
|
2448
|
+
ipcMain.handle(IPC_CHANNELS.MCP_GET_SERVERS, async () => {
|
|
2449
|
+
const settings = MCPSettingsManager.loadSettings();
|
|
2450
|
+
return settings.servers;
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2453
|
+
// Add a server
|
|
2454
|
+
ipcMain.handle(IPC_CHANNELS.MCP_ADD_SERVER, async (_, serverConfig) => {
|
|
2455
|
+
checkRateLimit(IPC_CHANNELS.MCP_ADD_SERVER);
|
|
2456
|
+
const validated = validateInput(MCPServerConfigSchema, serverConfig, 'MCP server config');
|
|
2457
|
+
const { id: _id, ...configWithoutId } = validated;
|
|
2458
|
+
return MCPSettingsManager.addServer(configWithoutId as Omit<MCPServerConfig, 'id'>);
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
// Update a server
|
|
2462
|
+
ipcMain.handle(IPC_CHANNELS.MCP_UPDATE_SERVER, async (_, serverId: string, updates) => {
|
|
2463
|
+
const validatedId = validateInput(UUIDSchema, serverId, 'server ID');
|
|
2464
|
+
const validatedUpdates = validateInput(MCPServerUpdateSchema, updates, 'server updates') as Partial<MCPServerConfig>;
|
|
2465
|
+
return MCPSettingsManager.updateServer(validatedId, validatedUpdates);
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
// Remove a server
|
|
2469
|
+
ipcMain.handle(IPC_CHANNELS.MCP_REMOVE_SERVER, async (_, serverId: string) => {
|
|
2470
|
+
const validatedId = validateInput(UUIDSchema, serverId, 'server ID');
|
|
2471
|
+
|
|
2472
|
+
// Disconnect if connected
|
|
2473
|
+
try {
|
|
2474
|
+
await MCPClientManager.getInstance().disconnectServer(validatedId);
|
|
2475
|
+
} catch {
|
|
2476
|
+
// Ignore errors
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
return MCPSettingsManager.removeServer(validatedId);
|
|
2480
|
+
});
|
|
2481
|
+
|
|
2482
|
+
// Connect to a server
|
|
2483
|
+
ipcMain.handle(IPC_CHANNELS.MCP_CONNECT_SERVER, async (_, serverId: string) => {
|
|
2484
|
+
checkRateLimit(IPC_CHANNELS.MCP_CONNECT_SERVER);
|
|
2485
|
+
const validatedId = validateInput(UUIDSchema, serverId, 'server ID');
|
|
2486
|
+
await MCPClientManager.getInstance().connectServer(validatedId);
|
|
2487
|
+
return { success: true };
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
// Disconnect from a server
|
|
2491
|
+
ipcMain.handle(IPC_CHANNELS.MCP_DISCONNECT_SERVER, async (_, serverId: string) => {
|
|
2492
|
+
const validatedId = validateInput(UUIDSchema, serverId, 'server ID');
|
|
2493
|
+
await MCPClientManager.getInstance().disconnectServer(validatedId);
|
|
2494
|
+
return { success: true };
|
|
2495
|
+
});
|
|
2496
|
+
|
|
2497
|
+
// Get status of all servers
|
|
2498
|
+
ipcMain.handle(IPC_CHANNELS.MCP_GET_STATUS, async () => {
|
|
2499
|
+
return MCPClientManager.getInstance().getStatus();
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
// Get tools from a specific server
|
|
2503
|
+
ipcMain.handle(IPC_CHANNELS.MCP_GET_SERVER_TOOLS, async (_, serverId: string) => {
|
|
2504
|
+
const validatedId = validateInput(UUIDSchema, serverId, 'server ID');
|
|
2505
|
+
return MCPClientManager.getInstance().getServerTools(validatedId);
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
// Test server connection
|
|
2509
|
+
ipcMain.handle(IPC_CHANNELS.MCP_TEST_SERVER, async (_, serverId: string) => {
|
|
2510
|
+
checkRateLimit(IPC_CHANNELS.MCP_TEST_SERVER);
|
|
2511
|
+
const validatedId = validateInput(UUIDSchema, serverId, 'server ID');
|
|
2512
|
+
return MCPClientManager.getInstance().testServer(validatedId);
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
// MCP Registry handlers
|
|
2516
|
+
ipcMain.handle(IPC_CHANNELS.MCP_REGISTRY_FETCH, async () => {
|
|
2517
|
+
const registry = await MCPRegistryManager.fetchRegistry();
|
|
2518
|
+
const categories = await MCPRegistryManager.getCategories();
|
|
2519
|
+
const featured = registry.servers.filter(s => s.featured);
|
|
2520
|
+
return { ...registry, categories, featured };
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
ipcMain.handle(IPC_CHANNELS.MCP_REGISTRY_SEARCH, async (_, options) => {
|
|
2524
|
+
const validatedOptions = validateInput(MCPRegistrySearchSchema, options, 'registry search options');
|
|
2525
|
+
return MCPRegistryManager.searchServers(validatedOptions);
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2528
|
+
ipcMain.handle(IPC_CHANNELS.MCP_REGISTRY_INSTALL, async (_, entryId: string) => {
|
|
2529
|
+
checkRateLimit(IPC_CHANNELS.MCP_REGISTRY_INSTALL);
|
|
2530
|
+
const validatedId = validateInput(StringIdSchema, entryId, 'registry entry ID');
|
|
2531
|
+
return MCPRegistryManager.installServer(validatedId);
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
ipcMain.handle(IPC_CHANNELS.MCP_REGISTRY_UNINSTALL, async (_, serverId: string) => {
|
|
2535
|
+
const validatedId = validateInput(UUIDSchema, serverId, 'server ID');
|
|
2536
|
+
|
|
2537
|
+
// Disconnect if connected
|
|
2538
|
+
try {
|
|
2539
|
+
await MCPClientManager.getInstance().disconnectServer(validatedId);
|
|
2540
|
+
} catch {
|
|
2541
|
+
// Ignore errors
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
await MCPRegistryManager.uninstallServer(validatedId);
|
|
2545
|
+
});
|
|
2546
|
+
|
|
2547
|
+
ipcMain.handle(IPC_CHANNELS.MCP_REGISTRY_CHECK_UPDATES, async () => {
|
|
2548
|
+
return MCPRegistryManager.checkForUpdates();
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
ipcMain.handle(IPC_CHANNELS.MCP_REGISTRY_UPDATE_SERVER, async (_, serverId: string) => {
|
|
2552
|
+
const validatedId = validateInput(UUIDSchema, serverId, 'server ID');
|
|
2553
|
+
return MCPRegistryManager.updateServer(validatedId);
|
|
2554
|
+
});
|
|
2555
|
+
|
|
2556
|
+
// MCP Host handlers
|
|
2557
|
+
ipcMain.handle(IPC_CHANNELS.MCP_HOST_START, async () => {
|
|
2558
|
+
const hostServer = MCPHostServer.getInstance();
|
|
2559
|
+
|
|
2560
|
+
// If no tool provider is set, create a minimal one that exposes MCP tools
|
|
2561
|
+
// from connected servers (useful for tool aggregation/forwarding)
|
|
2562
|
+
if (!hostServer.hasToolProvider()) {
|
|
2563
|
+
const mcpClientManager = MCPClientManager.getInstance();
|
|
2564
|
+
|
|
2565
|
+
// Create a minimal tool provider that exposes MCP tools
|
|
2566
|
+
hostServer.setToolProvider({
|
|
2567
|
+
getTools() {
|
|
2568
|
+
return mcpClientManager.getAllTools();
|
|
2569
|
+
},
|
|
2570
|
+
async executeTool(name: string, args: Record<string, any>) {
|
|
2571
|
+
return mcpClientManager.callTool(name, args);
|
|
2572
|
+
},
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
await hostServer.startStdio();
|
|
2577
|
+
return { success: true };
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2580
|
+
ipcMain.handle(IPC_CHANNELS.MCP_HOST_STOP, async () => {
|
|
2581
|
+
const hostServer = MCPHostServer.getInstance();
|
|
2582
|
+
await hostServer.stop();
|
|
2583
|
+
return { success: true };
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
ipcMain.handle(IPC_CHANNELS.MCP_HOST_GET_STATUS, async () => {
|
|
2587
|
+
const hostServer = MCPHostServer.getInstance();
|
|
2588
|
+
return {
|
|
2589
|
+
running: hostServer.isRunning(),
|
|
2590
|
+
toolCount: hostServer.hasToolProvider() ? MCPClientManager.getInstance().getAllTools().length : 0,
|
|
2591
|
+
};
|
|
2592
|
+
});
|
|
2593
|
+
|
|
2594
|
+
// =====================
|
|
2595
|
+
// Built-in Tools Settings Handlers
|
|
2596
|
+
// =====================
|
|
2597
|
+
|
|
2598
|
+
ipcMain.handle(IPC_CHANNELS.BUILTIN_TOOLS_GET_SETTINGS, async () => {
|
|
2599
|
+
return BuiltinToolsSettingsManager.loadSettings();
|
|
2600
|
+
});
|
|
2601
|
+
|
|
2602
|
+
ipcMain.handle(IPC_CHANNELS.BUILTIN_TOOLS_SAVE_SETTINGS, async (_, settings) => {
|
|
2603
|
+
BuiltinToolsSettingsManager.saveSettings(settings);
|
|
2604
|
+
BuiltinToolsSettingsManager.clearCache(); // Clear cache to force reload
|
|
2605
|
+
return { success: true };
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
ipcMain.handle(IPC_CHANNELS.BUILTIN_TOOLS_GET_CATEGORIES, async () => {
|
|
2609
|
+
return BuiltinToolsSettingsManager.getToolsByCategory();
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
// =====================
|
|
2613
|
+
// Tray (Menu Bar) Handlers
|
|
2614
|
+
// =====================
|
|
2615
|
+
|
|
2616
|
+
ipcMain.handle(IPC_CHANNELS.TRAY_GET_SETTINGS, async () => {
|
|
2617
|
+
// Import trayManager lazily to avoid circular dependencies
|
|
2618
|
+
const { trayManager } = await import('../tray');
|
|
2619
|
+
return trayManager.getSettings();
|
|
2620
|
+
});
|
|
2621
|
+
|
|
2622
|
+
ipcMain.handle(IPC_CHANNELS.TRAY_SAVE_SETTINGS, async (_, settings) => {
|
|
2623
|
+
const { trayManager } = await import('../tray');
|
|
2624
|
+
trayManager.saveSettings(settings);
|
|
2625
|
+
return { success: true };
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
// =====================
|
|
2629
|
+
// Cron (Scheduled Tasks) Handlers
|
|
2630
|
+
// =====================
|
|
2631
|
+
setupCronHandlers();
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
/**
|
|
2635
|
+
* Set up Cron (Scheduled Tasks) IPC handlers
|
|
2636
|
+
*/
|
|
2637
|
+
function setupCronHandlers(): void {
|
|
2638
|
+
const { getCronService } = require('../cron');
|
|
2639
|
+
|
|
2640
|
+
// Get service status
|
|
2641
|
+
ipcMain.handle(IPC_CHANNELS.CRON_GET_STATUS, async () => {
|
|
2642
|
+
const service = getCronService();
|
|
2643
|
+
if (!service) {
|
|
2644
|
+
return {
|
|
2645
|
+
enabled: false,
|
|
2646
|
+
storePath: '',
|
|
2647
|
+
jobCount: 0,
|
|
2648
|
+
enabledJobCount: 0,
|
|
2649
|
+
nextWakeAtMs: null,
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
return service.status();
|
|
2653
|
+
});
|
|
2654
|
+
|
|
2655
|
+
// List all jobs
|
|
2656
|
+
ipcMain.handle(IPC_CHANNELS.CRON_LIST_JOBS, async (_, opts?: { includeDisabled?: boolean }) => {
|
|
2657
|
+
const service = getCronService();
|
|
2658
|
+
if (!service) return [];
|
|
2659
|
+
return service.list(opts);
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
// Get a single job
|
|
2663
|
+
ipcMain.handle(IPC_CHANNELS.CRON_GET_JOB, async (_, id: string) => {
|
|
2664
|
+
const service = getCronService();
|
|
2665
|
+
if (!service) return null;
|
|
2666
|
+
return service.get(id);
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2669
|
+
// Add a new job
|
|
2670
|
+
ipcMain.handle(IPC_CHANNELS.CRON_ADD_JOB, async (_, jobData) => {
|
|
2671
|
+
const service = getCronService();
|
|
2672
|
+
if (!service) {
|
|
2673
|
+
return { ok: false, error: 'Cron service not initialized' };
|
|
2674
|
+
}
|
|
2675
|
+
return service.add(jobData);
|
|
2676
|
+
});
|
|
2677
|
+
|
|
2678
|
+
// Update an existing job
|
|
2679
|
+
ipcMain.handle(IPC_CHANNELS.CRON_UPDATE_JOB, async (_, id: string, patch) => {
|
|
2680
|
+
const service = getCronService();
|
|
2681
|
+
if (!service) {
|
|
2682
|
+
return { ok: false, error: 'Cron service not initialized' };
|
|
2683
|
+
}
|
|
2684
|
+
return service.update(id, patch);
|
|
2685
|
+
});
|
|
2686
|
+
|
|
2687
|
+
// Remove a job
|
|
2688
|
+
ipcMain.handle(IPC_CHANNELS.CRON_REMOVE_JOB, async (_, id: string) => {
|
|
2689
|
+
const service = getCronService();
|
|
2690
|
+
if (!service) {
|
|
2691
|
+
return { ok: false, removed: false, error: 'Cron service not initialized' };
|
|
2692
|
+
}
|
|
2693
|
+
return service.remove(id);
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
// Run a job immediately
|
|
2697
|
+
ipcMain.handle(IPC_CHANNELS.CRON_RUN_JOB, async (_, id: string, mode?: 'due' | 'force') => {
|
|
2698
|
+
const service = getCronService();
|
|
2699
|
+
if (!service) {
|
|
2700
|
+
return { ok: false, error: 'Cron service not initialized' };
|
|
2701
|
+
}
|
|
2702
|
+
return service.run(id, mode);
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2705
|
+
// Get run history for a job
|
|
2706
|
+
ipcMain.handle('cron:getRunHistory', async (_, id: string) => {
|
|
2707
|
+
const service = getCronService();
|
|
2708
|
+
if (!service) return null;
|
|
2709
|
+
return service.getRunHistory(id);
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
// Clear run history for a job
|
|
2713
|
+
ipcMain.handle('cron:clearRunHistory', async (_, id: string) => {
|
|
2714
|
+
const service = getCronService();
|
|
2715
|
+
if (!service) return false;
|
|
2716
|
+
return service.clearRunHistory(id);
|
|
2717
|
+
});
|
|
2718
|
+
|
|
2719
|
+
// Get webhook status
|
|
2720
|
+
ipcMain.handle('cron:getWebhookStatus', async () => {
|
|
2721
|
+
const service = getCronService();
|
|
2722
|
+
if (!service) return { enabled: false };
|
|
2723
|
+
const status = await service.status();
|
|
2724
|
+
return status.webhook ?? { enabled: false };
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
/**
|
|
2729
|
+
* Set up Notification IPC handlers
|
|
2730
|
+
*/
|
|
2731
|
+
function setupNotificationHandlers(): void {
|
|
2732
|
+
// Initialize notification service with event forwarding to main window
|
|
2733
|
+
notificationService = new NotificationService({
|
|
2734
|
+
onEvent: (event) => {
|
|
2735
|
+
// Forward notification events to renderer
|
|
2736
|
+
// We need to import BrowserWindow from electron to send to all windows
|
|
2737
|
+
const { BrowserWindow } = require('electron');
|
|
2738
|
+
const windows = BrowserWindow.getAllWindows();
|
|
2739
|
+
for (const win of windows) {
|
|
2740
|
+
if (win.webContents) {
|
|
2741
|
+
win.webContents.send(IPC_CHANNELS.NOTIFICATION_EVENT, event);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
},
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
console.log('[Notifications] Service initialized');
|
|
2748
|
+
|
|
2749
|
+
// List all notifications
|
|
2750
|
+
ipcMain.handle(IPC_CHANNELS.NOTIFICATION_LIST, async () => {
|
|
2751
|
+
if (!notificationService) return [];
|
|
2752
|
+
return notificationService.list();
|
|
2753
|
+
});
|
|
2754
|
+
|
|
2755
|
+
// Get unread count
|
|
2756
|
+
ipcMain.handle('notification:unreadCount', async () => {
|
|
2757
|
+
if (!notificationService) return 0;
|
|
2758
|
+
return notificationService.getUnreadCount();
|
|
2759
|
+
});
|
|
2760
|
+
|
|
2761
|
+
// Mark notification as read
|
|
2762
|
+
ipcMain.handle(IPC_CHANNELS.NOTIFICATION_MARK_READ, async (_, id: string) => {
|
|
2763
|
+
if (!notificationService) return null;
|
|
2764
|
+
return notificationService.markRead(id);
|
|
2765
|
+
});
|
|
2766
|
+
|
|
2767
|
+
// Mark all notifications as read
|
|
2768
|
+
ipcMain.handle(IPC_CHANNELS.NOTIFICATION_MARK_ALL_READ, async () => {
|
|
2769
|
+
if (!notificationService) return;
|
|
2770
|
+
await notificationService.markAllRead();
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
// Delete a notification
|
|
2774
|
+
ipcMain.handle(IPC_CHANNELS.NOTIFICATION_DELETE, async (_, id: string) => {
|
|
2775
|
+
if (!notificationService) return false;
|
|
2776
|
+
return notificationService.delete(id);
|
|
2777
|
+
});
|
|
2778
|
+
|
|
2779
|
+
// Delete all notifications
|
|
2780
|
+
ipcMain.handle(IPC_CHANNELS.NOTIFICATION_DELETE_ALL, async () => {
|
|
2781
|
+
if (!notificationService) return;
|
|
2782
|
+
await notificationService.deleteAll();
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
// Add a notification (internal use, for programmatic notifications)
|
|
2786
|
+
ipcMain.handle(IPC_CHANNELS.NOTIFICATION_ADD, async (_, data: {
|
|
2787
|
+
type: NotificationType;
|
|
2788
|
+
title: string;
|
|
2789
|
+
message: string;
|
|
2790
|
+
taskId?: string;
|
|
2791
|
+
cronJobId?: string;
|
|
2792
|
+
workspaceId?: string;
|
|
2793
|
+
}) => {
|
|
2794
|
+
if (!notificationService) return null;
|
|
2795
|
+
return notificationService.add(data);
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// Global hooks server instance
|
|
2800
|
+
let hooksServer: HooksServer | null = null;
|
|
2801
|
+
let hooksServerStarting = false; // Lock to prevent concurrent server creation
|
|
2802
|
+
|
|
2803
|
+
/**
|
|
2804
|
+
* Get the hooks server instance
|
|
2805
|
+
*/
|
|
2806
|
+
export function getHooksServer(): HooksServer | null {
|
|
2807
|
+
return hooksServer;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
/**
|
|
2811
|
+
* Set up Hooks (Webhooks & Gmail Pub/Sub) IPC handlers
|
|
2812
|
+
*/
|
|
2813
|
+
function setupHooksHandlers(agentDaemon: AgentDaemon): void {
|
|
2814
|
+
// Initialize settings manager
|
|
2815
|
+
HooksSettingsManager.initialize();
|
|
2816
|
+
|
|
2817
|
+
// Get hooks settings
|
|
2818
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_GET_SETTINGS, async (): Promise<HooksSettingsData> => {
|
|
2819
|
+
const settings = HooksSettingsManager.getSettingsForDisplay();
|
|
2820
|
+
return {
|
|
2821
|
+
enabled: settings.enabled,
|
|
2822
|
+
token: settings.token,
|
|
2823
|
+
path: settings.path,
|
|
2824
|
+
maxBodyBytes: settings.maxBodyBytes,
|
|
2825
|
+
port: DEFAULT_HOOKS_PORT,
|
|
2826
|
+
host: '127.0.0.1',
|
|
2827
|
+
presets: settings.presets,
|
|
2828
|
+
mappings: settings.mappings as HookMappingData[],
|
|
2829
|
+
gmail: settings.gmail as GmailHooksSettingsData | undefined,
|
|
2830
|
+
};
|
|
2831
|
+
});
|
|
2832
|
+
|
|
2833
|
+
// Save hooks settings
|
|
2834
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_SAVE_SETTINGS, async (_, data: Partial<HooksSettingsData>) => {
|
|
2835
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_SAVE_SETTINGS, RATE_LIMIT_CONFIGS.limited);
|
|
2836
|
+
|
|
2837
|
+
const currentSettings = HooksSettingsManager.loadSettings();
|
|
2838
|
+
const updated = HooksSettingsManager.updateConfig({
|
|
2839
|
+
...currentSettings,
|
|
2840
|
+
enabled: data.enabled ?? currentSettings.enabled,
|
|
2841
|
+
token: data.token ?? currentSettings.token,
|
|
2842
|
+
path: data.path ?? currentSettings.path,
|
|
2843
|
+
maxBodyBytes: data.maxBodyBytes ?? currentSettings.maxBodyBytes,
|
|
2844
|
+
presets: data.presets ?? currentSettings.presets,
|
|
2845
|
+
mappings: data.mappings ?? currentSettings.mappings,
|
|
2846
|
+
gmail: data.gmail ?? currentSettings.gmail,
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2849
|
+
// Restart hooks server if needed
|
|
2850
|
+
if (hooksServer && updated.enabled) {
|
|
2851
|
+
hooksServer.setHooksConfig(updated);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
return {
|
|
2855
|
+
enabled: updated.enabled,
|
|
2856
|
+
token: updated.token ? '***configured***' : '',
|
|
2857
|
+
path: updated.path,
|
|
2858
|
+
maxBodyBytes: updated.maxBodyBytes,
|
|
2859
|
+
port: DEFAULT_HOOKS_PORT,
|
|
2860
|
+
host: '127.0.0.1',
|
|
2861
|
+
presets: updated.presets,
|
|
2862
|
+
mappings: updated.mappings as HookMappingData[],
|
|
2863
|
+
gmail: updated.gmail as GmailHooksSettingsData | undefined,
|
|
2864
|
+
};
|
|
2865
|
+
});
|
|
2866
|
+
|
|
2867
|
+
// Enable hooks
|
|
2868
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_ENABLE, async () => {
|
|
2869
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_ENABLE, RATE_LIMIT_CONFIGS.limited);
|
|
2870
|
+
|
|
2871
|
+
// Prevent concurrent enable attempts
|
|
2872
|
+
if (hooksServerStarting) {
|
|
2873
|
+
throw new Error('Hooks server is already starting. Please wait.');
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
const settings = HooksSettingsManager.enableHooks();
|
|
2877
|
+
|
|
2878
|
+
// Start the hooks server if not running
|
|
2879
|
+
if (!hooksServer) {
|
|
2880
|
+
hooksServerStarting = true;
|
|
2881
|
+
|
|
2882
|
+
const server = new HooksServer({
|
|
2883
|
+
port: DEFAULT_HOOKS_PORT,
|
|
2884
|
+
host: '127.0.0.1',
|
|
2885
|
+
enabled: true,
|
|
2886
|
+
});
|
|
2887
|
+
|
|
2888
|
+
server.setHooksConfig(settings);
|
|
2889
|
+
|
|
2890
|
+
// Set up handlers for hook actions
|
|
2891
|
+
server.setHandlers({
|
|
2892
|
+
onWake: async (action) => {
|
|
2893
|
+
console.log('[Hooks] Wake action:', action);
|
|
2894
|
+
// For now, just log. In the future, this could trigger a heartbeat
|
|
2895
|
+
},
|
|
2896
|
+
onAgent: async (action) => {
|
|
2897
|
+
console.log('[Hooks] Agent action:', action.message.substring(0, 100));
|
|
2898
|
+
|
|
2899
|
+
// Create a task for the agent action
|
|
2900
|
+
const task = await agentDaemon.createTask({
|
|
2901
|
+
title: action.name || 'Webhook Task',
|
|
2902
|
+
prompt: action.message,
|
|
2903
|
+
workspaceId: action.workspaceId || TEMP_WORKSPACE_ID,
|
|
2904
|
+
});
|
|
2905
|
+
|
|
2906
|
+
return { taskId: task.id };
|
|
2907
|
+
},
|
|
2908
|
+
onEvent: (event) => {
|
|
2909
|
+
console.log('[Hooks] Server event:', event.action);
|
|
2910
|
+
// Forward events to renderer (with error handling for destroyed windows)
|
|
2911
|
+
const windows = BrowserWindow.getAllWindows();
|
|
2912
|
+
for (const win of windows) {
|
|
2913
|
+
try {
|
|
2914
|
+
if (win.webContents && !win.isDestroyed()) {
|
|
2915
|
+
win.webContents.send(IPC_CHANNELS.HOOKS_EVENT, event);
|
|
2916
|
+
}
|
|
2917
|
+
} catch (err) {
|
|
2918
|
+
// Window may have been destroyed between check and send
|
|
2919
|
+
console.warn('[Hooks] Failed to send event to window:', err);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
},
|
|
2923
|
+
});
|
|
2924
|
+
|
|
2925
|
+
try {
|
|
2926
|
+
await server.start();
|
|
2927
|
+
hooksServer = server;
|
|
2928
|
+
} catch (err) {
|
|
2929
|
+
console.error('[Hooks] Failed to start hooks server:', err);
|
|
2930
|
+
throw new Error(`Failed to start hooks server: ${err instanceof Error ? err.message : String(err)}`);
|
|
2931
|
+
} finally {
|
|
2932
|
+
hooksServerStarting = false;
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// Start Gmail watcher if configured (capture result for response)
|
|
2937
|
+
let gmailWatcherError: string | undefined;
|
|
2938
|
+
if (settings.gmail?.account) {
|
|
2939
|
+
try {
|
|
2940
|
+
const result = await startGmailWatcher(settings);
|
|
2941
|
+
if (!result.started) {
|
|
2942
|
+
gmailWatcherError = result.reason;
|
|
2943
|
+
console.warn('[Hooks] Gmail watcher not started:', result.reason);
|
|
2944
|
+
}
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
gmailWatcherError = err instanceof Error ? err.message : String(err);
|
|
2947
|
+
console.error('[Hooks] Failed to start Gmail watcher:', err);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
return { enabled: true, gmailWatcherError };
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2954
|
+
// Disable hooks
|
|
2955
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_DISABLE, async () => {
|
|
2956
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_DISABLE, RATE_LIMIT_CONFIGS.limited);
|
|
2957
|
+
|
|
2958
|
+
HooksSettingsManager.disableHooks();
|
|
2959
|
+
|
|
2960
|
+
// Stop the hooks server
|
|
2961
|
+
if (hooksServer) {
|
|
2962
|
+
await hooksServer.stop();
|
|
2963
|
+
hooksServer = null;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// Stop Gmail watcher
|
|
2967
|
+
await stopGmailWatcher();
|
|
2968
|
+
|
|
2969
|
+
return { enabled: false };
|
|
2970
|
+
});
|
|
2971
|
+
|
|
2972
|
+
// Regenerate hook token
|
|
2973
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_REGENERATE_TOKEN, async () => {
|
|
2974
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_REGENERATE_TOKEN, RATE_LIMIT_CONFIGS.limited);
|
|
2975
|
+
const newToken = HooksSettingsManager.regenerateToken();
|
|
2976
|
+
|
|
2977
|
+
// Update the running server with new token
|
|
2978
|
+
if (hooksServer) {
|
|
2979
|
+
const settings = HooksSettingsManager.loadSettings();
|
|
2980
|
+
hooksServer.setHooksConfig(settings);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
return { token: newToken };
|
|
2984
|
+
});
|
|
2985
|
+
|
|
2986
|
+
// Get hooks status
|
|
2987
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_GET_STATUS, async (): Promise<HooksStatus> => {
|
|
2988
|
+
const settings = HooksSettingsManager.loadSettings();
|
|
2989
|
+
const gogAvailable = await isGogAvailable();
|
|
2990
|
+
|
|
2991
|
+
return {
|
|
2992
|
+
enabled: settings.enabled,
|
|
2993
|
+
serverRunning: hooksServer?.isRunning() ?? false,
|
|
2994
|
+
serverAddress: hooksServer?.getAddress() ?? undefined,
|
|
2995
|
+
gmailWatcherRunning: isGmailWatcherRunning(),
|
|
2996
|
+
gmailAccount: settings.gmail?.account,
|
|
2997
|
+
gogAvailable,
|
|
2998
|
+
};
|
|
2999
|
+
});
|
|
3000
|
+
|
|
3001
|
+
// Add a hook mapping
|
|
3002
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_ADD_MAPPING, async (_, mapping: HookMappingData) => {
|
|
3003
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_ADD_MAPPING, RATE_LIMIT_CONFIGS.limited);
|
|
3004
|
+
|
|
3005
|
+
// Validate the mapping input
|
|
3006
|
+
const validated = validateInput(HookMappingSchema, mapping, 'hook mapping');
|
|
3007
|
+
|
|
3008
|
+
const settings = HooksSettingsManager.addMapping(validated);
|
|
3009
|
+
|
|
3010
|
+
// Update the server config if running
|
|
3011
|
+
if (hooksServer) {
|
|
3012
|
+
hooksServer.setHooksConfig(settings);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
return { ok: true };
|
|
3016
|
+
});
|
|
3017
|
+
|
|
3018
|
+
// Remove a hook mapping
|
|
3019
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_REMOVE_MAPPING, async (_, id: string) => {
|
|
3020
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_REMOVE_MAPPING, RATE_LIMIT_CONFIGS.limited);
|
|
3021
|
+
|
|
3022
|
+
// Validate the mapping ID
|
|
3023
|
+
const validatedId = validateInput(StringIdSchema, id, 'mapping ID');
|
|
3024
|
+
|
|
3025
|
+
const settings = HooksSettingsManager.removeMapping(validatedId);
|
|
3026
|
+
|
|
3027
|
+
// Update the server config if running
|
|
3028
|
+
if (hooksServer) {
|
|
3029
|
+
hooksServer.setHooksConfig(settings);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
return { ok: true };
|
|
3033
|
+
});
|
|
3034
|
+
|
|
3035
|
+
// Configure Gmail hooks
|
|
3036
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_CONFIGURE_GMAIL, async (_, config: GmailHooksSettingsData) => {
|
|
3037
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_CONFIGURE_GMAIL, RATE_LIMIT_CONFIGS.limited);
|
|
3038
|
+
|
|
3039
|
+
// Generate push token if not provided
|
|
3040
|
+
if (!config.pushToken) {
|
|
3041
|
+
config.pushToken = generateHookToken();
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
const settings = HooksSettingsManager.configureGmail(config);
|
|
3045
|
+
|
|
3046
|
+
// Update the server config if running
|
|
3047
|
+
if (hooksServer) {
|
|
3048
|
+
hooksServer.setHooksConfig(settings);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
return {
|
|
3052
|
+
ok: true,
|
|
3053
|
+
gmail: HooksSettingsManager.getGmailConfig(),
|
|
3054
|
+
};
|
|
3055
|
+
});
|
|
3056
|
+
|
|
3057
|
+
// Get Gmail watcher status
|
|
3058
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_GET_GMAIL_STATUS, async () => {
|
|
3059
|
+
const settings = HooksSettingsManager.loadSettings();
|
|
3060
|
+
const gogAvailable = await isGogAvailable();
|
|
3061
|
+
|
|
3062
|
+
return {
|
|
3063
|
+
configured: HooksSettingsManager.isGmailConfigured(),
|
|
3064
|
+
running: isGmailWatcherRunning(),
|
|
3065
|
+
account: settings.gmail?.account,
|
|
3066
|
+
topic: settings.gmail?.topic,
|
|
3067
|
+
gogAvailable,
|
|
3068
|
+
};
|
|
3069
|
+
});
|
|
3070
|
+
|
|
3071
|
+
// Start Gmail watcher manually
|
|
3072
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_START_GMAIL_WATCHER, async () => {
|
|
3073
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_START_GMAIL_WATCHER, RATE_LIMIT_CONFIGS.expensive);
|
|
3074
|
+
|
|
3075
|
+
const settings = HooksSettingsManager.loadSettings();
|
|
3076
|
+
if (!settings.enabled) {
|
|
3077
|
+
return { ok: false, error: 'Hooks must be enabled first' };
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
if (!HooksSettingsManager.isGmailConfigured()) {
|
|
3081
|
+
return { ok: false, error: 'Gmail hooks not configured' };
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
const result = await startGmailWatcher(settings);
|
|
3085
|
+
return { ok: result.started, error: result.reason };
|
|
3086
|
+
});
|
|
3087
|
+
|
|
3088
|
+
// Stop Gmail watcher manually
|
|
3089
|
+
ipcMain.handle(IPC_CHANNELS.HOOKS_STOP_GMAIL_WATCHER, async () => {
|
|
3090
|
+
checkRateLimit(IPC_CHANNELS.HOOKS_STOP_GMAIL_WATCHER, RATE_LIMIT_CONFIGS.limited);
|
|
3091
|
+
await stopGmailWatcher();
|
|
3092
|
+
return { ok: true };
|
|
3093
|
+
});
|
|
3094
|
+
|
|
3095
|
+
console.log('[Hooks] IPC handlers initialized');
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
/**
|
|
3099
|
+
* Broadcast personality settings changed event to all renderer windows.
|
|
3100
|
+
* This allows the UI to stay in sync when settings are changed via tools.
|
|
3101
|
+
*/
|
|
3102
|
+
function broadcastPersonalitySettingsChanged(settings: any): void {
|
|
3103
|
+
try {
|
|
3104
|
+
const windows = BrowserWindow.getAllWindows();
|
|
3105
|
+
for (const win of windows) {
|
|
3106
|
+
try {
|
|
3107
|
+
if (win.webContents && !win.isDestroyed()) {
|
|
3108
|
+
win.webContents.send(IPC_CHANNELS.PERSONALITY_SETTINGS_CHANGED, settings);
|
|
3109
|
+
}
|
|
3110
|
+
} catch (err) {
|
|
3111
|
+
// Window may have been destroyed between check and send
|
|
3112
|
+
console.warn('[Personality] Failed to send settings changed event to window:', err);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
} catch (err) {
|
|
3116
|
+
console.error('[Personality] Failed to broadcast settings changed:', err);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
/**
|
|
3121
|
+
* Set up Memory System IPC handlers
|
|
3122
|
+
*/
|
|
3123
|
+
function setupMemoryHandlers(): void {
|
|
3124
|
+
// Get memory settings for a workspace
|
|
3125
|
+
ipcMain.handle(IPC_CHANNELS.MEMORY_GET_SETTINGS, async (_, workspaceId: string) => {
|
|
3126
|
+
try {
|
|
3127
|
+
return MemoryService.getSettings(workspaceId);
|
|
3128
|
+
} catch (error) {
|
|
3129
|
+
console.error('[Memory] Failed to get settings:', error);
|
|
3130
|
+
// Return default settings if service not initialized
|
|
3131
|
+
return {
|
|
3132
|
+
workspaceId,
|
|
3133
|
+
enabled: true,
|
|
3134
|
+
autoCapture: true,
|
|
3135
|
+
compressionEnabled: true,
|
|
3136
|
+
retentionDays: 90,
|
|
3137
|
+
maxStorageMb: 100,
|
|
3138
|
+
privacyMode: 'normal',
|
|
3139
|
+
excludedPatterns: [],
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
|
|
3144
|
+
// Save memory settings for a workspace
|
|
3145
|
+
ipcMain.handle(
|
|
3146
|
+
IPC_CHANNELS.MEMORY_SAVE_SETTINGS,
|
|
3147
|
+
async (_, data: { workspaceId: string; settings: Partial<MemorySettings> }) => {
|
|
3148
|
+
checkRateLimit(IPC_CHANNELS.MEMORY_SAVE_SETTINGS, RATE_LIMIT_CONFIGS.limited);
|
|
3149
|
+
try {
|
|
3150
|
+
MemoryService.updateSettings(data.workspaceId, data.settings);
|
|
3151
|
+
return { success: true };
|
|
3152
|
+
} catch (error) {
|
|
3153
|
+
console.error('[Memory] Failed to save settings:', error);
|
|
3154
|
+
throw error;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
);
|
|
3158
|
+
|
|
3159
|
+
// Search memories
|
|
3160
|
+
ipcMain.handle(
|
|
3161
|
+
IPC_CHANNELS.MEMORY_SEARCH,
|
|
3162
|
+
async (_, data: { workspaceId: string; query: string; limit?: number }) => {
|
|
3163
|
+
try {
|
|
3164
|
+
return MemoryService.search(data.workspaceId, data.query, data.limit);
|
|
3165
|
+
} catch (error) {
|
|
3166
|
+
console.error('[Memory] Failed to search:', error);
|
|
3167
|
+
return [];
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
);
|
|
3171
|
+
|
|
3172
|
+
// Get timeline context (Layer 2)
|
|
3173
|
+
ipcMain.handle(
|
|
3174
|
+
IPC_CHANNELS.MEMORY_GET_TIMELINE,
|
|
3175
|
+
async (_, data: { memoryId: string; windowSize?: number }) => {
|
|
3176
|
+
try {
|
|
3177
|
+
return MemoryService.getTimelineContext(data.memoryId, data.windowSize);
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
console.error('[Memory] Failed to get timeline:', error);
|
|
3180
|
+
return [];
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
);
|
|
3184
|
+
|
|
3185
|
+
// Get full details (Layer 3)
|
|
3186
|
+
ipcMain.handle(IPC_CHANNELS.MEMORY_GET_DETAILS, async (_, ids: string[]) => {
|
|
3187
|
+
try {
|
|
3188
|
+
return MemoryService.getFullDetails(ids);
|
|
3189
|
+
} catch (error) {
|
|
3190
|
+
console.error('[Memory] Failed to get details:', error);
|
|
3191
|
+
return [];
|
|
3192
|
+
}
|
|
3193
|
+
});
|
|
3194
|
+
|
|
3195
|
+
// Get recent memories
|
|
3196
|
+
ipcMain.handle(
|
|
3197
|
+
IPC_CHANNELS.MEMORY_GET_RECENT,
|
|
3198
|
+
async (_, data: { workspaceId: string; limit?: number }) => {
|
|
3199
|
+
try {
|
|
3200
|
+
return MemoryService.getRecent(data.workspaceId, data.limit);
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
console.error('[Memory] Failed to get recent:', error);
|
|
3203
|
+
return [];
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
);
|
|
3207
|
+
|
|
3208
|
+
// Get memory statistics
|
|
3209
|
+
ipcMain.handle(IPC_CHANNELS.MEMORY_GET_STATS, async (_, workspaceId: string) => {
|
|
3210
|
+
try {
|
|
3211
|
+
return MemoryService.getStats(workspaceId);
|
|
3212
|
+
} catch (error) {
|
|
3213
|
+
console.error('[Memory] Failed to get stats:', error);
|
|
3214
|
+
return { count: 0, totalTokens: 0, compressedCount: 0, compressionRatio: 0 };
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3217
|
+
|
|
3218
|
+
// Clear all memories for a workspace
|
|
3219
|
+
ipcMain.handle(IPC_CHANNELS.MEMORY_CLEAR, async (_, workspaceId: string) => {
|
|
3220
|
+
checkRateLimit(IPC_CHANNELS.MEMORY_CLEAR, RATE_LIMIT_CONFIGS.limited);
|
|
3221
|
+
try {
|
|
3222
|
+
MemoryService.clearWorkspace(workspaceId);
|
|
3223
|
+
return { success: true };
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
console.error('[Memory] Failed to clear:', error);
|
|
3226
|
+
throw error;
|
|
3227
|
+
}
|
|
3228
|
+
});
|
|
3229
|
+
|
|
3230
|
+
console.log('[Memory] Handlers initialized');
|
|
3231
|
+
|
|
3232
|
+
// === Migration Status Handlers ===
|
|
3233
|
+
// These handlers help show one-time notifications after app migration (cowork-oss → cowork-os)
|
|
3234
|
+
|
|
3235
|
+
const userDataPath = app.getPath('userData');
|
|
3236
|
+
const migrationMarkerPath = path.join(userDataPath, '.migrated-from-cowork-oss');
|
|
3237
|
+
const notificationDismissedPath = path.join(userDataPath, '.migration-notification-dismissed');
|
|
3238
|
+
|
|
3239
|
+
// Get migration status
|
|
3240
|
+
ipcMain.handle(IPC_CHANNELS.MIGRATION_GET_STATUS, async () => {
|
|
3241
|
+
try {
|
|
3242
|
+
const migrated = fsSync.existsSync(migrationMarkerPath);
|
|
3243
|
+
const notificationDismissed = fsSync.existsSync(notificationDismissedPath);
|
|
3244
|
+
|
|
3245
|
+
let timestamp: string | undefined;
|
|
3246
|
+
if (migrated) {
|
|
3247
|
+
try {
|
|
3248
|
+
const markerContent = fsSync.readFileSync(migrationMarkerPath, 'utf-8');
|
|
3249
|
+
const markerData = JSON.parse(markerContent);
|
|
3250
|
+
timestamp = markerData.timestamp;
|
|
3251
|
+
} catch {
|
|
3252
|
+
// Old format marker or read error
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
return {
|
|
3257
|
+
migrated,
|
|
3258
|
+
notificationDismissed,
|
|
3259
|
+
timestamp,
|
|
3260
|
+
};
|
|
3261
|
+
} catch (error) {
|
|
3262
|
+
console.error('[Migration] Failed to get status:', error);
|
|
3263
|
+
return { migrated: false, notificationDismissed: true }; // Default to no notification on error
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
3266
|
+
|
|
3267
|
+
// Dismiss migration notification (user has acknowledged it)
|
|
3268
|
+
ipcMain.handle(IPC_CHANNELS.MIGRATION_DISMISS_NOTIFICATION, async () => {
|
|
3269
|
+
try {
|
|
3270
|
+
fsSync.writeFileSync(notificationDismissedPath, JSON.stringify({
|
|
3271
|
+
dismissedAt: new Date().toISOString(),
|
|
3272
|
+
}));
|
|
3273
|
+
console.log('[Migration] Notification dismissed');
|
|
3274
|
+
return { success: true };
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
console.error('[Migration] Failed to dismiss notification:', error);
|
|
3277
|
+
throw error;
|
|
3278
|
+
}
|
|
3279
|
+
});
|
|
3280
|
+
|
|
3281
|
+
console.log('[Migration] Handlers initialized');
|
|
3282
|
+
|
|
3283
|
+
// === Extension / Plugin Handlers ===
|
|
3284
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
3285
|
+
const { getPluginRegistry } = require('../extensions/registry');
|
|
3286
|
+
|
|
3287
|
+
// List all extensions
|
|
3288
|
+
ipcMain.handle(IPC_CHANNELS.EXTENSIONS_LIST, async () => {
|
|
3289
|
+
try {
|
|
3290
|
+
const registry = getPluginRegistry();
|
|
3291
|
+
const plugins = registry.getPlugins();
|
|
3292
|
+
return plugins.map((p: any) => ({
|
|
3293
|
+
name: p.manifest.name,
|
|
3294
|
+
displayName: p.manifest.displayName,
|
|
3295
|
+
version: p.manifest.version,
|
|
3296
|
+
description: p.manifest.description,
|
|
3297
|
+
author: p.manifest.author,
|
|
3298
|
+
type: p.manifest.type,
|
|
3299
|
+
state: p.state,
|
|
3300
|
+
path: p.path,
|
|
3301
|
+
loadedAt: p.loadedAt.getTime(),
|
|
3302
|
+
error: p.error?.message,
|
|
3303
|
+
capabilities: p.manifest.capabilities,
|
|
3304
|
+
configSchema: p.manifest.configSchema,
|
|
3305
|
+
}));
|
|
3306
|
+
} catch (error) {
|
|
3307
|
+
console.error('[Extensions] Failed to list:', error);
|
|
3308
|
+
return [];
|
|
3309
|
+
}
|
|
3310
|
+
});
|
|
3311
|
+
|
|
3312
|
+
// Get single extension
|
|
3313
|
+
ipcMain.handle(IPC_CHANNELS.EXTENSIONS_GET, async (_, name: string) => {
|
|
3314
|
+
try {
|
|
3315
|
+
const registry = getPluginRegistry();
|
|
3316
|
+
const plugin = registry.getPlugin(name);
|
|
3317
|
+
if (!plugin) return null;
|
|
3318
|
+
return {
|
|
3319
|
+
name: plugin.manifest.name,
|
|
3320
|
+
displayName: plugin.manifest.displayName,
|
|
3321
|
+
version: plugin.manifest.version,
|
|
3322
|
+
description: plugin.manifest.description,
|
|
3323
|
+
author: plugin.manifest.author,
|
|
3324
|
+
type: plugin.manifest.type,
|
|
3325
|
+
state: plugin.state,
|
|
3326
|
+
path: plugin.path,
|
|
3327
|
+
loadedAt: plugin.loadedAt.getTime(),
|
|
3328
|
+
error: plugin.error?.message,
|
|
3329
|
+
capabilities: plugin.manifest.capabilities,
|
|
3330
|
+
configSchema: plugin.manifest.configSchema,
|
|
3331
|
+
};
|
|
3332
|
+
} catch (error) {
|
|
3333
|
+
console.error('[Extensions] Failed to get:', error);
|
|
3334
|
+
return null;
|
|
3335
|
+
}
|
|
3336
|
+
});
|
|
3337
|
+
|
|
3338
|
+
// Enable extension
|
|
3339
|
+
ipcMain.handle(IPC_CHANNELS.EXTENSIONS_ENABLE, async (_, name: string) => {
|
|
3340
|
+
try {
|
|
3341
|
+
const registry = getPluginRegistry();
|
|
3342
|
+
await registry.enablePlugin(name);
|
|
3343
|
+
return { success: true };
|
|
3344
|
+
} catch (error: any) {
|
|
3345
|
+
console.error('[Extensions] Failed to enable:', error);
|
|
3346
|
+
return { success: false, error: error.message };
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
|
|
3350
|
+
// Disable extension
|
|
3351
|
+
ipcMain.handle(IPC_CHANNELS.EXTENSIONS_DISABLE, async (_, name: string) => {
|
|
3352
|
+
try {
|
|
3353
|
+
const registry = getPluginRegistry();
|
|
3354
|
+
await registry.disablePlugin(name);
|
|
3355
|
+
return { success: true };
|
|
3356
|
+
} catch (error: any) {
|
|
3357
|
+
console.error('[Extensions] Failed to disable:', error);
|
|
3358
|
+
return { success: false, error: error.message };
|
|
3359
|
+
}
|
|
3360
|
+
});
|
|
3361
|
+
|
|
3362
|
+
// Reload extension
|
|
3363
|
+
ipcMain.handle(IPC_CHANNELS.EXTENSIONS_RELOAD, async (_, name: string) => {
|
|
3364
|
+
try {
|
|
3365
|
+
const registry = getPluginRegistry();
|
|
3366
|
+
await registry.reloadPlugin(name);
|
|
3367
|
+
return { success: true };
|
|
3368
|
+
} catch (error: any) {
|
|
3369
|
+
console.error('[Extensions] Failed to reload:', error);
|
|
3370
|
+
return { success: false, error: error.message };
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
// Get extension config
|
|
3375
|
+
ipcMain.handle(IPC_CHANNELS.EXTENSIONS_GET_CONFIG, async (_, name: string) => {
|
|
3376
|
+
try {
|
|
3377
|
+
const registry = getPluginRegistry();
|
|
3378
|
+
return registry.getPluginConfig(name) || {};
|
|
3379
|
+
} catch (error) {
|
|
3380
|
+
console.error('[Extensions] Failed to get config:', error);
|
|
3381
|
+
return {};
|
|
3382
|
+
}
|
|
3383
|
+
});
|
|
3384
|
+
|
|
3385
|
+
// Set extension config
|
|
3386
|
+
ipcMain.handle(IPC_CHANNELS.EXTENSIONS_SET_CONFIG, async (_, data: { name: string; config: Record<string, unknown> }) => {
|
|
3387
|
+
try {
|
|
3388
|
+
const registry = getPluginRegistry();
|
|
3389
|
+
await registry.setPluginConfig(data.name, data.config);
|
|
3390
|
+
return { success: true };
|
|
3391
|
+
} catch (error: any) {
|
|
3392
|
+
console.error('[Extensions] Failed to set config:', error);
|
|
3393
|
+
return { success: false, error: error.message };
|
|
3394
|
+
}
|
|
3395
|
+
});
|
|
3396
|
+
|
|
3397
|
+
// Discover extensions (re-scan directories)
|
|
3398
|
+
ipcMain.handle(IPC_CHANNELS.EXTENSIONS_DISCOVER, async () => {
|
|
3399
|
+
try {
|
|
3400
|
+
const registry = getPluginRegistry();
|
|
3401
|
+
await registry.initialize();
|
|
3402
|
+
const plugins = registry.getPlugins();
|
|
3403
|
+
return plugins.map((p: any) => ({
|
|
3404
|
+
name: p.manifest.name,
|
|
3405
|
+
displayName: p.manifest.displayName,
|
|
3406
|
+
version: p.manifest.version,
|
|
3407
|
+
description: p.manifest.description,
|
|
3408
|
+
type: p.manifest.type,
|
|
3409
|
+
state: p.state,
|
|
3410
|
+
}));
|
|
3411
|
+
} catch (error) {
|
|
3412
|
+
console.error('[Extensions] Failed to discover:', error);
|
|
3413
|
+
return [];
|
|
3414
|
+
}
|
|
3415
|
+
});
|
|
3416
|
+
|
|
3417
|
+
console.log('[Extensions] Handlers initialized');
|
|
3418
|
+
|
|
3419
|
+
// === Webhook Tunnel Handlers ===
|
|
3420
|
+
let tunnelManager: any = null;
|
|
3421
|
+
|
|
3422
|
+
// Get tunnel status
|
|
3423
|
+
ipcMain.handle(IPC_CHANNELS.TUNNEL_GET_STATUS, async () => {
|
|
3424
|
+
try {
|
|
3425
|
+
if (!tunnelManager) {
|
|
3426
|
+
return { status: 'stopped' };
|
|
3427
|
+
}
|
|
3428
|
+
return {
|
|
3429
|
+
status: tunnelManager.status,
|
|
3430
|
+
provider: tunnelManager.config?.provider,
|
|
3431
|
+
url: tunnelManager.url,
|
|
3432
|
+
error: tunnelManager.error?.message,
|
|
3433
|
+
startedAt: tunnelManager.startedAt?.getTime(),
|
|
3434
|
+
};
|
|
3435
|
+
} catch (error) {
|
|
3436
|
+
console.error('[Tunnel] Failed to get status:', error);
|
|
3437
|
+
return { status: 'stopped' };
|
|
3438
|
+
}
|
|
3439
|
+
});
|
|
3440
|
+
|
|
3441
|
+
// Start tunnel
|
|
3442
|
+
ipcMain.handle(IPC_CHANNELS.TUNNEL_START, async (_, config: any) => {
|
|
3443
|
+
try {
|
|
3444
|
+
const { TunnelManager } = await import('../gateway/tunnel');
|
|
3445
|
+
if (tunnelManager) {
|
|
3446
|
+
await tunnelManager.stop();
|
|
3447
|
+
}
|
|
3448
|
+
tunnelManager = new TunnelManager(config);
|
|
3449
|
+
const url = await tunnelManager.start();
|
|
3450
|
+
return { success: true, url };
|
|
3451
|
+
} catch (error: any) {
|
|
3452
|
+
console.error('[Tunnel] Failed to start:', error);
|
|
3453
|
+
return { success: false, error: error.message };
|
|
3454
|
+
}
|
|
3455
|
+
});
|
|
3456
|
+
|
|
3457
|
+
// Stop tunnel
|
|
3458
|
+
ipcMain.handle(IPC_CHANNELS.TUNNEL_STOP, async () => {
|
|
3459
|
+
try {
|
|
3460
|
+
if (tunnelManager) {
|
|
3461
|
+
await tunnelManager.stop();
|
|
3462
|
+
tunnelManager = null;
|
|
3463
|
+
}
|
|
3464
|
+
return { success: true };
|
|
3465
|
+
} catch (error: any) {
|
|
3466
|
+
console.error('[Tunnel] Failed to stop:', error);
|
|
3467
|
+
return { success: false, error: error.message };
|
|
3468
|
+
}
|
|
3469
|
+
});
|
|
3470
|
+
|
|
3471
|
+
console.log('[Tunnel] Handlers initialized');
|
|
3472
|
+
|
|
3473
|
+
// === Voice Mode Handlers ===
|
|
3474
|
+
|
|
3475
|
+
// Initialize voice settings manager with secure database storage
|
|
3476
|
+
const voiceDb = DatabaseManager.getInstance().getDatabase();
|
|
3477
|
+
VoiceSettingsManager.initialize(voiceDb);
|
|
3478
|
+
|
|
3479
|
+
// Get voice settings
|
|
3480
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_GET_SETTINGS, async () => {
|
|
3481
|
+
try {
|
|
3482
|
+
return VoiceSettingsManager.loadSettings();
|
|
3483
|
+
} catch (error) {
|
|
3484
|
+
console.error('[Voice] Failed to get settings:', error);
|
|
3485
|
+
throw error;
|
|
3486
|
+
}
|
|
3487
|
+
});
|
|
3488
|
+
|
|
3489
|
+
// Save voice settings
|
|
3490
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_SAVE_SETTINGS, async (_, settings: any) => {
|
|
3491
|
+
try {
|
|
3492
|
+
const updated = VoiceSettingsManager.updateSettings(settings);
|
|
3493
|
+
// Update the voice service with new settings
|
|
3494
|
+
const voiceService = getVoiceService();
|
|
3495
|
+
voiceService.updateSettings(updated);
|
|
3496
|
+
return updated;
|
|
3497
|
+
} catch (error) {
|
|
3498
|
+
console.error('[Voice] Failed to save settings:', error);
|
|
3499
|
+
throw error;
|
|
3500
|
+
}
|
|
3501
|
+
});
|
|
3502
|
+
|
|
3503
|
+
// Get voice state
|
|
3504
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_GET_STATE, async () => {
|
|
3505
|
+
try {
|
|
3506
|
+
const voiceService = getVoiceService();
|
|
3507
|
+
return voiceService.getState();
|
|
3508
|
+
} catch (error) {
|
|
3509
|
+
console.error('[Voice] Failed to get state:', error);
|
|
3510
|
+
throw error;
|
|
3511
|
+
}
|
|
3512
|
+
});
|
|
3513
|
+
|
|
3514
|
+
// Speak text - returns audio data for renderer to play
|
|
3515
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_SPEAK, async (_, text: string) => {
|
|
3516
|
+
try {
|
|
3517
|
+
const voiceService = getVoiceService();
|
|
3518
|
+
const audioBuffer = await voiceService.speak(text);
|
|
3519
|
+
if (audioBuffer) {
|
|
3520
|
+
// Return audio data as array for serialization over IPC
|
|
3521
|
+
return { success: true, audioData: Array.from(audioBuffer) };
|
|
3522
|
+
}
|
|
3523
|
+
return { success: true, audioData: null };
|
|
3524
|
+
} catch (error: any) {
|
|
3525
|
+
console.error('[Voice] Failed to speak:', error);
|
|
3526
|
+
return { success: false, error: error.message, audioData: null };
|
|
3527
|
+
}
|
|
3528
|
+
});
|
|
3529
|
+
|
|
3530
|
+
// Stop speaking
|
|
3531
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_STOP_SPEAKING, async () => {
|
|
3532
|
+
try {
|
|
3533
|
+
const voiceService = getVoiceService();
|
|
3534
|
+
voiceService.stopSpeaking();
|
|
3535
|
+
return { success: true };
|
|
3536
|
+
} catch (error: any) {
|
|
3537
|
+
console.error('[Voice] Failed to stop speaking:', error);
|
|
3538
|
+
return { success: false, error: error.message };
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
|
|
3542
|
+
// Transcribe audio - accepts audio data as array from renderer
|
|
3543
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_TRANSCRIBE, async (_, audioData: number[]) => {
|
|
3544
|
+
try {
|
|
3545
|
+
const voiceService = getVoiceService();
|
|
3546
|
+
// Convert array back to Buffer
|
|
3547
|
+
const audioBuffer = Buffer.from(audioData);
|
|
3548
|
+
const text = await voiceService.transcribe(audioBuffer);
|
|
3549
|
+
return { text };
|
|
3550
|
+
} catch (error: any) {
|
|
3551
|
+
console.error('[Voice] Failed to transcribe:', error);
|
|
3552
|
+
return { text: '', error: error.message };
|
|
3553
|
+
}
|
|
3554
|
+
});
|
|
3555
|
+
|
|
3556
|
+
// Get ElevenLabs voices
|
|
3557
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_GET_ELEVENLABS_VOICES, async () => {
|
|
3558
|
+
try {
|
|
3559
|
+
const voiceService = getVoiceService();
|
|
3560
|
+
return await voiceService.getElevenLabsVoices();
|
|
3561
|
+
} catch (error: any) {
|
|
3562
|
+
console.error('[Voice] Failed to get ElevenLabs voices:', error);
|
|
3563
|
+
return [];
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
// Test ElevenLabs connection
|
|
3568
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_TEST_ELEVENLABS, async () => {
|
|
3569
|
+
try {
|
|
3570
|
+
const voiceService = getVoiceService();
|
|
3571
|
+
return await voiceService.testElevenLabsConnection();
|
|
3572
|
+
} catch (error: any) {
|
|
3573
|
+
console.error('[Voice] Failed to test ElevenLabs:', error);
|
|
3574
|
+
return { success: false, error: error.message };
|
|
3575
|
+
}
|
|
3576
|
+
});
|
|
3577
|
+
|
|
3578
|
+
// Test OpenAI voice connection
|
|
3579
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_TEST_OPENAI, async () => {
|
|
3580
|
+
try {
|
|
3581
|
+
const voiceService = getVoiceService();
|
|
3582
|
+
return await voiceService.testOpenAIConnection();
|
|
3583
|
+
} catch (error: any) {
|
|
3584
|
+
console.error('[Voice] Failed to test OpenAI voice:', error);
|
|
3585
|
+
return { success: false, error: error.message };
|
|
3586
|
+
}
|
|
3587
|
+
});
|
|
3588
|
+
|
|
3589
|
+
// Test Azure OpenAI voice connection
|
|
3590
|
+
ipcMain.handle(IPC_CHANNELS.VOICE_TEST_AZURE, async () => {
|
|
3591
|
+
try {
|
|
3592
|
+
const voiceService = getVoiceService();
|
|
3593
|
+
return await voiceService.testAzureConnection();
|
|
3594
|
+
} catch (error: any) {
|
|
3595
|
+
console.error('[Voice] Failed to test Azure OpenAI voice:', error);
|
|
3596
|
+
return { success: false, error: error.message };
|
|
3597
|
+
}
|
|
3598
|
+
});
|
|
3599
|
+
|
|
3600
|
+
// Initialize voice service with saved settings
|
|
3601
|
+
const savedVoiceSettings = VoiceSettingsManager.loadSettings();
|
|
3602
|
+
const voiceService = getVoiceService({ settings: savedVoiceSettings });
|
|
3603
|
+
|
|
3604
|
+
// Forward voice events to renderer
|
|
3605
|
+
voiceService.on('stateChange', (state) => {
|
|
3606
|
+
const mainWindow = getMainWindow();
|
|
3607
|
+
if (mainWindow) {
|
|
3608
|
+
mainWindow.webContents.send(IPC_CHANNELS.VOICE_EVENT, {
|
|
3609
|
+
type: 'voice:state-changed',
|
|
3610
|
+
data: state,
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
voiceService.on('speakingStart', (text) => {
|
|
3616
|
+
const mainWindow = getMainWindow();
|
|
3617
|
+
if (mainWindow) {
|
|
3618
|
+
mainWindow.webContents.send(IPC_CHANNELS.VOICE_EVENT, {
|
|
3619
|
+
type: 'voice:speaking-start',
|
|
3620
|
+
data: text,
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
});
|
|
3624
|
+
|
|
3625
|
+
voiceService.on('speakingEnd', () => {
|
|
3626
|
+
const mainWindow = getMainWindow();
|
|
3627
|
+
if (mainWindow) {
|
|
3628
|
+
mainWindow.webContents.send(IPC_CHANNELS.VOICE_EVENT, {
|
|
3629
|
+
type: 'voice:speaking-end',
|
|
3630
|
+
data: null,
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
voiceService.on('transcript', (text) => {
|
|
3636
|
+
const mainWindow = getMainWindow();
|
|
3637
|
+
if (mainWindow) {
|
|
3638
|
+
mainWindow.webContents.send(IPC_CHANNELS.VOICE_EVENT, {
|
|
3639
|
+
type: 'voice:transcript',
|
|
3640
|
+
data: text,
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
});
|
|
3644
|
+
|
|
3645
|
+
voiceService.on('error', (error) => {
|
|
3646
|
+
const mainWindow = getMainWindow();
|
|
3647
|
+
if (mainWindow) {
|
|
3648
|
+
mainWindow.webContents.send(IPC_CHANNELS.VOICE_EVENT, {
|
|
3649
|
+
type: 'voice:error',
|
|
3650
|
+
data: { message: error.message },
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
});
|
|
3654
|
+
|
|
3655
|
+
// Initialize voice service
|
|
3656
|
+
voiceService.initialize().catch((err) => {
|
|
3657
|
+
console.error('[Voice] Failed to initialize:', err);
|
|
3658
|
+
});
|
|
3659
|
+
|
|
3660
|
+
console.log('[Voice] Handlers initialized');
|
|
3661
|
+
}
|