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