cowork-os 0.3.21 → 0.3.25

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 (252) hide show
  1. package/README.md +372 -10
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
  48. package/dist/electron/electron/agent/daemon.js +189 -13
  49. package/dist/electron/electron/agent/executor.js +895 -78
  50. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  51. package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
  52. package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
  53. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  54. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  55. package/dist/electron/electron/agent/llm/index.js +13 -1
  56. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  57. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  58. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  59. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  60. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  61. package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
  62. package/dist/electron/electron/agent/llm/types.js +66 -1
  63. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  64. package/dist/electron/electron/agent/search/provider-factory.js +38 -2
  65. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  66. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  67. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  68. package/dist/electron/electron/agent/tools/file-tools.js +66 -3
  69. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  70. package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
  71. package/dist/electron/electron/agent/tools/image-tools.js +11 -1
  72. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  73. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  74. package/dist/electron/electron/agent/tools/registry.js +548 -10
  75. package/dist/electron/electron/agent/tools/search-tools.js +28 -10
  76. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  77. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  78. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  79. package/dist/electron/electron/agents/agent-dispatch.js +63 -0
  80. package/dist/electron/electron/database/repositories.js +19 -5
  81. package/dist/electron/electron/database/schema.js +8 -0
  82. package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
  83. package/dist/electron/electron/gateway/index.js +75 -1
  84. package/dist/electron/electron/gateway/router.js +209 -154
  85. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  86. package/dist/electron/electron/ipc/handlers.js +763 -267
  87. package/dist/electron/electron/main.js +63 -0
  88. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  89. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  90. package/dist/electron/electron/memory/MemoryService.js +2 -1
  91. package/dist/electron/electron/preload.js +78 -1
  92. package/dist/electron/electron/settings/appearance-manager.js +18 -1
  93. package/dist/electron/electron/settings/box-manager.js +54 -0
  94. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  95. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  96. package/dist/electron/electron/settings/notion-manager.js +56 -0
  97. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  98. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  99. package/dist/electron/electron/utils/box-api.js +153 -0
  100. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  101. package/dist/electron/electron/utils/env-migration.js +19 -0
  102. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  103. package/dist/electron/electron/utils/notion-api.js +103 -0
  104. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  105. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  106. package/dist/electron/electron/utils/validation.js +98 -3
  107. package/dist/electron/electron/utils/x-cli.js +1 -1
  108. package/dist/electron/shared/channelMessages.js +284 -3
  109. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  110. package/dist/electron/shared/types.js +90 -1
  111. package/package.json +14 -3
  112. package/resources/skills/nano-banana-pro.json +4 -4
  113. package/resources/skills/openai-image-gen.json +3 -3
  114. package/resources/skills/scripts/gen.py +163 -0
  115. package/resources/skills/scripts/generate_image.py +91 -0
  116. package/src/electron/agent/custom-skill-loader.ts +34 -1
  117. package/src/electron/agent/daemon.ts +210 -14
  118. package/src/electron/agent/executor.ts +1124 -85
  119. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  120. package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
  121. package/src/electron/agent/llm/bedrock-provider.ts +62 -9
  122. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  123. package/src/electron/agent/llm/groq-provider.ts +39 -0
  124. package/src/electron/agent/llm/index.ts +6 -0
  125. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  126. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  127. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  128. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  129. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  130. package/src/electron/agent/llm/provider-factory.ts +459 -6
  131. package/src/electron/agent/llm/types.ts +95 -1
  132. package/src/electron/agent/llm/xai-provider.ts +39 -0
  133. package/src/electron/agent/search/provider-factory.ts +43 -2
  134. package/src/electron/agent/tools/box-tools.ts +239 -0
  135. package/src/electron/agent/tools/builtin-settings.ts +36 -0
  136. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  137. package/src/electron/agent/tools/file-tools.ts +66 -3
  138. package/src/electron/agent/tools/gmail-tools.ts +240 -0
  139. package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
  140. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  141. package/src/electron/agent/tools/grep-tools.ts +97 -12
  142. package/src/electron/agent/tools/image-tools.ts +11 -1
  143. package/src/electron/agent/tools/notion-tools.ts +330 -0
  144. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  145. package/src/electron/agent/tools/registry.ts +794 -10
  146. package/src/electron/agent/tools/search-tools.ts +29 -11
  147. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  148. package/src/electron/agent/tools/shell-tools.ts +11 -3
  149. package/src/electron/agent/tools/x-tools.ts +1 -1
  150. package/src/electron/agents/agent-dispatch.ts +79 -0
  151. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  152. package/src/electron/database/repositories.ts +58 -6
  153. package/src/electron/database/schema.ts +8 -0
  154. package/src/electron/gateway/channels/discord.ts +4 -0
  155. package/src/electron/gateway/channels/google-chat.ts +3 -0
  156. package/src/electron/gateway/channels/line.ts +3 -0
  157. package/src/electron/gateway/channels/matrix-client.ts +15 -0
  158. package/src/electron/gateway/channels/matrix.ts +31 -0
  159. package/src/electron/gateway/channels/mattermost.ts +3 -0
  160. package/src/electron/gateway/channels/signal.ts +3 -0
  161. package/src/electron/gateway/channels/slack.ts +9 -4
  162. package/src/electron/gateway/channels/teams.ts +4 -0
  163. package/src/electron/gateway/channels/telegram.ts +2 -0
  164. package/src/electron/gateway/channels/twitch.ts +2 -0
  165. package/src/electron/gateway/channels/types.ts +8 -0
  166. package/src/electron/gateway/channels/whatsapp.ts +66 -0
  167. package/src/electron/gateway/index.ts +95 -2
  168. package/src/electron/gateway/router.ts +231 -161
  169. package/src/electron/gateway/security.ts +21 -9
  170. package/src/electron/ipc/canvas-handlers.ts +10 -0
  171. package/src/electron/ipc/handlers.ts +848 -292
  172. package/src/electron/main.ts +35 -0
  173. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  174. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  175. package/src/electron/memory/MemoryService.ts +7 -1
  176. package/src/electron/preload.ts +200 -5
  177. package/src/electron/settings/appearance-manager.ts +20 -2
  178. package/src/electron/settings/box-manager.ts +58 -0
  179. package/src/electron/settings/dropbox-manager.ts +58 -0
  180. package/src/electron/settings/google-workspace-manager.ts +59 -0
  181. package/src/electron/settings/notion-manager.ts +60 -0
  182. package/src/electron/settings/onedrive-manager.ts +58 -0
  183. package/src/electron/settings/sharepoint-manager.ts +58 -0
  184. package/src/electron/utils/box-api.ts +184 -0
  185. package/src/electron/utils/dropbox-api.ts +171 -0
  186. package/src/electron/utils/env-migration.ts +22 -0
  187. package/src/electron/utils/gmail-api.ts +121 -0
  188. package/src/electron/utils/google-calendar-api.ts +115 -0
  189. package/src/electron/utils/google-workspace-api.ts +228 -0
  190. package/src/electron/utils/google-workspace-auth.ts +109 -0
  191. package/src/electron/utils/google-workspace-oauth.ts +232 -0
  192. package/src/electron/utils/notion-api.ts +126 -0
  193. package/src/electron/utils/onedrive-api.ts +137 -0
  194. package/src/electron/utils/sharepoint-api.ts +132 -0
  195. package/src/electron/utils/validation.ts +128 -1
  196. package/src/electron/utils/x-cli.ts +1 -1
  197. package/src/renderer/App.tsx +119 -8
  198. package/src/renderer/components/ActivityFeedItem.tsx +34 -17
  199. package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
  200. package/src/renderer/components/AppearanceSettings.tsx +37 -2
  201. package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
  202. package/src/renderer/components/BoxSettings.tsx +203 -0
  203. package/src/renderer/components/BrowserView.tsx +101 -0
  204. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  205. package/src/renderer/components/CanvasPreview.tsx +68 -1
  206. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  207. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  208. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  209. package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
  210. package/src/renderer/components/DiscordSettings.tsx +18 -7
  211. package/src/renderer/components/DropboxSettings.tsx +202 -0
  212. package/src/renderer/components/EmailSettings.tsx +18 -7
  213. package/src/renderer/components/FileViewer.tsx +21 -13
  214. package/src/renderer/components/GoogleChatSettings.tsx +17 -7
  215. package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
  216. package/src/renderer/components/ImessageSettings.tsx +22 -11
  217. package/src/renderer/components/LineIcons.tsx +376 -0
  218. package/src/renderer/components/LineSettings.tsx +18 -7
  219. package/src/renderer/components/MCPSettings.tsx +56 -0
  220. package/src/renderer/components/MainContent.tsx +740 -76
  221. package/src/renderer/components/MatrixSettings.tsx +18 -7
  222. package/src/renderer/components/MattermostSettings.tsx +18 -7
  223. package/src/renderer/components/NodesSettings.tsx +58 -99
  224. package/src/renderer/components/NotificationPanel.tsx +25 -11
  225. package/src/renderer/components/NotionSettings.tsx +231 -0
  226. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  227. package/src/renderer/components/OnboardingModal.tsx +70 -1
  228. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  229. package/src/renderer/components/RightPanel.tsx +141 -28
  230. package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
  231. package/src/renderer/components/SearchSettings.tsx +118 -114
  232. package/src/renderer/components/Settings.tsx +1425 -651
  233. package/src/renderer/components/SharePointSettings.tsx +224 -0
  234. package/src/renderer/components/Sidebar.tsx +94 -19
  235. package/src/renderer/components/SignalSettings.tsx +18 -7
  236. package/src/renderer/components/SkillHubBrowser.tsx +144 -185
  237. package/src/renderer/components/SlackSettings.tsx +18 -7
  238. package/src/renderer/components/TaskQuickActions.tsx +11 -6
  239. package/src/renderer/components/TaskTimeline.tsx +58 -26
  240. package/src/renderer/components/TeamsSettings.tsx +18 -7
  241. package/src/renderer/components/TelegramSettings.tsx +18 -7
  242. package/src/renderer/components/ThemeIcon.tsx +16 -0
  243. package/src/renderer/components/TwitchSettings.tsx +18 -7
  244. package/src/renderer/components/VoiceSettings.tsx +30 -74
  245. package/src/renderer/components/WhatsAppSettings.tsx +48 -37
  246. package/src/renderer/components/WorkingStateHistory.tsx +7 -5
  247. package/src/renderer/components/WorkspaceSelector.tsx +42 -13
  248. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  249. package/src/renderer/styles/index.css +2333 -209
  250. package/src/shared/channelMessages.ts +367 -4
  251. package/src/shared/llm-provider-catalog.ts +217 -0
  252. package/src/shared/types.ts +251 -2
@@ -3,6 +3,7 @@ import * as path from 'path';
3
3
  import * as fs from 'fs/promises';
4
4
  import * as fsSync from 'fs';
5
5
  import mammoth from 'mammoth';
6
+ import mime from 'mime-types';
6
7
 
7
8
  // eslint-disable-next-line @typescript-eslint/no-require-imports
8
9
  const pdfParseModule = require('pdf-parse');
@@ -28,10 +29,23 @@ import { MentionRepository } from '../agents/MentionRepository';
28
29
  import { TaskLabelRepository } from '../database/TaskLabelRepository';
29
30
  import { WorkingStateRepository } from '../agents/WorkingStateRepository';
30
31
  import { ContextPolicyManager } from '../gateway/context-policy';
31
- import { IPC_CHANNELS, LLMSettingsData, AddChannelRequest, UpdateChannelRequest, SecurityMode, UpdateInfo, TEMP_WORKSPACE_ID, TEMP_WORKSPACE_NAME, Workspace, AgentRole, Task, BoardColumn, XSettingsData } from '../../shared/types';
32
+ import { IPC_CHANNELS, LLMSettingsData, AddChannelRequest, UpdateChannelRequest, SecurityMode, UpdateInfo, TEMP_WORKSPACE_ID, TEMP_WORKSPACE_NAME, Workspace, AgentRole, Task, BoardColumn, XSettingsData, NotionSettingsData, BoxSettingsData, OneDriveSettingsData, GoogleWorkspaceSettingsData, DropboxSettingsData, SharePointSettingsData } from '../../shared/types';
33
+ import { CUSTOM_PROVIDER_MAP, CUSTOM_PROVIDER_IDS } from '../../shared/llm-provider-catalog';
32
34
  import * as os from 'os';
33
35
  import { AgentDaemon } from '../agent/daemon';
34
- import { LLMProviderFactory, LLMProviderConfig, ModelKey, MODELS, GEMINI_MODELS, OPENROUTER_MODELS, OLLAMA_MODELS, OpenAIOAuth } from '../agent/llm';
36
+ import {
37
+ LLMProviderFactory,
38
+ LLMProviderConfig,
39
+ ModelKey,
40
+ MODELS,
41
+ GEMINI_MODELS,
42
+ OPENROUTER_MODELS,
43
+ OLLAMA_MODELS,
44
+ GROQ_MODELS,
45
+ XAI_MODELS,
46
+ KIMI_MODELS,
47
+ OpenAIOAuth,
48
+ } from '../agent/llm';
35
49
  import { SearchProviderFactory, SearchSettings, SearchProviderType } from '../agent/search';
36
50
  import { ChannelGateway } from '../gateway';
37
51
  import { updateManager } from '../updater';
@@ -42,10 +56,18 @@ import {
42
56
  TaskCreateSchema,
43
57
  TaskRenameSchema,
44
58
  TaskMessageSchema,
59
+ FileImportSchema,
60
+ FileImportDataSchema,
45
61
  ApprovalResponseSchema,
46
62
  LLMSettingsSchema,
47
63
  SearchSettingsSchema,
48
64
  XSettingsSchema,
65
+ NotionSettingsSchema,
66
+ BoxSettingsSchema,
67
+ OneDriveSettingsSchema,
68
+ GoogleWorkspaceSettingsSchema,
69
+ DropboxSettingsSchema,
70
+ SharePointSettingsSchema,
49
71
  AddChannelSchema,
50
72
  UpdateChannelSchema,
51
73
  GrantAccessSchema,
@@ -54,10 +76,25 @@ import {
54
76
  GuardrailSettingsSchema,
55
77
  UUIDSchema,
56
78
  StringIdSchema,
79
+ MCPConnectorOAuthSchema,
57
80
  } from '../utils/validation';
58
81
  import { GuardrailManager } from '../guardrails/guardrail-manager';
59
82
  import { AppearanceManager } from '../settings/appearance-manager';
60
83
  import { PersonalityManager } from '../settings/personality-manager';
84
+ import { NotionSettingsManager } from '../settings/notion-manager';
85
+ import { testNotionConnection } from '../utils/notion-api';
86
+ import { BoxSettingsManager } from '../settings/box-manager';
87
+ import { OneDriveSettingsManager } from '../settings/onedrive-manager';
88
+ import { GoogleWorkspaceSettingsManager } from '../settings/google-workspace-manager';
89
+ import { DropboxSettingsManager } from '../settings/dropbox-manager';
90
+ import { SharePointSettingsManager } from '../settings/sharepoint-manager';
91
+ import { testBoxConnection } from '../utils/box-api';
92
+ import { testOneDriveConnection } from '../utils/onedrive-api';
93
+ import { testGoogleWorkspaceConnection } from '../utils/google-workspace-api';
94
+ import { testDropboxConnection } from '../utils/dropbox-api';
95
+ import { testSharePointConnection } from '../utils/sharepoint-api';
96
+ import { startConnectorOAuth } from '../mcp/oauth/connector-oauth';
97
+ import { startGoogleWorkspaceOAuth } from '../utils/google-workspace-oauth';
61
98
 
62
99
  const normalizeMentionToken = (value: string): string =>
63
100
  value.toLowerCase().replace(/[^a-z0-9]/g, '');
@@ -124,7 +161,9 @@ const scoreAgentForTask = (role: AgentRole, text: string) => {
124
161
  return score;
125
162
  };
126
163
 
127
- const selectBestAgentsForTask = (text: string, roles: AgentRole[]) => {
164
+ const MAX_AUTO_AGENTS = 4;
165
+
166
+ const selectBestAgentsForTask = (text: string, roles: AgentRole[], maxAgents = MAX_AUTO_AGENTS) => {
128
167
  if (roles.length === 0) return roles;
129
168
  const scored = roles
130
169
  .map((role) => ({ role, score: scoreAgentForTask(role, text) }))
@@ -139,19 +178,19 @@ const selectBestAgentsForTask = (text: string, roles: AgentRole[]) => {
139
178
  const threshold = Math.max(1, maxScore - 2);
140
179
  const selected = withScore
141
180
  .filter((entry) => entry.score >= threshold)
142
- .slice(0, 4)
181
+ .slice(0, maxAgents)
143
182
  .map((entry) => entry.role);
144
- return selected.length > 0 ? selected : withScore.slice(0, 3).map((entry) => entry.role);
183
+ return selected.length > 0 ? selected : withScore.slice(0, maxAgents).map((entry) => entry.role);
145
184
  }
146
185
 
147
186
  const leads = roles
148
187
  .filter((role) => role.autonomyLevel === 'lead')
149
188
  .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
150
189
  if (leads.length > 0) {
151
- return leads.slice(0, 3);
190
+ return leads.slice(0, maxAgents);
152
191
  }
153
192
 
154
- return roles.slice(0, Math.min(3, roles.length));
193
+ return roles.slice(0, Math.min(maxAgents, roles.length));
155
194
  };
156
195
 
157
196
  const extractMentionedRoles = (
@@ -159,10 +198,9 @@ const extractMentionedRoles = (
159
198
  roles: AgentRole[]
160
199
  ) => {
161
200
  const normalizedText = text.toLowerCase();
162
- const useSmartSelection = /\B@everybody\b/.test(normalizedText);
163
- if (/\B@all\b/.test(normalizedText) || /\B@everyone\b/.test(normalizedText)) {
164
- return roles;
165
- }
201
+ const useSmartSelection = /\B@everybody\b/.test(normalizedText) ||
202
+ /\B@all\b/.test(normalizedText) ||
203
+ /\B@everyone\b/.test(normalizedText);
166
204
 
167
205
  const index = buildAgentMentionIndex(roles);
168
206
  const matches = new Map<string, AgentRole>();
@@ -180,11 +218,15 @@ const extractMentionedRoles = (
180
218
 
181
219
  if (matches.size > 0) {
182
220
  if (useSmartSelection) {
183
- const selected = selectBestAgentsForTask(text, roles);
184
221
  const merged = new Map<string, AgentRole>();
185
- selected.forEach((role) => merged.set(role.id, role));
186
222
  matches.forEach((role) => merged.set(role.id, role));
187
- return Array.from(merged.values());
223
+ const selected = selectBestAgentsForTask(text, roles, MAX_AUTO_AGENTS);
224
+ selected.forEach((role) => {
225
+ if (merged.size < MAX_AUTO_AGENTS) {
226
+ merged.set(role.id, role);
227
+ }
228
+ });
229
+ return Array.from(merged.values()).slice(0, MAX_AUTO_AGENTS);
188
230
  }
189
231
  return Array.from(matches.values());
190
232
  }
@@ -200,73 +242,12 @@ const extractMentionedRoles = (
200
242
  });
201
243
 
202
244
  if (useSmartSelection) {
203
- return selectBestAgentsForTask(text, roles);
245
+ return selectBestAgentsForTask(text, roles, MAX_AUTO_AGENTS);
204
246
  }
205
247
 
206
248
  return Array.from(matches.values());
207
249
  };
208
250
 
209
- const buildSoulSummary = (soul?: string): string | null => {
210
- if (!soul) return null;
211
- try {
212
- const parsed = JSON.parse(soul) as Record<string, unknown>;
213
- const parts: string[] = [];
214
- if (typeof parsed.name === 'string') parts.push(`Name: ${parsed.name}`);
215
- if (typeof parsed.role === 'string') parts.push(`Role: ${parsed.role}`);
216
- if (typeof parsed.personality === 'string') parts.push(`Personality: ${parsed.personality}`);
217
- if (typeof parsed.communicationStyle === 'string') parts.push(`Style: ${parsed.communicationStyle}`);
218
- if (Array.isArray(parsed.focusAreas)) parts.push(`Focus: ${parsed.focusAreas.join(', ')}`);
219
- if (Array.isArray(parsed.strengths)) parts.push(`Strengths: ${parsed.strengths.join(', ')}`);
220
- if (parts.length === 0) {
221
- return null;
222
- }
223
- return parts.join('\n');
224
- } catch {
225
- return soul;
226
- }
227
- };
228
-
229
- const buildAgentDispatchPrompt = (
230
- role: {
231
- displayName: string;
232
- description?: string | null;
233
- capabilities?: string[];
234
- systemPrompt?: string | null;
235
- soul?: string | null;
236
- },
237
- parentTask: { title: string; prompt: string }
238
- ) => {
239
- const lines: string[] = [
240
- `You are ${role.displayName}${role.description ? ` — ${role.description}` : ''}.`,
241
- ];
242
-
243
- if (role.capabilities && role.capabilities.length > 0) {
244
- lines.push(`Capabilities: ${role.capabilities.join(', ')}`);
245
- }
246
-
247
- if (role.systemPrompt) {
248
- lines.push('System guidance:');
249
- lines.push(role.systemPrompt);
250
- }
251
-
252
- const soulSummary = buildSoulSummary(role.soul || undefined);
253
- if (soulSummary) {
254
- lines.push('Role notes:');
255
- lines.push(soulSummary);
256
- }
257
-
258
- lines.push('');
259
- lines.push(`Parent task: ${parentTask.title}`);
260
- lines.push('Request:');
261
- lines.push(parentTask.prompt);
262
- lines.push('');
263
- lines.push('Deliverables:');
264
- lines.push('- Provide a concise summary of your findings.');
265
- lines.push('- Call out risks or open questions.');
266
- lines.push('- Recommend next steps.');
267
-
268
- return lines.join('\n');
269
- };
270
251
  import { XSettingsManager } from '../settings/x-manager';
271
252
  import { testXConnection, checkBirdInstalled } from '../utils/x-cli';
272
253
  import { getCustomSkillLoader } from '../agent/custom-skill-loader';
@@ -303,6 +284,8 @@ import { getVoiceService } from '../voice/VoiceService';
303
284
 
304
285
  // Global notification service instance
305
286
  let notificationService: NotificationService | null = null;
287
+ const resolveCustomProviderId = (providerType: string) =>
288
+ providerType === 'kimi-coding' ? 'kimi-code' : providerType;
306
289
 
307
290
  /**
308
291
  * Get the notification service instance
@@ -329,6 +312,9 @@ rateLimiter.configure(IPC_CHANNELS.LLM_GET_OLLAMA_MODELS, RATE_LIMIT_CONFIGS.sta
329
312
  rateLimiter.configure(IPC_CHANNELS.LLM_GET_GEMINI_MODELS, RATE_LIMIT_CONFIGS.standard);
330
313
  rateLimiter.configure(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS, RATE_LIMIT_CONFIGS.standard);
331
314
  rateLimiter.configure(IPC_CHANNELS.LLM_GET_BEDROCK_MODELS, RATE_LIMIT_CONFIGS.standard);
315
+ rateLimiter.configure(IPC_CHANNELS.LLM_GET_GROQ_MODELS, RATE_LIMIT_CONFIGS.standard);
316
+ rateLimiter.configure(IPC_CHANNELS.LLM_GET_XAI_MODELS, RATE_LIMIT_CONFIGS.standard);
317
+ rateLimiter.configure(IPC_CHANNELS.LLM_GET_KIMI_MODELS, RATE_LIMIT_CONFIGS.standard);
332
318
  rateLimiter.configure(IPC_CHANNELS.SEARCH_SAVE_SETTINGS, RATE_LIMIT_CONFIGS.limited);
333
319
  rateLimiter.configure(IPC_CHANNELS.SEARCH_TEST_PROVIDER, RATE_LIMIT_CONFIGS.expensive);
334
320
  rateLimiter.configure(IPC_CHANNELS.GATEWAY_ADD_CHANNEL, RATE_LIMIT_CONFIGS.limited);
@@ -624,6 +610,159 @@ export async function setupIpcHandlers(
624
610
  }
625
611
  });
626
612
 
613
+ // File import handler - copy selected files into the workspace for attachment use
614
+ ipcMain.handle('file:importToWorkspace', async (_, data: { workspaceId: string; files: string[] }) => {
615
+ const validated = validateInput(FileImportSchema, data, 'file import');
616
+ const workspace = workspaceRepo.findById(validated.workspaceId);
617
+
618
+ if (!workspace) {
619
+ throw new Error(`Workspace not found: ${validated.workspaceId}`);
620
+ }
621
+
622
+ if (!workspace.permissions.write) {
623
+ throw new Error('Write permission not granted for workspace');
624
+ }
625
+
626
+ const sanitizeFileName = (fileName: string): string => {
627
+ const sanitized = fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').trim();
628
+ return sanitized.length > 0 ? sanitized : 'file';
629
+ };
630
+
631
+ const ensureUniqueName = (dir: string, baseName: string, usedNames: Set<string>): string => {
632
+ const ext = path.extname(baseName);
633
+ const stem = path.basename(baseName, ext);
634
+ let candidate = baseName;
635
+ let counter = 1;
636
+ while (usedNames.has(candidate) || fsSync.existsSync(path.join(dir, candidate))) {
637
+ candidate = `${stem}-${counter}${ext}`;
638
+ counter += 1;
639
+ }
640
+ usedNames.add(candidate);
641
+ return candidate;
642
+ };
643
+
644
+ let uploadRoot: string | null = null;
645
+ const usedNames = new Set<string>();
646
+
647
+ const ensureUploadRoot = async (): Promise<string> => {
648
+ if (uploadRoot) return uploadRoot;
649
+ uploadRoot = path.join(workspace.path, '.cowork', 'uploads', `${Date.now()}`);
650
+ await fs.mkdir(uploadRoot, { recursive: true });
651
+ return uploadRoot;
652
+ };
653
+
654
+ const results: Array<{ relativePath: string; fileName: string; size: number; mimeType?: string }> = [];
655
+
656
+ for (const filePath of validated.files) {
657
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
658
+ const stats = await fs.stat(absolutePath);
659
+
660
+ if (!stats.isFile()) {
661
+ throw new Error(`Not a file: ${filePath}`);
662
+ }
663
+
664
+ const sizeCheck = GuardrailManager.isFileSizeExceeded(stats.size);
665
+ if (sizeCheck.exceeded) {
666
+ throw new Error(`File "${path.basename(filePath)}" is ${sizeCheck.sizeMB.toFixed(1)}MB and exceeds the ${sizeCheck.limitMB}MB limit.`);
667
+ }
668
+
669
+ const mimeType = (mime.lookup(absolutePath) || undefined) as string | undefined;
670
+
671
+ if (isPathWithinWorkspace(absolutePath, workspace.path)) {
672
+ results.push({
673
+ relativePath: path.relative(workspace.path, absolutePath),
674
+ fileName: path.basename(absolutePath),
675
+ size: stats.size,
676
+ mimeType,
677
+ });
678
+ continue;
679
+ }
680
+
681
+ const safeName = sanitizeFileName(path.basename(absolutePath));
682
+ const targetRoot = await ensureUploadRoot();
683
+ const uniqueName = ensureUniqueName(targetRoot, safeName, usedNames);
684
+ const destination = path.join(targetRoot, uniqueName);
685
+
686
+ await fs.copyFile(absolutePath, destination);
687
+
688
+ results.push({
689
+ relativePath: path.relative(workspace.path, destination),
690
+ fileName: uniqueName,
691
+ size: stats.size,
692
+ mimeType,
693
+ });
694
+ }
695
+
696
+ return results;
697
+ });
698
+
699
+ // File import handler - save provided file data into the workspace (clipboard / drag data)
700
+ ipcMain.handle('file:importDataToWorkspace', async (_, data: { workspaceId: string; files: Array<{ name: string; data: string; mimeType?: string }> }) => {
701
+ const validated = validateInput(FileImportDataSchema, data, 'file import data');
702
+ const workspace = workspaceRepo.findById(validated.workspaceId);
703
+
704
+ if (!workspace) {
705
+ throw new Error(`Workspace not found: ${validated.workspaceId}`);
706
+ }
707
+
708
+ if (!workspace.permissions.write) {
709
+ throw new Error('Write permission not granted for workspace');
710
+ }
711
+
712
+ const sanitizeFileName = (fileName: string): string => {
713
+ const sanitized = fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').trim();
714
+ return sanitized.length > 0 ? sanitized : 'file';
715
+ };
716
+
717
+ const ensureExtension = (fileName: string, mimeType?: string): string => {
718
+ if (path.extname(fileName) || !mimeType) return fileName;
719
+ const ext = mime.extension(mimeType);
720
+ return ext ? `${fileName}.${ext}` : fileName;
721
+ };
722
+
723
+ const ensureUniqueName = (dir: string, baseName: string, usedNames: Set<string>): string => {
724
+ const ext = path.extname(baseName);
725
+ const stem = path.basename(baseName, ext);
726
+ let candidate = baseName;
727
+ let counter = 1;
728
+ while (usedNames.has(candidate) || fsSync.existsSync(path.join(dir, candidate))) {
729
+ candidate = `${stem}-${counter}${ext}`;
730
+ counter += 1;
731
+ }
732
+ usedNames.add(candidate);
733
+ return candidate;
734
+ };
735
+
736
+ const uploadRoot = path.join(workspace.path, '.cowork', 'uploads', `${Date.now()}`);
737
+ await fs.mkdir(uploadRoot, { recursive: true });
738
+ const usedNames = new Set<string>();
739
+
740
+ const results: Array<{ relativePath: string; fileName: string; size: number; mimeType?: string }> = [];
741
+
742
+ for (const file of validated.files) {
743
+ const rawName = ensureExtension(sanitizeFileName(file.name), file.mimeType);
744
+ const uniqueName = ensureUniqueName(uploadRoot, rawName, usedNames);
745
+ const destination = path.join(uploadRoot, uniqueName);
746
+ const buffer = Buffer.from(file.data, 'base64');
747
+
748
+ const sizeCheck = GuardrailManager.isFileSizeExceeded(buffer.length);
749
+ if (sizeCheck.exceeded) {
750
+ throw new Error(`File "${rawName}" is ${sizeCheck.sizeMB.toFixed(1)}MB and exceeds the ${sizeCheck.limitMB}MB limit.`);
751
+ }
752
+
753
+ await fs.writeFile(destination, buffer);
754
+
755
+ results.push({
756
+ relativePath: path.relative(workspace.path, destination),
757
+ fileName: uniqueName,
758
+ size: buffer.length,
759
+ mimeType: file.mimeType,
760
+ });
761
+ }
762
+
763
+ return results;
764
+ });
765
+
627
766
  // Workspace handlers
628
767
  ipcMain.handle(IPC_CHANNELS.WORKSPACE_CREATE, async (_, data) => {
629
768
  const validated = validateInput(WorkspaceCreateSchema, data, 'workspace');
@@ -659,7 +798,15 @@ export async function setupIpcHandlers(
659
798
  });
660
799
 
661
800
  ipcMain.handle(IPC_CHANNELS.WORKSPACE_SELECT, async (_, id: string) => {
662
- return workspaceRepo.findById(id);
801
+ const workspace = workspaceRepo.findById(id);
802
+ if (workspace && workspace.id !== TEMP_WORKSPACE_ID) {
803
+ try {
804
+ workspaceRepo.updateLastUsedAt(workspace.id);
805
+ } catch (error) {
806
+ console.warn('Failed to update workspace last used time:', error);
807
+ }
808
+ }
809
+ return workspace;
663
810
  });
664
811
 
665
812
  ipcMain.handle(IPC_CHANNELS.WORKSPACE_UPDATE_PERMISSIONS, async (_, id: string, permissions: { shell?: boolean; network?: boolean; read?: boolean; write?: boolean; delete?: boolean }) => {
@@ -672,6 +819,15 @@ export async function setupIpcHandlers(
672
819
  return workspaceRepo.findById(id);
673
820
  });
674
821
 
822
+ ipcMain.handle(IPC_CHANNELS.WORKSPACE_TOUCH, async (_, id: string) => {
823
+ const workspace = workspaceRepo.findById(id);
824
+ if (!workspace) {
825
+ throw new Error(`Workspace not found: ${id}`);
826
+ }
827
+ workspaceRepo.updateLastUsedAt(id);
828
+ return workspaceRepo.findById(id);
829
+ });
830
+
675
831
  // Task handlers
676
832
  ipcMain.handle(IPC_CHANNELS.TASK_CREATE, async (_, data) => {
677
833
  checkRateLimit(IPC_CHANNELS.TASK_CREATE);
@@ -686,104 +842,49 @@ export async function setupIpcHandlers(
686
842
  budgetCost,
687
843
  });
688
844
 
689
- // Start task execution in agent daemon
690
- try {
691
- await agentDaemon.startTask(task);
692
- } catch (error: any) {
693
- // Update task status to failed if we can't start it
694
- taskRepo.update(task.id, {
695
- status: 'failed',
696
- error: error.message || 'Failed to start task',
697
- });
698
- throw new Error(error.message || 'Failed to start task. Please check your LLM provider settings.');
845
+ if (workspaceId !== TEMP_WORKSPACE_ID) {
846
+ try {
847
+ workspaceRepo.updateLastUsedAt(workspaceId);
848
+ } catch (error) {
849
+ console.warn('Failed to update workspace last used time:', error);
850
+ }
699
851
  }
700
852
 
701
- // Dispatch to mentioned agents (e.g., "Please review @Vision @Loki")
853
+ // Capture mentioned agent roles for deferred dispatch (after main plan is created)
702
854
  try {
703
855
  const activeRoles = agentRoleRepo.findAll(false).filter((role) => role.isActive);
704
856
  const mentionedRoles = extractMentionedRoles(`${title}\n${prompt}`, activeRoles);
705
- const dispatchRoles = mentionedRoles.length > 0 ? mentionedRoles : activeRoles;
706
-
707
- if (dispatchRoles.length > 0) {
708
- const taskUpdate: Partial<Task> = {
709
- mentionedAgentRoleIds: dispatchRoles.map((role) => role.id),
710
- };
711
- taskRepo.update(task.id, taskUpdate);
712
-
713
- // Parallelize child task creation for better performance
714
- const dispatchPromises = dispatchRoles.map(async (role) => {
715
- const childPrompt = buildAgentDispatchPrompt(role, task);
716
- const childTask = await agentDaemon.createChildTask({
717
- title: `@${role.displayName}: ${task.title}`,
718
- prompt: childPrompt,
719
- workspaceId: task.workspaceId,
720
- parentTaskId: task.id,
721
- agentType: 'sub',
722
- agentConfig: {
723
- ...(role.modelKey ? { modelKey: role.modelKey } : {}),
724
- ...(role.personalityId ? { personalityId: role.personalityId } : {}),
725
- retainMemory: false,
726
- },
727
- });
728
-
729
- const childUpdate: Partial<Task> = {
730
- assignedAgentRoleId: role.id,
731
- boardColumn: 'todo' as BoardColumn,
732
- };
733
- taskRepo.update(childTask.id, childUpdate);
734
-
735
- const dispatchActivity = activityRepo.create({
736
- workspaceId: task.workspaceId,
737
- taskId: task.id,
738
- agentRoleId: role.id,
739
- actorType: 'system',
740
- activityType: 'agent_assigned',
741
- title: `Dispatched to ${role.displayName}`,
742
- description: childTask.title,
743
- });
744
- getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity: dispatchActivity });
745
-
746
- const mention = mentionRepo.create({
747
- workspaceId: task.workspaceId,
748
- taskId: task.id,
749
- toAgentRoleId: role.id,
750
- mentionType: 'request',
751
- context: `New task: ${task.title}`,
752
- });
753
- getMainWindow()?.webContents.send(IPC_CHANNELS.MENTION_EVENT, { type: 'created', mention });
754
-
755
- const mentionActivity = activityRepo.create({
756
- workspaceId: task.workspaceId,
757
- taskId: task.id,
758
- agentRoleId: role.id,
759
- actorType: 'user',
760
- activityType: 'mention',
761
- title: `@${role.displayName} mentioned`,
762
- description: mention.context,
763
- metadata: { mentionId: mention.id, mentionType: mention.mentionType },
764
- });
765
- getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity: mentionActivity });
766
-
767
- return { role, childTask };
768
- });
769
-
770
- await Promise.all(dispatchPromises);
857
+ const mentionedAgentRoleIds = mentionedRoles.map((role) => role.id);
858
+ if (mentionedAgentRoleIds.length > 0) {
859
+ taskRepo.update(task.id, { mentionedAgentRoleIds });
771
860
  }
772
861
  } catch (error: unknown) {
773
862
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
774
- console.error('Failed to dispatch to mentioned agents:', error);
863
+ console.error('Failed to record mentioned agents:', error);
775
864
  // Notify user of dispatch failure via activity feed
776
865
  const errorActivity = activityRepo.create({
777
866
  workspaceId: task.workspaceId,
778
867
  taskId: task.id,
779
868
  actorType: 'system',
780
869
  activityType: 'error',
781
- title: 'Agent dispatch failed',
782
- description: `Failed to dispatch task to mentioned agents: ${errorMessage}`,
870
+ title: 'Agent mention capture failed',
871
+ description: `Failed to record mentioned agents for deferred dispatch: ${errorMessage}`,
783
872
  });
784
873
  getMainWindow()?.webContents.send(IPC_CHANNELS.ACTIVITY_EVENT, { type: 'created', activity: errorActivity });
785
874
  }
786
875
 
876
+ // Start task execution in agent daemon
877
+ try {
878
+ await agentDaemon.startTask(task);
879
+ } catch (error: any) {
880
+ // Update task status to failed if we can't start it
881
+ taskRepo.update(task.id, {
882
+ status: 'failed',
883
+ error: error.message || 'Failed to start task',
884
+ });
885
+ throw new Error(error.message || 'Failed to start task. Please check your LLM provider settings.');
886
+ }
887
+
787
888
  return task;
788
889
  });
789
890
 
@@ -1031,6 +1132,29 @@ export async function setupIpcHandlers(
1031
1132
  };
1032
1133
  }
1033
1134
 
1135
+ const normalizeAzureSettings = (
1136
+ incoming?: LLMSettingsData['azure'],
1137
+ existing?: LLMSettingsData['azure']
1138
+ ): LLMSettingsData['azure'] | undefined => {
1139
+ if (!incoming && !existing) return undefined;
1140
+ const mergedDeployments = [
1141
+ ...(incoming?.deployments || []),
1142
+ ...(existing?.deployments || []),
1143
+ ]
1144
+ .map((entry) => entry.trim())
1145
+ .filter(Boolean);
1146
+ const deployment = (incoming?.deployment || existing?.deployment || mergedDeployments[0] || '').trim();
1147
+ if (deployment && !mergedDeployments.includes(deployment)) {
1148
+ mergedDeployments.unshift(deployment);
1149
+ }
1150
+ return {
1151
+ ...(existing || {}),
1152
+ ...(incoming || {}),
1153
+ deployment: deployment || undefined,
1154
+ deployments: mergedDeployments.length > 0 ? Array.from(new Set(mergedDeployments)) : undefined,
1155
+ };
1156
+ };
1157
+
1034
1158
  LLMProviderFactory.saveSettings({
1035
1159
  providerType: validated.providerType,
1036
1160
  modelKey: validated.modelKey as ModelKey,
@@ -1040,12 +1164,20 @@ export async function setupIpcHandlers(
1040
1164
  gemini: validated.gemini,
1041
1165
  openrouter: validated.openrouter,
1042
1166
  openai: openaiSettings,
1167
+ azure: normalizeAzureSettings(validated.azure, existingSettings.azure),
1168
+ groq: validated.groq,
1169
+ xai: validated.xai,
1170
+ kimi: validated.kimi,
1171
+ customProviders: validated.customProviders ?? existingSettings.customProviders,
1043
1172
  // Preserve cached models from existing settings
1044
1173
  cachedGeminiModels: existingSettings.cachedGeminiModels,
1045
1174
  cachedOpenRouterModels: existingSettings.cachedOpenRouterModels,
1046
1175
  cachedOllamaModels: existingSettings.cachedOllamaModels,
1047
1176
  cachedBedrockModels: existingSettings.cachedBedrockModels,
1048
1177
  cachedOpenAIModels: existingSettings.cachedOpenAIModels,
1178
+ cachedGroqModels: existingSettings.cachedGroqModels,
1179
+ cachedXaiModels: existingSettings.cachedXaiModels,
1180
+ cachedKimiModels: existingSettings.cachedKimiModels,
1049
1181
  });
1050
1182
  // Clear cache so next task uses new settings
1051
1183
  LLMProviderFactory.clearCache();
@@ -1062,6 +1194,9 @@ export async function setupIpcHandlers(
1062
1194
  openaiAccessToken = settings.openai?.accessToken;
1063
1195
  openaiRefreshToken = settings.openai?.refreshToken;
1064
1196
  }
1197
+ const resolvedProviderType = resolveCustomProviderId(config.providerType);
1198
+ const customProviderConfig = config.customProviders?.[resolvedProviderType] || config.customProviders?.[config.providerType];
1199
+ const azureDeployment = config.azure?.deployment || config.azure?.deployments?.[0];
1065
1200
  const providerConfig: LLMProviderConfig = {
1066
1201
  type: config.providerType,
1067
1202
  model: LLMProviderFactory.getModelId(
@@ -1070,7 +1205,12 @@ export async function setupIpcHandlers(
1070
1205
  config.ollama?.model,
1071
1206
  config.gemini?.model,
1072
1207
  config.openrouter?.model,
1073
- config.openai?.model
1208
+ config.openai?.model,
1209
+ azureDeployment,
1210
+ config.groq?.model,
1211
+ config.xai?.model,
1212
+ config.kimi?.model,
1213
+ config.customProviders
1074
1214
  ),
1075
1215
  anthropicApiKey: config.anthropic?.apiKey,
1076
1216
  awsRegion: config.bedrock?.region,
@@ -1082,9 +1222,22 @@ export async function setupIpcHandlers(
1082
1222
  ollamaApiKey: config.ollama?.apiKey,
1083
1223
  geminiApiKey: config.gemini?.apiKey,
1084
1224
  openrouterApiKey: config.openrouter?.apiKey,
1225
+ openrouterBaseUrl: config.openrouter?.baseUrl,
1085
1226
  openaiApiKey: config.openai?.apiKey,
1086
1227
  openaiAccessToken: openaiAccessToken,
1087
1228
  openaiRefreshToken: openaiRefreshToken,
1229
+ azureApiKey: config.azure?.apiKey,
1230
+ azureEndpoint: config.azure?.endpoint,
1231
+ azureDeployment: azureDeployment,
1232
+ azureApiVersion: config.azure?.apiVersion,
1233
+ groqApiKey: config.groq?.apiKey,
1234
+ groqBaseUrl: config.groq?.baseUrl,
1235
+ xaiApiKey: config.xai?.apiKey,
1236
+ xaiBaseUrl: config.xai?.baseUrl,
1237
+ kimiApiKey: config.kimi?.apiKey,
1238
+ kimiBaseUrl: config.kimi?.baseUrl,
1239
+ providerApiKey: customProviderConfig?.apiKey,
1240
+ providerBaseUrl: customProviderConfig?.baseUrl,
1088
1241
  };
1089
1242
  return LLMProviderFactory.testProvider(providerConfig);
1090
1243
  });
@@ -1106,134 +1259,232 @@ export async function setupIpcHandlers(
1106
1259
  // Get models based on the current provider type
1107
1260
  let models: Array<{ key: string; displayName: string; description: string }> = [];
1108
1261
  let currentModel = settings.modelKey;
1109
-
1110
- switch (settings.providerType) {
1111
- case 'anthropic':
1112
- case 'bedrock':
1113
- // Use Anthropic/Bedrock models from MODELS
1114
- models = Object.entries(MODELS).map(([key, value]) => ({
1115
- key,
1116
- displayName: value.displayName,
1117
- description: key.includes('opus') ? 'Most capable for complex work' :
1118
- key.includes('sonnet') ? 'Balanced performance and speed' :
1119
- 'Fast and efficient',
1120
- }));
1121
- break;
1122
-
1123
- case 'gemini': {
1124
- // For Gemini, use the specific model from settings (full model ID)
1125
- currentModel = settings.gemini?.model || 'gemini-2.0-flash';
1126
- // Use cached models if available, otherwise fall back to static list
1127
- const cachedGemini = LLMProviderFactory.getCachedModels('gemini');
1128
- if (cachedGemini && cachedGemini.length > 0) {
1129
- models = cachedGemini;
1130
- } else {
1131
- // Fall back to static models
1132
- models = Object.values(GEMINI_MODELS).map((value) => ({
1133
- key: value.id,
1262
+ const resolvedProviderType = resolveCustomProviderId(settings.providerType);
1263
+ const customEntry = CUSTOM_PROVIDER_MAP.get(resolvedProviderType as any);
1264
+
1265
+ if (customEntry) {
1266
+ const customConfig = settings.customProviders?.[resolvedProviderType] || settings.customProviders?.[settings.providerType];
1267
+ currentModel = customConfig?.model || customEntry.defaultModel;
1268
+ models = [
1269
+ {
1270
+ key: currentModel,
1271
+ displayName: currentModel,
1272
+ description: customEntry.description || `${customEntry.name} model`,
1273
+ },
1274
+ ];
1275
+ } else {
1276
+ switch (settings.providerType) {
1277
+ case 'anthropic':
1278
+ case 'bedrock':
1279
+ // Use Anthropic/Bedrock models from MODELS
1280
+ models = Object.entries(MODELS).map(([key, value]) => ({
1281
+ key,
1134
1282
  displayName: value.displayName,
1135
- description: value.description,
1283
+ description: key.includes('opus') ? 'Most capable for complex work' :
1284
+ key.includes('sonnet') ? 'Balanced performance and speed' :
1285
+ 'Fast and efficient',
1136
1286
  }));
1287
+ break;
1288
+
1289
+ case 'gemini': {
1290
+ // For Gemini, use the specific model from settings (full model ID)
1291
+ currentModel = settings.gemini?.model || 'gemini-2.0-flash';
1292
+ // Use cached models if available, otherwise fall back to static list
1293
+ const cachedGemini = LLMProviderFactory.getCachedModels('gemini');
1294
+ if (cachedGemini && cachedGemini.length > 0) {
1295
+ models = cachedGemini;
1296
+ } else {
1297
+ // Fall back to static models
1298
+ models = Object.values(GEMINI_MODELS).map((value) => ({
1299
+ key: value.id,
1300
+ displayName: value.displayName,
1301
+ description: value.description,
1302
+ }));
1303
+ }
1304
+ // Ensure the currently selected model is in the list
1305
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1306
+ models.unshift({
1307
+ key: currentModel,
1308
+ displayName: currentModel,
1309
+ description: 'Selected model',
1310
+ });
1311
+ }
1312
+ break;
1137
1313
  }
1138
- // Ensure the currently selected model is in the list
1139
- if (currentModel && !models.some(m => m.key === currentModel)) {
1140
- models.unshift({
1141
- key: currentModel,
1142
- displayName: currentModel,
1143
- description: 'Selected model',
1144
- });
1314
+
1315
+ case 'openrouter': {
1316
+ // For OpenRouter, use the specific model from settings (full model ID)
1317
+ currentModel = settings.openrouter?.model || 'anthropic/claude-3.5-sonnet';
1318
+ // Use cached models if available, otherwise fall back to static list
1319
+ const cachedOpenRouter = LLMProviderFactory.getCachedModels('openrouter');
1320
+ if (cachedOpenRouter && cachedOpenRouter.length > 0) {
1321
+ models = cachedOpenRouter;
1322
+ } else {
1323
+ // Fall back to static models
1324
+ models = Object.values(OPENROUTER_MODELS).map((value) => ({
1325
+ key: value.id,
1326
+ displayName: value.displayName,
1327
+ description: value.description,
1328
+ }));
1329
+ }
1330
+ // Ensure the currently selected model is in the list
1331
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1332
+ models.unshift({
1333
+ key: currentModel,
1334
+ displayName: currentModel,
1335
+ description: 'Selected model',
1336
+ });
1337
+ }
1338
+ break;
1145
1339
  }
1146
- break;
1147
- }
1148
1340
 
1149
- case 'openrouter': {
1150
- // For OpenRouter, use the specific model from settings (full model ID)
1151
- currentModel = settings.openrouter?.model || 'anthropic/claude-3.5-sonnet';
1152
- // Use cached models if available, otherwise fall back to static list
1153
- const cachedOpenRouter = LLMProviderFactory.getCachedModels('openrouter');
1154
- if (cachedOpenRouter && cachedOpenRouter.length > 0) {
1155
- models = cachedOpenRouter;
1156
- } else {
1157
- // Fall back to static models
1158
- models = Object.values(OPENROUTER_MODELS).map((value) => ({
1159
- key: value.id,
1160
- displayName: value.displayName,
1161
- description: value.description,
1162
- }));
1341
+ case 'ollama': {
1342
+ // For Ollama, use the specific model from settings
1343
+ currentModel = settings.ollama?.model || 'llama3.2';
1344
+ // Use cached models if available, otherwise fall back to static list
1345
+ const cachedOllama = LLMProviderFactory.getCachedModels('ollama');
1346
+ if (cachedOllama && cachedOllama.length > 0) {
1347
+ models = cachedOllama;
1348
+ } else {
1349
+ // Fall back to static models
1350
+ models = Object.entries(OLLAMA_MODELS).map(([key, value]) => ({
1351
+ key,
1352
+ displayName: value.displayName,
1353
+ description: `${value.size} parameter model`,
1354
+ }));
1355
+ }
1356
+ // Ensure the currently selected model is in the list
1357
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1358
+ models.unshift({
1359
+ key: currentModel,
1360
+ displayName: currentModel,
1361
+ description: 'Selected model',
1362
+ });
1363
+ }
1364
+ break;
1163
1365
  }
1164
- // Ensure the currently selected model is in the list
1165
- if (currentModel && !models.some(m => m.key === currentModel)) {
1166
- models.unshift({
1167
- key: currentModel,
1168
- displayName: currentModel,
1169
- description: 'Selected model',
1170
- });
1366
+
1367
+ case 'openai': {
1368
+ // For OpenAI, use the specific model from settings
1369
+ currentModel = settings.openai?.model || 'gpt-4o-mini';
1370
+ // Use cached models if available, otherwise fall back to static list
1371
+ const cachedOpenAI = LLMProviderFactory.getCachedModels('openai');
1372
+ if (cachedOpenAI && cachedOpenAI.length > 0) {
1373
+ models = cachedOpenAI;
1374
+ } else {
1375
+ // Fall back to static models
1376
+ models = [
1377
+ { key: 'gpt-4o', displayName: 'GPT-4o', description: 'Most capable model for complex tasks' },
1378
+ { key: 'gpt-4o-mini', displayName: 'GPT-4o Mini', description: 'Fast and affordable for most tasks' },
1379
+ { key: 'gpt-4-turbo', displayName: 'GPT-4 Turbo', description: 'Previous generation flagship' },
1380
+ { key: 'gpt-3.5-turbo', displayName: 'GPT-3.5 Turbo', description: 'Fast and cost-effective' },
1381
+ { key: 'o1', displayName: 'o1', description: 'Advanced reasoning model' },
1382
+ { key: 'o1-mini', displayName: 'o1 Mini', description: 'Fast reasoning model' },
1383
+ ];
1384
+ }
1385
+ // Ensure the currently selected model is in the list
1386
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1387
+ models.unshift({
1388
+ key: currentModel,
1389
+ displayName: currentModel,
1390
+ description: 'Selected model',
1391
+ });
1392
+ }
1393
+ break;
1171
1394
  }
1172
- break;
1173
- }
1174
1395
 
1175
- case 'ollama': {
1176
- // For Ollama, use the specific model from settings
1177
- currentModel = settings.ollama?.model || 'llama3.2';
1178
- // Use cached models if available, otherwise fall back to static list
1179
- const cachedOllama = LLMProviderFactory.getCachedModels('ollama');
1180
- if (cachedOllama && cachedOllama.length > 0) {
1181
- models = cachedOllama;
1182
- } else {
1183
- // Fall back to static models
1184
- models = Object.entries(OLLAMA_MODELS).map(([key, value]) => ({
1185
- key,
1186
- displayName: value.displayName,
1187
- description: `${value.size} parameter model`,
1396
+ case 'azure': {
1397
+ const deployments = (settings.azure?.deployments || []).filter(Boolean);
1398
+ currentModel = settings.azure?.deployment || deployments[0] || 'deployment-name';
1399
+ models = deployments.map((deployment) => ({
1400
+ key: deployment,
1401
+ displayName: deployment,
1402
+ description: 'Azure OpenAI deployment',
1188
1403
  }));
1404
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1405
+ models.unshift({
1406
+ key: currentModel,
1407
+ displayName: currentModel,
1408
+ description: 'Selected model',
1409
+ });
1410
+ }
1411
+ break;
1189
1412
  }
1190
- // Ensure the currently selected model is in the list
1191
- if (currentModel && !models.some(m => m.key === currentModel)) {
1192
- models.unshift({
1193
- key: currentModel,
1194
- displayName: currentModel,
1195
- description: 'Selected model',
1196
- });
1413
+
1414
+ case 'groq': {
1415
+ currentModel = settings.groq?.model || 'llama-3.1-8b-instant';
1416
+ const cachedGroq = LLMProviderFactory.getCachedModels('groq');
1417
+ if (cachedGroq && cachedGroq.length > 0) {
1418
+ models = cachedGroq;
1419
+ } else {
1420
+ models = Object.values(GROQ_MODELS).map((value) => ({
1421
+ key: value.id,
1422
+ displayName: value.displayName,
1423
+ description: value.description,
1424
+ }));
1425
+ }
1426
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1427
+ models.unshift({
1428
+ key: currentModel,
1429
+ displayName: currentModel,
1430
+ description: 'Selected model',
1431
+ });
1432
+ }
1433
+ break;
1197
1434
  }
1198
- break;
1199
- }
1200
1435
 
1201
- case 'openai': {
1202
- // For OpenAI, use the specific model from settings
1203
- currentModel = settings.openai?.model || 'gpt-4o-mini';
1204
- // Use cached models if available, otherwise fall back to static list
1205
- const cachedOpenAI = LLMProviderFactory.getCachedModels('openai');
1206
- if (cachedOpenAI && cachedOpenAI.length > 0) {
1207
- models = cachedOpenAI;
1208
- } else {
1209
- // Fall back to static models
1210
- models = [
1211
- { key: 'gpt-4o', displayName: 'GPT-4o', description: 'Most capable model for complex tasks' },
1212
- { key: 'gpt-4o-mini', displayName: 'GPT-4o Mini', description: 'Fast and affordable for most tasks' },
1213
- { key: 'gpt-4-turbo', displayName: 'GPT-4 Turbo', description: 'Previous generation flagship' },
1214
- { key: 'gpt-3.5-turbo', displayName: 'GPT-3.5 Turbo', description: 'Fast and cost-effective' },
1215
- { key: 'o1', displayName: 'o1', description: 'Advanced reasoning model' },
1216
- { key: 'o1-mini', displayName: 'o1 Mini', description: 'Fast reasoning model' },
1217
- ];
1436
+ case 'xai': {
1437
+ currentModel = settings.xai?.model || 'grok-4-fast-non-reasoning';
1438
+ const cachedXai = LLMProviderFactory.getCachedModels('xai');
1439
+ if (cachedXai && cachedXai.length > 0) {
1440
+ models = cachedXai;
1441
+ } else {
1442
+ models = Object.values(XAI_MODELS).map((value) => ({
1443
+ key: value.id,
1444
+ displayName: value.displayName,
1445
+ description: value.description,
1446
+ }));
1447
+ }
1448
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1449
+ models.unshift({
1450
+ key: currentModel,
1451
+ displayName: currentModel,
1452
+ description: 'Selected model',
1453
+ });
1454
+ }
1455
+ break;
1218
1456
  }
1219
- // Ensure the currently selected model is in the list
1220
- if (currentModel && !models.some(m => m.key === currentModel)) {
1221
- models.unshift({
1222
- key: currentModel,
1223
- displayName: currentModel,
1224
- description: 'Selected model',
1225
- });
1457
+
1458
+ case 'kimi': {
1459
+ currentModel = settings.kimi?.model || 'kimi-k2.5';
1460
+ const cachedKimi = LLMProviderFactory.getCachedModels('kimi');
1461
+ if (cachedKimi && cachedKimi.length > 0) {
1462
+ models = cachedKimi;
1463
+ } else {
1464
+ models = Object.values(KIMI_MODELS).map((value) => ({
1465
+ key: value.id,
1466
+ displayName: value.displayName,
1467
+ description: value.description,
1468
+ }));
1469
+ }
1470
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1471
+ models.unshift({
1472
+ key: currentModel,
1473
+ displayName: currentModel,
1474
+ description: 'Selected model',
1475
+ });
1476
+ }
1477
+ break;
1226
1478
  }
1227
- break;
1228
- }
1229
1479
 
1230
- default:
1231
- // Fallback to Anthropic models
1232
- models = Object.entries(MODELS).map(([key, value]) => ({
1233
- key,
1234
- displayName: value.displayName,
1235
- description: 'Claude model',
1236
- }));
1480
+ default:
1481
+ // Fallback to Anthropic models
1482
+ models = Object.entries(MODELS).map(([key, value]) => ({
1483
+ key,
1484
+ displayName: value.displayName,
1485
+ description: 'Claude model',
1486
+ }));
1487
+ }
1237
1488
  }
1238
1489
 
1239
1490
  return {
@@ -1247,27 +1498,61 @@ export async function setupIpcHandlers(
1247
1498
  // Set the current model (persists selection across sessions)
1248
1499
  ipcMain.handle(IPC_CHANNELS.LLM_SET_MODEL, async (_, modelKey: string) => {
1249
1500
  const settings = LLMProviderFactory.loadSettings();
1501
+ const resolvedProviderType = resolveCustomProviderId(settings.providerType);
1250
1502
 
1251
1503
  // Update the model key based on the current provider
1252
- switch (settings.providerType) {
1253
- case 'gemini':
1254
- settings.gemini = { ...settings.gemini, model: modelKey };
1255
- break;
1256
- case 'openrouter':
1257
- settings.openrouter = { ...settings.openrouter, model: modelKey };
1258
- break;
1259
- case 'ollama':
1260
- settings.ollama = { ...settings.ollama, model: modelKey };
1261
- break;
1262
- case 'openai':
1263
- settings.openai = { ...settings.openai, model: modelKey };
1264
- break;
1265
- case 'anthropic':
1266
- case 'bedrock':
1267
- default:
1268
- // For Anthropic/Bedrock, use the modelKey field
1269
- settings.modelKey = modelKey as ModelKey;
1270
- break;
1504
+ if (CUSTOM_PROVIDER_IDS.has(resolvedProviderType as any)) {
1505
+ const existing = settings.customProviders?.[resolvedProviderType] || {};
1506
+ settings.customProviders = {
1507
+ ...(settings.customProviders || {}),
1508
+ [resolvedProviderType]: {
1509
+ ...existing,
1510
+ model: modelKey,
1511
+ },
1512
+ };
1513
+ } else {
1514
+ switch (settings.providerType) {
1515
+ case 'gemini':
1516
+ settings.gemini = { ...settings.gemini, model: modelKey };
1517
+ break;
1518
+ case 'openrouter':
1519
+ settings.openrouter = { ...settings.openrouter, model: modelKey };
1520
+ break;
1521
+ case 'ollama':
1522
+ settings.ollama = { ...settings.ollama, model: modelKey };
1523
+ break;
1524
+ case 'openai':
1525
+ settings.openai = { ...settings.openai, model: modelKey };
1526
+ break;
1527
+ case 'azure':
1528
+ {
1529
+ const existingDeployments = (settings.azure?.deployments || []).filter(Boolean);
1530
+ const nextDeployments = existingDeployments.includes(modelKey)
1531
+ ? existingDeployments
1532
+ : [modelKey, ...existingDeployments];
1533
+ settings.azure = {
1534
+ ...settings.azure,
1535
+ deployment: modelKey,
1536
+ deployments: nextDeployments.length > 0 ? nextDeployments : undefined,
1537
+ };
1538
+ }
1539
+ break;
1540
+ case 'groq':
1541
+ settings.groq = { ...settings.groq, model: modelKey };
1542
+ break;
1543
+ case 'xai':
1544
+ settings.xai = { ...settings.xai, model: modelKey };
1545
+ break;
1546
+ case 'kimi':
1547
+ settings.kimi = { ...settings.kimi, model: modelKey };
1548
+ break;
1549
+ case 'anthropic':
1550
+ case 'bedrock':
1551
+ default:
1552
+ // For Anthropic/Bedrock, use the modelKey field
1553
+ settings.modelKey = modelKey as ModelKey;
1554
+ break;
1555
+ }
1271
1556
  }
1272
1557
 
1273
1558
  LLMProviderFactory.saveSettings(settings);
@@ -1302,9 +1587,9 @@ export async function setupIpcHandlers(
1302
1587
  return models;
1303
1588
  });
1304
1589
 
1305
- ipcMain.handle(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS, async (_, apiKey?: string) => {
1590
+ ipcMain.handle(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS, async (_, apiKey?: string, baseUrl?: string) => {
1306
1591
  checkRateLimit(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS);
1307
- const models = await LLMProviderFactory.getOpenRouterModels(apiKey);
1592
+ const models = await LLMProviderFactory.getOpenRouterModels(apiKey, baseUrl);
1308
1593
  // Cache the models for use in config status
1309
1594
  const cachedModels = models.map(m => ({
1310
1595
  key: m.id,
@@ -1329,6 +1614,42 @@ export async function setupIpcHandlers(
1329
1614
  return models;
1330
1615
  });
1331
1616
 
1617
+ ipcMain.handle(IPC_CHANNELS.LLM_GET_GROQ_MODELS, async (_, apiKey?: string, baseUrl?: string) => {
1618
+ checkRateLimit(IPC_CHANNELS.LLM_GET_GROQ_MODELS);
1619
+ const models = await LLMProviderFactory.getGroqModels(apiKey, baseUrl);
1620
+ const cachedModels = models.map(m => ({
1621
+ key: m.id,
1622
+ displayName: m.name,
1623
+ description: 'Groq model',
1624
+ }));
1625
+ LLMProviderFactory.saveCachedModels('groq', cachedModels);
1626
+ return models;
1627
+ });
1628
+
1629
+ ipcMain.handle(IPC_CHANNELS.LLM_GET_XAI_MODELS, async (_, apiKey?: string, baseUrl?: string) => {
1630
+ checkRateLimit(IPC_CHANNELS.LLM_GET_XAI_MODELS);
1631
+ const models = await LLMProviderFactory.getXAIModels(apiKey, baseUrl);
1632
+ const cachedModels = models.map(m => ({
1633
+ key: m.id,
1634
+ displayName: m.name,
1635
+ description: 'xAI model',
1636
+ }));
1637
+ LLMProviderFactory.saveCachedModels('xai', cachedModels);
1638
+ return models;
1639
+ });
1640
+
1641
+ ipcMain.handle(IPC_CHANNELS.LLM_GET_KIMI_MODELS, async (_, apiKey?: string, baseUrl?: string) => {
1642
+ checkRateLimit(IPC_CHANNELS.LLM_GET_KIMI_MODELS);
1643
+ const models = await LLMProviderFactory.getKimiModels(apiKey, baseUrl);
1644
+ const cachedModels = models.map(m => ({
1645
+ key: m.id,
1646
+ displayName: m.name,
1647
+ description: 'Kimi model',
1648
+ }));
1649
+ LLMProviderFactory.saveCachedModels('kimi', cachedModels);
1650
+ return models;
1651
+ });
1652
+
1332
1653
  // OpenAI OAuth handlers
1333
1654
  ipcMain.handle(IPC_CHANNELS.LLM_OPENAI_OAUTH_START, async () => {
1334
1655
  checkRateLimit(IPC_CHANNELS.LLM_OPENAI_OAUTH_START);
@@ -1464,6 +1785,233 @@ export async function setupIpcHandlers(
1464
1785
  };
1465
1786
  });
1466
1787
 
1788
+ // Notion Settings handlers
1789
+ ipcMain.handle(IPC_CHANNELS.NOTION_GET_SETTINGS, async () => {
1790
+ return NotionSettingsManager.loadSettings();
1791
+ });
1792
+
1793
+ ipcMain.handle(IPC_CHANNELS.NOTION_SAVE_SETTINGS, async (_, settings) => {
1794
+ checkRateLimit(IPC_CHANNELS.NOTION_SAVE_SETTINGS);
1795
+ const validated = validateInput(NotionSettingsSchema, settings, 'notion settings') as NotionSettingsData;
1796
+ NotionSettingsManager.saveSettings(validated);
1797
+ NotionSettingsManager.clearCache();
1798
+ return { success: true };
1799
+ });
1800
+
1801
+ ipcMain.handle(IPC_CHANNELS.NOTION_TEST_CONNECTION, async () => {
1802
+ checkRateLimit(IPC_CHANNELS.NOTION_TEST_CONNECTION);
1803
+ const settings = NotionSettingsManager.loadSettings();
1804
+ return testNotionConnection(settings);
1805
+ });
1806
+
1807
+ ipcMain.handle(IPC_CHANNELS.NOTION_GET_STATUS, async () => {
1808
+ checkRateLimit(IPC_CHANNELS.NOTION_GET_STATUS);
1809
+ const settings = NotionSettingsManager.loadSettings();
1810
+ if (!settings.apiKey) {
1811
+ return { configured: false, connected: false };
1812
+ }
1813
+ if (!settings.enabled) {
1814
+ return { configured: true, connected: false };
1815
+ }
1816
+ const result = await testNotionConnection(settings);
1817
+ return {
1818
+ configured: true,
1819
+ connected: result.success,
1820
+ name: result.name,
1821
+ error: result.success ? undefined : result.error,
1822
+ };
1823
+ });
1824
+
1825
+ // Box Settings handlers
1826
+ ipcMain.handle(IPC_CHANNELS.BOX_GET_SETTINGS, async () => {
1827
+ return BoxSettingsManager.loadSettings();
1828
+ });
1829
+
1830
+ ipcMain.handle(IPC_CHANNELS.BOX_SAVE_SETTINGS, async (_, settings) => {
1831
+ checkRateLimit(IPC_CHANNELS.BOX_SAVE_SETTINGS);
1832
+ const validated = validateInput(BoxSettingsSchema, settings, 'box settings') as BoxSettingsData;
1833
+ BoxSettingsManager.saveSettings(validated);
1834
+ BoxSettingsManager.clearCache();
1835
+ return { success: true };
1836
+ });
1837
+
1838
+ ipcMain.handle(IPC_CHANNELS.BOX_TEST_CONNECTION, async () => {
1839
+ checkRateLimit(IPC_CHANNELS.BOX_TEST_CONNECTION);
1840
+ const settings = BoxSettingsManager.loadSettings();
1841
+ return testBoxConnection(settings);
1842
+ });
1843
+
1844
+ ipcMain.handle(IPC_CHANNELS.BOX_GET_STATUS, async () => {
1845
+ checkRateLimit(IPC_CHANNELS.BOX_GET_STATUS);
1846
+ const settings = BoxSettingsManager.loadSettings();
1847
+ if (!settings.accessToken) {
1848
+ return { configured: false, connected: false };
1849
+ }
1850
+ if (!settings.enabled) {
1851
+ return { configured: true, connected: false };
1852
+ }
1853
+ const result = await testBoxConnection(settings);
1854
+ return {
1855
+ configured: true,
1856
+ connected: result.success,
1857
+ name: result.name,
1858
+ error: result.success ? undefined : result.error,
1859
+ };
1860
+ });
1861
+
1862
+ // OneDrive Settings handlers
1863
+ ipcMain.handle(IPC_CHANNELS.ONEDRIVE_GET_SETTINGS, async () => {
1864
+ return OneDriveSettingsManager.loadSettings();
1865
+ });
1866
+
1867
+ ipcMain.handle(IPC_CHANNELS.ONEDRIVE_SAVE_SETTINGS, async (_, settings) => {
1868
+ checkRateLimit(IPC_CHANNELS.ONEDRIVE_SAVE_SETTINGS);
1869
+ const validated = validateInput(OneDriveSettingsSchema, settings, 'onedrive settings') as OneDriveSettingsData;
1870
+ OneDriveSettingsManager.saveSettings(validated);
1871
+ OneDriveSettingsManager.clearCache();
1872
+ return { success: true };
1873
+ });
1874
+
1875
+ ipcMain.handle(IPC_CHANNELS.ONEDRIVE_TEST_CONNECTION, async () => {
1876
+ checkRateLimit(IPC_CHANNELS.ONEDRIVE_TEST_CONNECTION);
1877
+ const settings = OneDriveSettingsManager.loadSettings();
1878
+ return testOneDriveConnection(settings);
1879
+ });
1880
+
1881
+ ipcMain.handle(IPC_CHANNELS.ONEDRIVE_GET_STATUS, async () => {
1882
+ checkRateLimit(IPC_CHANNELS.ONEDRIVE_GET_STATUS);
1883
+ const settings = OneDriveSettingsManager.loadSettings();
1884
+ if (!settings.accessToken) {
1885
+ return { configured: false, connected: false };
1886
+ }
1887
+ if (!settings.enabled) {
1888
+ return { configured: true, connected: false };
1889
+ }
1890
+ const result = await testOneDriveConnection(settings);
1891
+ return {
1892
+ configured: true,
1893
+ connected: result.success,
1894
+ name: result.name,
1895
+ error: result.success ? undefined : result.error,
1896
+ };
1897
+ });
1898
+
1899
+ // Google Workspace Settings handlers
1900
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_WORKSPACE_GET_SETTINGS, async () => {
1901
+ return GoogleWorkspaceSettingsManager.loadSettings();
1902
+ });
1903
+
1904
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_WORKSPACE_SAVE_SETTINGS, async (_, settings) => {
1905
+ checkRateLimit(IPC_CHANNELS.GOOGLE_WORKSPACE_SAVE_SETTINGS);
1906
+ const validated = validateInput(GoogleWorkspaceSettingsSchema, settings, 'google workspace settings') as GoogleWorkspaceSettingsData;
1907
+ GoogleWorkspaceSettingsManager.saveSettings(validated);
1908
+ GoogleWorkspaceSettingsManager.clearCache();
1909
+ return { success: true };
1910
+ });
1911
+
1912
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_WORKSPACE_TEST_CONNECTION, async () => {
1913
+ checkRateLimit(IPC_CHANNELS.GOOGLE_WORKSPACE_TEST_CONNECTION);
1914
+ const settings = GoogleWorkspaceSettingsManager.loadSettings();
1915
+ return testGoogleWorkspaceConnection(settings);
1916
+ });
1917
+
1918
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_WORKSPACE_GET_STATUS, async () => {
1919
+ checkRateLimit(IPC_CHANNELS.GOOGLE_WORKSPACE_GET_STATUS);
1920
+ const settings = GoogleWorkspaceSettingsManager.loadSettings();
1921
+ if (!settings.accessToken && !settings.refreshToken) {
1922
+ return { configured: false, connected: false };
1923
+ }
1924
+ if (!settings.enabled) {
1925
+ return { configured: true, connected: false };
1926
+ }
1927
+ const result = await testGoogleWorkspaceConnection(settings);
1928
+ return {
1929
+ configured: true,
1930
+ connected: result.success,
1931
+ name: result.name,
1932
+ error: result.success ? undefined : result.error,
1933
+ };
1934
+ });
1935
+
1936
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_WORKSPACE_OAUTH_START, async (_, payload) => {
1937
+ checkRateLimit(IPC_CHANNELS.GOOGLE_WORKSPACE_OAUTH_START);
1938
+ return startGoogleWorkspaceOAuth(payload);
1939
+ });
1940
+
1941
+ // Dropbox Settings handlers
1942
+ ipcMain.handle(IPC_CHANNELS.DROPBOX_GET_SETTINGS, async () => {
1943
+ return DropboxSettingsManager.loadSettings();
1944
+ });
1945
+
1946
+ ipcMain.handle(IPC_CHANNELS.DROPBOX_SAVE_SETTINGS, async (_, settings) => {
1947
+ checkRateLimit(IPC_CHANNELS.DROPBOX_SAVE_SETTINGS);
1948
+ const validated = validateInput(DropboxSettingsSchema, settings, 'dropbox settings') as DropboxSettingsData;
1949
+ DropboxSettingsManager.saveSettings(validated);
1950
+ DropboxSettingsManager.clearCache();
1951
+ return { success: true };
1952
+ });
1953
+
1954
+ ipcMain.handle(IPC_CHANNELS.DROPBOX_TEST_CONNECTION, async () => {
1955
+ checkRateLimit(IPC_CHANNELS.DROPBOX_TEST_CONNECTION);
1956
+ const settings = DropboxSettingsManager.loadSettings();
1957
+ return testDropboxConnection(settings);
1958
+ });
1959
+
1960
+ ipcMain.handle(IPC_CHANNELS.DROPBOX_GET_STATUS, async () => {
1961
+ checkRateLimit(IPC_CHANNELS.DROPBOX_GET_STATUS);
1962
+ const settings = DropboxSettingsManager.loadSettings();
1963
+ if (!settings.accessToken) {
1964
+ return { configured: false, connected: false };
1965
+ }
1966
+ if (!settings.enabled) {
1967
+ return { configured: true, connected: false };
1968
+ }
1969
+ const result = await testDropboxConnection(settings);
1970
+ return {
1971
+ configured: true,
1972
+ connected: result.success,
1973
+ name: result.name,
1974
+ error: result.success ? undefined : result.error,
1975
+ };
1976
+ });
1977
+
1978
+ // SharePoint Settings handlers
1979
+ ipcMain.handle(IPC_CHANNELS.SHAREPOINT_GET_SETTINGS, async () => {
1980
+ return SharePointSettingsManager.loadSettings();
1981
+ });
1982
+
1983
+ ipcMain.handle(IPC_CHANNELS.SHAREPOINT_SAVE_SETTINGS, async (_, settings) => {
1984
+ checkRateLimit(IPC_CHANNELS.SHAREPOINT_SAVE_SETTINGS);
1985
+ const validated = validateInput(SharePointSettingsSchema, settings, 'sharepoint settings') as SharePointSettingsData;
1986
+ SharePointSettingsManager.saveSettings(validated);
1987
+ SharePointSettingsManager.clearCache();
1988
+ return { success: true };
1989
+ });
1990
+
1991
+ ipcMain.handle(IPC_CHANNELS.SHAREPOINT_TEST_CONNECTION, async () => {
1992
+ checkRateLimit(IPC_CHANNELS.SHAREPOINT_TEST_CONNECTION);
1993
+ const settings = SharePointSettingsManager.loadSettings();
1994
+ return testSharePointConnection(settings);
1995
+ });
1996
+
1997
+ ipcMain.handle(IPC_CHANNELS.SHAREPOINT_GET_STATUS, async () => {
1998
+ checkRateLimit(IPC_CHANNELS.SHAREPOINT_GET_STATUS);
1999
+ const settings = SharePointSettingsManager.loadSettings();
2000
+ if (!settings.accessToken) {
2001
+ return { configured: false, connected: false };
2002
+ }
2003
+ if (!settings.enabled) {
2004
+ return { configured: true, connected: false };
2005
+ }
2006
+ const result = await testSharePointConnection(settings);
2007
+ return {
2008
+ configured: true,
2009
+ connected: result.success,
2010
+ name: result.name,
2011
+ error: result.success ? undefined : result.error,
2012
+ };
2013
+ });
2014
+
1467
2015
  // Gateway / Channel handlers
1468
2016
  ipcMain.handle(IPC_CHANNELS.GATEWAY_GET_CHANNELS, async () => {
1469
2017
  if (!gateway) return [];
@@ -2426,6 +2974,7 @@ function setupMCPHandlers(): void {
2426
2974
  rateLimiter.configure(IPC_CHANNELS.MCP_CONNECT_SERVER, RATE_LIMIT_CONFIGS.expensive);
2427
2975
  rateLimiter.configure(IPC_CHANNELS.MCP_TEST_SERVER, RATE_LIMIT_CONFIGS.expensive);
2428
2976
  rateLimiter.configure(IPC_CHANNELS.MCP_REGISTRY_INSTALL, RATE_LIMIT_CONFIGS.expensive);
2977
+ rateLimiter.configure(IPC_CHANNELS.MCP_CONNECTOR_OAUTH_START, RATE_LIMIT_CONFIGS.expensive);
2429
2978
 
2430
2979
  // Initialize MCP settings manager
2431
2980
  MCPSettingsManager.initialize();
@@ -2553,6 +3102,13 @@ function setupMCPHandlers(): void {
2553
3102
  return MCPRegistryManager.updateServer(validatedId);
2554
3103
  });
2555
3104
 
3105
+ // MCP Connector OAuth (Salesforce/Jira)
3106
+ ipcMain.handle(IPC_CHANNELS.MCP_CONNECTOR_OAUTH_START, async (_, payload) => {
3107
+ checkRateLimit(IPC_CHANNELS.MCP_CONNECTOR_OAUTH_START);
3108
+ const validated = validateInput(MCPConnectorOAuthSchema, payload, 'connector oauth');
3109
+ return startConnectorOAuth(validated);
3110
+ });
3111
+
2556
3112
  // MCP Host handlers
2557
3113
  ipcMain.handle(IPC_CHANNELS.MCP_HOST_START, async () => {
2558
3114
  const hostServer = MCPHostServer.getInstance();