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.
Files changed (526) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1638 -0
  3. package/bin/cowork.js +42 -0
  4. package/build/entitlements.mac.plist +16 -0
  5. package/build/icon.icns +0 -0
  6. package/build/icon.png +0 -0
  7. package/dist/electron/electron/activity/ActivityRepository.js +190 -0
  8. package/dist/electron/electron/agent/browser/browser-service.js +639 -0
  9. package/dist/electron/electron/agent/context-manager.js +225 -0
  10. package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
  11. package/dist/electron/electron/agent/daemon.js +975 -0
  12. package/dist/electron/electron/agent/executor.js +3561 -0
  13. package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
  14. package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
  15. package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
  16. package/dist/electron/electron/agent/llm/index.js +34 -0
  17. package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
  18. package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
  19. package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
  20. package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
  21. package/dist/electron/electron/agent/llm/pricing.js +160 -0
  22. package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
  23. package/dist/electron/electron/agent/llm/types.js +178 -0
  24. package/dist/electron/electron/agent/queue-manager.js +378 -0
  25. package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
  26. package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
  27. package/dist/electron/electron/agent/sandbox/runner.js +410 -0
  28. package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
  29. package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
  30. package/dist/electron/electron/agent/search/brave-provider.js +119 -0
  31. package/dist/electron/electron/agent/search/google-provider.js +100 -0
  32. package/dist/electron/electron/agent/search/index.js +28 -0
  33. package/dist/electron/electron/agent/search/provider-factory.js +395 -0
  34. package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
  35. package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
  36. package/dist/electron/electron/agent/search/types.js +40 -0
  37. package/dist/electron/electron/agent/security/index.js +12 -0
  38. package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
  39. package/dist/electron/electron/agent/security/output-filter.js +217 -0
  40. package/dist/electron/electron/agent/skill-eligibility.js +281 -0
  41. package/dist/electron/electron/agent/skill-registry.js +396 -0
  42. package/dist/electron/electron/agent/skills/document.js +878 -0
  43. package/dist/electron/electron/agent/skills/image-generator.js +225 -0
  44. package/dist/electron/electron/agent/skills/organizer.js +141 -0
  45. package/dist/electron/electron/agent/skills/presentation.js +367 -0
  46. package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
  47. package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
  48. package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
  49. package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
  50. package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
  51. package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
  52. package/dist/electron/electron/agent/tools/file-tools.js +719 -0
  53. package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
  54. package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
  55. package/dist/electron/electron/agent/tools/image-tools.js +111 -0
  56. package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
  57. package/dist/electron/electron/agent/tools/node-tools.js +476 -0
  58. package/dist/electron/electron/agent/tools/registry.js +2719 -0
  59. package/dist/electron/electron/agent/tools/search-tools.js +91 -0
  60. package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
  61. package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
  62. package/dist/electron/electron/agent/tools/system-tools.js +578 -0
  63. package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
  64. package/dist/electron/electron/agent/tools/x-tools.js +264 -0
  65. package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
  66. package/dist/electron/electron/agents/HeartbeatService.js +356 -0
  67. package/dist/electron/electron/agents/MentionRepository.js +197 -0
  68. package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
  69. package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
  70. package/dist/electron/electron/canvas/canvas-manager.js +714 -0
  71. package/dist/electron/electron/canvas/canvas-preload.js +53 -0
  72. package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
  73. package/dist/electron/electron/canvas/canvas-store.js +174 -0
  74. package/dist/electron/electron/canvas/index.js +13 -0
  75. package/dist/electron/electron/control-plane/client.js +364 -0
  76. package/dist/electron/electron/control-plane/handlers.js +572 -0
  77. package/dist/electron/electron/control-plane/index.js +41 -0
  78. package/dist/electron/electron/control-plane/node-manager.js +264 -0
  79. package/dist/electron/electron/control-plane/protocol.js +194 -0
  80. package/dist/electron/electron/control-plane/remote-client.js +437 -0
  81. package/dist/electron/electron/control-plane/server.js +640 -0
  82. package/dist/electron/electron/control-plane/settings.js +369 -0
  83. package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
  84. package/dist/electron/electron/cron/index.js +30 -0
  85. package/dist/electron/electron/cron/schedule.js +190 -0
  86. package/dist/electron/electron/cron/service.js +614 -0
  87. package/dist/electron/electron/cron/store.js +155 -0
  88. package/dist/electron/electron/cron/types.js +82 -0
  89. package/dist/electron/electron/cron/webhook.js +258 -0
  90. package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
  91. package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
  92. package/dist/electron/electron/database/repositories.js +1781 -0
  93. package/dist/electron/electron/database/schema.js +978 -0
  94. package/dist/electron/electron/extensions/index.js +33 -0
  95. package/dist/electron/electron/extensions/loader.js +313 -0
  96. package/dist/electron/electron/extensions/registry.js +485 -0
  97. package/dist/electron/electron/extensions/types.js +11 -0
  98. package/dist/electron/electron/gateway/channel-registry.js +1102 -0
  99. package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
  100. package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
  101. package/dist/electron/electron/gateway/channels/discord.js +975 -0
  102. package/dist/electron/electron/gateway/channels/email-client.js +593 -0
  103. package/dist/electron/electron/gateway/channels/email.js +443 -0
  104. package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
  105. package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
  106. package/dist/electron/electron/gateway/channels/imessage.js +465 -0
  107. package/dist/electron/electron/gateway/channels/index.js +36 -0
  108. package/dist/electron/electron/gateway/channels/line-client.js +470 -0
  109. package/dist/electron/electron/gateway/channels/line.js +479 -0
  110. package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
  111. package/dist/electron/electron/gateway/channels/matrix.js +592 -0
  112. package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
  113. package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
  114. package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
  115. package/dist/electron/electron/gateway/channels/signal.js +582 -0
  116. package/dist/electron/electron/gateway/channels/slack.js +415 -0
  117. package/dist/electron/electron/gateway/channels/teams.js +596 -0
  118. package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
  119. package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
  120. package/dist/electron/electron/gateway/channels/twitch.js +396 -0
  121. package/dist/electron/electron/gateway/channels/types.js +8 -0
  122. package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
  123. package/dist/electron/electron/gateway/context-policy.js +268 -0
  124. package/dist/electron/electron/gateway/index.js +1063 -0
  125. package/dist/electron/electron/gateway/infrastructure.js +496 -0
  126. package/dist/electron/electron/gateway/router.js +2700 -0
  127. package/dist/electron/electron/gateway/security.js +375 -0
  128. package/dist/electron/electron/gateway/session.js +115 -0
  129. package/dist/electron/electron/gateway/tunnel.js +503 -0
  130. package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
  131. package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
  132. package/dist/electron/electron/hooks/index.js +46 -0
  133. package/dist/electron/electron/hooks/mappings.js +381 -0
  134. package/dist/electron/electron/hooks/server.js +480 -0
  135. package/dist/electron/electron/hooks/settings.js +447 -0
  136. package/dist/electron/electron/hooks/types.js +41 -0
  137. package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
  138. package/dist/electron/electron/ipc/handlers.js +3138 -0
  139. package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
  140. package/dist/electron/electron/main.js +448 -0
  141. package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
  142. package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
  143. package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
  144. package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
  145. package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
  146. package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
  147. package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
  148. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
  149. package/dist/electron/electron/mcp/settings.js +446 -0
  150. package/dist/electron/electron/mcp/types.js +59 -0
  151. package/dist/electron/electron/memory/MemoryService.js +435 -0
  152. package/dist/electron/electron/notifications/index.js +17 -0
  153. package/dist/electron/electron/notifications/service.js +118 -0
  154. package/dist/electron/electron/notifications/store.js +144 -0
  155. package/dist/electron/electron/preload.js +842 -0
  156. package/dist/electron/electron/reports/StandupReportService.js +272 -0
  157. package/dist/electron/electron/security/concurrency.js +293 -0
  158. package/dist/electron/electron/security/index.js +15 -0
  159. package/dist/electron/electron/security/policy-manager.js +435 -0
  160. package/dist/electron/electron/settings/appearance-manager.js +193 -0
  161. package/dist/electron/electron/settings/personality-manager.js +724 -0
  162. package/dist/electron/electron/settings/x-manager.js +58 -0
  163. package/dist/electron/electron/tailscale/exposure.js +188 -0
  164. package/dist/electron/electron/tailscale/index.js +28 -0
  165. package/dist/electron/electron/tailscale/settings.js +205 -0
  166. package/dist/electron/electron/tailscale/tailscale.js +355 -0
  167. package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
  168. package/dist/electron/electron/tray/TrayManager.js +895 -0
  169. package/dist/electron/electron/tray/index.js +9 -0
  170. package/dist/electron/electron/updater/index.js +6 -0
  171. package/dist/electron/electron/updater/update-manager.js +418 -0
  172. package/dist/electron/electron/utils/env-migration.js +209 -0
  173. package/dist/electron/electron/utils/process.js +102 -0
  174. package/dist/electron/electron/utils/rate-limiter.js +104 -0
  175. package/dist/electron/electron/utils/validation.js +419 -0
  176. package/dist/electron/electron/utils/x-cli.js +177 -0
  177. package/dist/electron/electron/voice/VoiceService.js +507 -0
  178. package/dist/electron/electron/voice/index.js +14 -0
  179. package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
  180. package/dist/electron/shared/channelMessages.js +170 -0
  181. package/dist/electron/shared/types.js +1185 -0
  182. package/package.json +159 -0
  183. package/resources/skills/1password.json +10 -0
  184. package/resources/skills/add-documentation.json +31 -0
  185. package/resources/skills/analyze-csv.json +17 -0
  186. package/resources/skills/apple-notes.json +10 -0
  187. package/resources/skills/apple-reminders.json +10 -0
  188. package/resources/skills/auto-commenter.json +10 -0
  189. package/resources/skills/bear-notes.json +10 -0
  190. package/resources/skills/bird.json +35 -0
  191. package/resources/skills/blogwatcher.json +10 -0
  192. package/resources/skills/blucli.json +10 -0
  193. package/resources/skills/bluebubbles.json +10 -0
  194. package/resources/skills/camsnap.json +10 -0
  195. package/resources/skills/clean-imports.json +18 -0
  196. package/resources/skills/code-review.json +18 -0
  197. package/resources/skills/coding-agent.json +10 -0
  198. package/resources/skills/compare-files.json +23 -0
  199. package/resources/skills/convert-code.json +34 -0
  200. package/resources/skills/create-changelog.json +24 -0
  201. package/resources/skills/debug-error.json +17 -0
  202. package/resources/skills/dependency-check.json +10 -0
  203. package/resources/skills/discord.json +10 -0
  204. package/resources/skills/eightctl.json +10 -0
  205. package/resources/skills/explain-code.json +29 -0
  206. package/resources/skills/extract-todos.json +18 -0
  207. package/resources/skills/food-order.json +10 -0
  208. package/resources/skills/gemini.json +10 -0
  209. package/resources/skills/generate-readme.json +10 -0
  210. package/resources/skills/gifgrep.json +10 -0
  211. package/resources/skills/git-commit.json +10 -0
  212. package/resources/skills/github.json +10 -0
  213. package/resources/skills/gog.json +10 -0
  214. package/resources/skills/goplaces.json +10 -0
  215. package/resources/skills/himalaya.json +10 -0
  216. package/resources/skills/imsg.json +10 -0
  217. package/resources/skills/karpathy-guidelines.json +12 -0
  218. package/resources/skills/last30days.json +26 -0
  219. package/resources/skills/local-places.json +10 -0
  220. package/resources/skills/mcporter.json +10 -0
  221. package/resources/skills/model-usage.json +10 -0
  222. package/resources/skills/nano-banana-pro.json +10 -0
  223. package/resources/skills/nano-pdf.json +10 -0
  224. package/resources/skills/notion.json +10 -0
  225. package/resources/skills/obsidian.json +10 -0
  226. package/resources/skills/openai-image-gen.json +10 -0
  227. package/resources/skills/openai-whisper-api.json +10 -0
  228. package/resources/skills/openai-whisper.json +10 -0
  229. package/resources/skills/openhue.json +10 -0
  230. package/resources/skills/oracle.json +10 -0
  231. package/resources/skills/ordercli.json +10 -0
  232. package/resources/skills/peekaboo.json +10 -0
  233. package/resources/skills/project-structure.json +10 -0
  234. package/resources/skills/proofread.json +17 -0
  235. package/resources/skills/refactor-code.json +31 -0
  236. package/resources/skills/rename-symbol.json +23 -0
  237. package/resources/skills/sag.json +10 -0
  238. package/resources/skills/security-audit.json +18 -0
  239. package/resources/skills/session-logs.json +10 -0
  240. package/resources/skills/sherpa-onnx-tts.json +10 -0
  241. package/resources/skills/skill-creator.json +15 -0
  242. package/resources/skills/skill-hub.json +29 -0
  243. package/resources/skills/slack.json +10 -0
  244. package/resources/skills/songsee.json +10 -0
  245. package/resources/skills/sonoscli.json +10 -0
  246. package/resources/skills/spotify-player.json +10 -0
  247. package/resources/skills/startup-cfo.json +55 -0
  248. package/resources/skills/summarize-folder.json +18 -0
  249. package/resources/skills/summarize.json +10 -0
  250. package/resources/skills/things-mac.json +10 -0
  251. package/resources/skills/tmux.json +10 -0
  252. package/resources/skills/translate.json +36 -0
  253. package/resources/skills/trello.json +10 -0
  254. package/resources/skills/video-frames.json +10 -0
  255. package/resources/skills/voice-call.json +10 -0
  256. package/resources/skills/wacli.json +10 -0
  257. package/resources/skills/weather.json +10 -0
  258. package/resources/skills/write-tests.json +31 -0
  259. package/src/electron/activity/ActivityRepository.ts +238 -0
  260. package/src/electron/agent/browser/browser-service.ts +721 -0
  261. package/src/electron/agent/context-manager.ts +257 -0
  262. package/src/electron/agent/custom-skill-loader.ts +634 -0
  263. package/src/electron/agent/daemon.ts +1097 -0
  264. package/src/electron/agent/executor.ts +4017 -0
  265. package/src/electron/agent/llm/anthropic-provider.ts +175 -0
  266. package/src/electron/agent/llm/bedrock-provider.ts +236 -0
  267. package/src/electron/agent/llm/gemini-provider.ts +422 -0
  268. package/src/electron/agent/llm/index.ts +9 -0
  269. package/src/electron/agent/llm/ollama-provider.ts +347 -0
  270. package/src/electron/agent/llm/openai-oauth.ts +127 -0
  271. package/src/electron/agent/llm/openai-provider.ts +686 -0
  272. package/src/electron/agent/llm/openrouter-provider.ts +273 -0
  273. package/src/electron/agent/llm/pricing.ts +180 -0
  274. package/src/electron/agent/llm/provider-factory.ts +971 -0
  275. package/src/electron/agent/llm/types.ts +291 -0
  276. package/src/electron/agent/queue-manager.ts +408 -0
  277. package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
  278. package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
  279. package/src/electron/agent/sandbox/runner.ts +453 -0
  280. package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
  281. package/src/electron/agent/sandbox/security-utils.ts +251 -0
  282. package/src/electron/agent/search/brave-provider.ts +141 -0
  283. package/src/electron/agent/search/google-provider.ts +131 -0
  284. package/src/electron/agent/search/index.ts +6 -0
  285. package/src/electron/agent/search/provider-factory.ts +450 -0
  286. package/src/electron/agent/search/serpapi-provider.ts +138 -0
  287. package/src/electron/agent/search/tavily-provider.ts +108 -0
  288. package/src/electron/agent/search/types.ts +118 -0
  289. package/src/electron/agent/security/index.ts +20 -0
  290. package/src/electron/agent/security/input-sanitizer.ts +380 -0
  291. package/src/electron/agent/security/output-filter.ts +259 -0
  292. package/src/electron/agent/skill-eligibility.ts +334 -0
  293. package/src/electron/agent/skill-registry.ts +457 -0
  294. package/src/electron/agent/skills/document.ts +1070 -0
  295. package/src/electron/agent/skills/image-generator.ts +272 -0
  296. package/src/electron/agent/skills/organizer.ts +131 -0
  297. package/src/electron/agent/skills/presentation.ts +418 -0
  298. package/src/electron/agent/skills/spreadsheet.ts +166 -0
  299. package/src/electron/agent/tools/browser-tools.ts +546 -0
  300. package/src/electron/agent/tools/builtin-settings.ts +422 -0
  301. package/src/electron/agent/tools/canvas-tools.ts +572 -0
  302. package/src/electron/agent/tools/cron-tools.ts +723 -0
  303. package/src/electron/agent/tools/edit-tools.ts +196 -0
  304. package/src/electron/agent/tools/file-tools.ts +811 -0
  305. package/src/electron/agent/tools/glob-tools.ts +303 -0
  306. package/src/electron/agent/tools/grep-tools.ts +432 -0
  307. package/src/electron/agent/tools/image-tools.ts +126 -0
  308. package/src/electron/agent/tools/mention-tools.ts +371 -0
  309. package/src/electron/agent/tools/node-tools.ts +550 -0
  310. package/src/electron/agent/tools/registry.ts +3052 -0
  311. package/src/electron/agent/tools/search-tools.ts +111 -0
  312. package/src/electron/agent/tools/shell-tools.ts +651 -0
  313. package/src/electron/agent/tools/skill-tools.ts +340 -0
  314. package/src/electron/agent/tools/system-tools.ts +665 -0
  315. package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
  316. package/src/electron/agent/tools/x-tools.ts +267 -0
  317. package/src/electron/agents/AgentRoleRepository.ts +557 -0
  318. package/src/electron/agents/HeartbeatService.ts +469 -0
  319. package/src/electron/agents/MentionRepository.ts +242 -0
  320. package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
  321. package/src/electron/agents/WorkingStateRepository.ts +278 -0
  322. package/src/electron/canvas/canvas-manager.ts +818 -0
  323. package/src/electron/canvas/canvas-preload.ts +102 -0
  324. package/src/electron/canvas/canvas-protocol.ts +174 -0
  325. package/src/electron/canvas/canvas-store.ts +200 -0
  326. package/src/electron/canvas/index.ts +8 -0
  327. package/src/electron/control-plane/client.ts +527 -0
  328. package/src/electron/control-plane/handlers.ts +723 -0
  329. package/src/electron/control-plane/index.ts +51 -0
  330. package/src/electron/control-plane/node-manager.ts +322 -0
  331. package/src/electron/control-plane/protocol.ts +269 -0
  332. package/src/electron/control-plane/remote-client.ts +517 -0
  333. package/src/electron/control-plane/server.ts +853 -0
  334. package/src/electron/control-plane/settings.ts +401 -0
  335. package/src/electron/control-plane/ssh-tunnel.ts +624 -0
  336. package/src/electron/cron/index.ts +9 -0
  337. package/src/electron/cron/schedule.ts +217 -0
  338. package/src/electron/cron/service.ts +743 -0
  339. package/src/electron/cron/store.ts +165 -0
  340. package/src/electron/cron/types.ts +291 -0
  341. package/src/electron/cron/webhook.ts +303 -0
  342. package/src/electron/database/SecureSettingsRepository.ts +514 -0
  343. package/src/electron/database/TaskLabelRepository.ts +148 -0
  344. package/src/electron/database/repositories.ts +2397 -0
  345. package/src/electron/database/schema.ts +1017 -0
  346. package/src/electron/extensions/index.ts +18 -0
  347. package/src/electron/extensions/loader.ts +336 -0
  348. package/src/electron/extensions/registry.ts +546 -0
  349. package/src/electron/extensions/types.ts +372 -0
  350. package/src/electron/gateway/channel-registry.ts +1267 -0
  351. package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
  352. package/src/electron/gateway/channels/bluebubbles.ts +509 -0
  353. package/src/electron/gateway/channels/discord.ts +1150 -0
  354. package/src/electron/gateway/channels/email-client.ts +708 -0
  355. package/src/electron/gateway/channels/email.ts +516 -0
  356. package/src/electron/gateway/channels/google-chat.ts +760 -0
  357. package/src/electron/gateway/channels/imessage-client.ts +473 -0
  358. package/src/electron/gateway/channels/imessage.ts +520 -0
  359. package/src/electron/gateway/channels/index.ts +21 -0
  360. package/src/electron/gateway/channels/line-client.ts +598 -0
  361. package/src/electron/gateway/channels/line.ts +559 -0
  362. package/src/electron/gateway/channels/matrix-client.ts +632 -0
  363. package/src/electron/gateway/channels/matrix.ts +655 -0
  364. package/src/electron/gateway/channels/mattermost-client.ts +526 -0
  365. package/src/electron/gateway/channels/mattermost.ts +550 -0
  366. package/src/electron/gateway/channels/signal-client.ts +722 -0
  367. package/src/electron/gateway/channels/signal.ts +666 -0
  368. package/src/electron/gateway/channels/slack.ts +458 -0
  369. package/src/electron/gateway/channels/teams.ts +681 -0
  370. package/src/electron/gateway/channels/telegram.ts +1727 -0
  371. package/src/electron/gateway/channels/twitch-client.ts +665 -0
  372. package/src/electron/gateway/channels/twitch.ts +468 -0
  373. package/src/electron/gateway/channels/types.ts +1002 -0
  374. package/src/electron/gateway/channels/whatsapp.ts +1101 -0
  375. package/src/electron/gateway/context-policy.ts +382 -0
  376. package/src/electron/gateway/index.ts +1274 -0
  377. package/src/electron/gateway/infrastructure.ts +645 -0
  378. package/src/electron/gateway/router.ts +3206 -0
  379. package/src/electron/gateway/security.ts +422 -0
  380. package/src/electron/gateway/session.ts +144 -0
  381. package/src/electron/gateway/tunnel.ts +626 -0
  382. package/src/electron/guardrails/guardrail-manager.ts +380 -0
  383. package/src/electron/hooks/gmail-watcher.ts +355 -0
  384. package/src/electron/hooks/index.ts +30 -0
  385. package/src/electron/hooks/mappings.ts +404 -0
  386. package/src/electron/hooks/server.ts +574 -0
  387. package/src/electron/hooks/settings.ts +466 -0
  388. package/src/electron/hooks/types.ts +245 -0
  389. package/src/electron/ipc/canvas-handlers.ts +223 -0
  390. package/src/electron/ipc/handlers.ts +3661 -0
  391. package/src/electron/ipc/mission-control-handlers.ts +182 -0
  392. package/src/electron/main.ts +496 -0
  393. package/src/electron/mcp/client/MCPClientManager.ts +406 -0
  394. package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
  395. package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
  396. package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
  397. package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
  398. package/src/electron/mcp/host/MCPHostServer.ts +388 -0
  399. package/src/electron/mcp/host/ToolAdapter.ts +140 -0
  400. package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
  401. package/src/electron/mcp/settings.ts +468 -0
  402. package/src/electron/mcp/types.ts +371 -0
  403. package/src/electron/memory/MemoryService.ts +523 -0
  404. package/src/electron/notifications/index.ts +16 -0
  405. package/src/electron/notifications/service.ts +161 -0
  406. package/src/electron/notifications/store.ts +163 -0
  407. package/src/electron/preload.ts +2845 -0
  408. package/src/electron/reports/StandupReportService.ts +356 -0
  409. package/src/electron/security/concurrency.ts +333 -0
  410. package/src/electron/security/index.ts +17 -0
  411. package/src/electron/security/policy-manager.ts +539 -0
  412. package/src/electron/settings/appearance-manager.ts +182 -0
  413. package/src/electron/settings/personality-manager.ts +800 -0
  414. package/src/electron/settings/x-manager.ts +62 -0
  415. package/src/electron/tailscale/exposure.ts +262 -0
  416. package/src/electron/tailscale/index.ts +34 -0
  417. package/src/electron/tailscale/settings.ts +218 -0
  418. package/src/electron/tailscale/tailscale.ts +379 -0
  419. package/src/electron/tray/QuickInputWindow.ts +609 -0
  420. package/src/electron/tray/TrayManager.ts +1005 -0
  421. package/src/electron/tray/index.ts +6 -0
  422. package/src/electron/updater/index.ts +1 -0
  423. package/src/electron/updater/update-manager.ts +447 -0
  424. package/src/electron/utils/env-migration.ts +203 -0
  425. package/src/electron/utils/process.ts +124 -0
  426. package/src/electron/utils/rate-limiter.ts +130 -0
  427. package/src/electron/utils/validation.ts +493 -0
  428. package/src/electron/utils/x-cli.ts +198 -0
  429. package/src/electron/voice/VoiceService.ts +583 -0
  430. package/src/electron/voice/index.ts +9 -0
  431. package/src/electron/voice/voice-settings-manager.ts +403 -0
  432. package/src/renderer/App.tsx +775 -0
  433. package/src/renderer/components/ActivityFeed.tsx +407 -0
  434. package/src/renderer/components/ActivityFeedItem.tsx +285 -0
  435. package/src/renderer/components/AgentRoleCard.tsx +343 -0
  436. package/src/renderer/components/AgentRoleEditor.tsx +805 -0
  437. package/src/renderer/components/AgentSquadSettings.tsx +295 -0
  438. package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
  439. package/src/renderer/components/AppearanceSettings.tsx +122 -0
  440. package/src/renderer/components/ApprovalDialog.tsx +100 -0
  441. package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
  442. package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
  443. package/src/renderer/components/CanvasPreview.tsx +1189 -0
  444. package/src/renderer/components/CommandOutput.tsx +202 -0
  445. package/src/renderer/components/ContextPolicySettings.tsx +523 -0
  446. package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
  447. package/src/renderer/components/DisclaimerModal.tsx +124 -0
  448. package/src/renderer/components/DiscordSettings.tsx +436 -0
  449. package/src/renderer/components/EmailSettings.tsx +606 -0
  450. package/src/renderer/components/ExtensionsSettings.tsx +542 -0
  451. package/src/renderer/components/FileViewer.tsx +224 -0
  452. package/src/renderer/components/GoogleChatSettings.tsx +535 -0
  453. package/src/renderer/components/GuardrailSettings.tsx +487 -0
  454. package/src/renderer/components/HooksSettings.tsx +581 -0
  455. package/src/renderer/components/ImessageSettings.tsx +484 -0
  456. package/src/renderer/components/LineSettings.tsx +483 -0
  457. package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
  458. package/src/renderer/components/MCPSettings.tsx +943 -0
  459. package/src/renderer/components/MainContent.tsx +2433 -0
  460. package/src/renderer/components/MatrixSettings.tsx +510 -0
  461. package/src/renderer/components/MattermostSettings.tsx +473 -0
  462. package/src/renderer/components/MemorySettings.tsx +247 -0
  463. package/src/renderer/components/MentionBadge.tsx +87 -0
  464. package/src/renderer/components/MentionInput.tsx +409 -0
  465. package/src/renderer/components/MentionList.tsx +476 -0
  466. package/src/renderer/components/MissionControlPanel.tsx +1995 -0
  467. package/src/renderer/components/NodesSettings.tsx +316 -0
  468. package/src/renderer/components/NotificationPanel.tsx +481 -0
  469. package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
  470. package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
  471. package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
  472. package/src/renderer/components/Onboarding/index.ts +3 -0
  473. package/src/renderer/components/OnboardingModal.tsx +698 -0
  474. package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
  475. package/src/renderer/components/PersonalitySettings.tsx +597 -0
  476. package/src/renderer/components/QueueSettings.tsx +119 -0
  477. package/src/renderer/components/QuickTaskFAB.tsx +71 -0
  478. package/src/renderer/components/RightPanel.tsx +413 -0
  479. package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
  480. package/src/renderer/components/SearchSettings.tsx +328 -0
  481. package/src/renderer/components/Settings.tsx +1504 -0
  482. package/src/renderer/components/Sidebar.tsx +344 -0
  483. package/src/renderer/components/SignalSettings.tsx +673 -0
  484. package/src/renderer/components/SkillHubBrowser.tsx +458 -0
  485. package/src/renderer/components/SkillParameterModal.tsx +185 -0
  486. package/src/renderer/components/SkillsSettings.tsx +451 -0
  487. package/src/renderer/components/SlackSettings.tsx +442 -0
  488. package/src/renderer/components/StandupReportViewer.tsx +614 -0
  489. package/src/renderer/components/TaskBoard.tsx +498 -0
  490. package/src/renderer/components/TaskBoardCard.tsx +357 -0
  491. package/src/renderer/components/TaskBoardColumn.tsx +211 -0
  492. package/src/renderer/components/TaskLabelManager.tsx +472 -0
  493. package/src/renderer/components/TaskQueuePanel.tsx +144 -0
  494. package/src/renderer/components/TaskQuickActions.tsx +492 -0
  495. package/src/renderer/components/TaskTimeline.tsx +216 -0
  496. package/src/renderer/components/TaskView.tsx +162 -0
  497. package/src/renderer/components/TeamsSettings.tsx +518 -0
  498. package/src/renderer/components/TelegramSettings.tsx +421 -0
  499. package/src/renderer/components/Toast.tsx +76 -0
  500. package/src/renderer/components/TraySettings.tsx +189 -0
  501. package/src/renderer/components/TwitchSettings.tsx +511 -0
  502. package/src/renderer/components/UpdateSettings.tsx +295 -0
  503. package/src/renderer/components/VoiceIndicator.tsx +270 -0
  504. package/src/renderer/components/VoiceSettings.tsx +867 -0
  505. package/src/renderer/components/WhatsAppSettings.tsx +721 -0
  506. package/src/renderer/components/WorkingStateEditor.tsx +309 -0
  507. package/src/renderer/components/WorkingStateHistory.tsx +481 -0
  508. package/src/renderer/components/WorkspaceSelector.tsx +150 -0
  509. package/src/renderer/components/XSettings.tsx +311 -0
  510. package/src/renderer/global.d.ts +9 -0
  511. package/src/renderer/hooks/useAgentContext.ts +153 -0
  512. package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
  513. package/src/renderer/hooks/useVoiceInput.ts +268 -0
  514. package/src/renderer/index.html +12 -0
  515. package/src/renderer/main.tsx +10 -0
  516. package/src/renderer/public/cowork-os-logo.png +0 -0
  517. package/src/renderer/quick-input.html +164 -0
  518. package/src/renderer/styles/index.css +14504 -0
  519. package/src/renderer/utils/agentMessages.ts +749 -0
  520. package/src/renderer/utils/voice-directives.ts +169 -0
  521. package/src/shared/channelMessages.ts +213 -0
  522. package/src/shared/types.ts +3608 -0
  523. package/tsconfig.electron.json +26 -0
  524. package/tsconfig.json +26 -0
  525. package/tsconfig.node.json +10 -0
  526. 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
+ }