cowork-os 0.3.21 → 0.3.23

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 (170) hide show
  1. package/README.md +293 -6
  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/daemon.js +25 -0
  48. package/dist/electron/electron/agent/executor.js +181 -26
  49. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  50. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  51. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  52. package/dist/electron/electron/agent/llm/index.js +11 -1
  53. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  54. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  55. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  56. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  57. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  58. package/dist/electron/electron/agent/llm/provider-factory.js +318 -4
  59. package/dist/electron/electron/agent/llm/types.js +66 -1
  60. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  61. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  62. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  63. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  64. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  65. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  66. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  67. package/dist/electron/electron/agent/tools/registry.js +541 -0
  68. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  69. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  70. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  71. package/dist/electron/electron/gateway/index.js +1 -0
  72. package/dist/electron/electron/gateway/router.js +123 -143
  73. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  74. package/dist/electron/electron/ipc/handlers.js +627 -158
  75. package/dist/electron/electron/main.js +63 -0
  76. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  77. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  78. package/dist/electron/electron/memory/MemoryService.js +1 -1
  79. package/dist/electron/electron/preload.js +74 -1
  80. package/dist/electron/electron/settings/box-manager.js +54 -0
  81. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  82. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  83. package/dist/electron/electron/settings/notion-manager.js +56 -0
  84. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  85. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  86. package/dist/electron/electron/utils/box-api.js +153 -0
  87. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  88. package/dist/electron/electron/utils/env-migration.js +19 -0
  89. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  90. package/dist/electron/electron/utils/notion-api.js +103 -0
  91. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  92. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  93. package/dist/electron/electron/utils/validation.js +82 -3
  94. package/dist/electron/electron/utils/x-cli.js +1 -1
  95. package/dist/electron/shared/channelMessages.js +284 -3
  96. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  97. package/dist/electron/shared/types.js +88 -1
  98. package/package.json +12 -2
  99. package/src/electron/agent/executor.ts +205 -28
  100. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  101. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  102. package/src/electron/agent/llm/groq-provider.ts +39 -0
  103. package/src/electron/agent/llm/index.ts +5 -0
  104. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  105. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  106. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  107. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  108. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  109. package/src/electron/agent/llm/provider-factory.ts +414 -6
  110. package/src/electron/agent/llm/types.ts +90 -1
  111. package/src/electron/agent/llm/xai-provider.ts +39 -0
  112. package/src/electron/agent/tools/box-tools.ts +239 -0
  113. package/src/electron/agent/tools/builtin-settings.ts +34 -0
  114. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  115. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  116. package/src/electron/agent/tools/notion-tools.ts +330 -0
  117. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  118. package/src/electron/agent/tools/registry.ts +565 -0
  119. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  120. package/src/electron/agent/tools/shell-tools.ts +11 -3
  121. package/src/electron/agent/tools/x-tools.ts +1 -1
  122. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  123. package/src/electron/gateway/index.ts +1 -0
  124. package/src/electron/gateway/router.ts +134 -149
  125. package/src/electron/ipc/canvas-handlers.ts +10 -0
  126. package/src/electron/ipc/handlers.ts +673 -153
  127. package/src/electron/main.ts +35 -0
  128. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  129. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  130. package/src/electron/memory/MemoryService.ts +5 -1
  131. package/src/electron/preload.ts +167 -4
  132. package/src/electron/settings/box-manager.ts +58 -0
  133. package/src/electron/settings/dropbox-manager.ts +58 -0
  134. package/src/electron/settings/google-drive-manager.ts +58 -0
  135. package/src/electron/settings/notion-manager.ts +60 -0
  136. package/src/electron/settings/onedrive-manager.ts +58 -0
  137. package/src/electron/settings/sharepoint-manager.ts +58 -0
  138. package/src/electron/utils/box-api.ts +184 -0
  139. package/src/electron/utils/dropbox-api.ts +171 -0
  140. package/src/electron/utils/env-migration.ts +22 -0
  141. package/src/electron/utils/google-drive-api.ts +183 -0
  142. package/src/electron/utils/notion-api.ts +126 -0
  143. package/src/electron/utils/onedrive-api.ts +137 -0
  144. package/src/electron/utils/sharepoint-api.ts +132 -0
  145. package/src/electron/utils/validation.ts +102 -1
  146. package/src/electron/utils/x-cli.ts +1 -1
  147. package/src/renderer/App.tsx +20 -2
  148. package/src/renderer/components/BoxSettings.tsx +203 -0
  149. package/src/renderer/components/BrowserView.tsx +101 -0
  150. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  151. package/src/renderer/components/CanvasPreview.tsx +68 -1
  152. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  153. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  154. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  155. package/src/renderer/components/DropboxSettings.tsx +202 -0
  156. package/src/renderer/components/GoogleDriveSettings.tsx +201 -0
  157. package/src/renderer/components/MCPSettings.tsx +56 -0
  158. package/src/renderer/components/MainContent.tsx +270 -34
  159. package/src/renderer/components/NotionSettings.tsx +231 -0
  160. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  161. package/src/renderer/components/OnboardingModal.tsx +70 -1
  162. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  163. package/src/renderer/components/Settings.tsx +611 -8
  164. package/src/renderer/components/SharePointSettings.tsx +224 -0
  165. package/src/renderer/components/Sidebar.tsx +25 -9
  166. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  167. package/src/renderer/styles/index.css +438 -25
  168. package/src/shared/channelMessages.ts +367 -4
  169. package/src/shared/llm-provider-catalog.ts +217 -0
  170. package/src/shared/types.ts +226 -1
@@ -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, GoogleDriveSettingsData, 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,17 @@ import {
42
56
  TaskCreateSchema,
43
57
  TaskRenameSchema,
44
58
  TaskMessageSchema,
59
+ FileImportSchema,
45
60
  ApprovalResponseSchema,
46
61
  LLMSettingsSchema,
47
62
  SearchSettingsSchema,
48
63
  XSettingsSchema,
64
+ NotionSettingsSchema,
65
+ BoxSettingsSchema,
66
+ OneDriveSettingsSchema,
67
+ GoogleDriveSettingsSchema,
68
+ DropboxSettingsSchema,
69
+ SharePointSettingsSchema,
49
70
  AddChannelSchema,
50
71
  UpdateChannelSchema,
51
72
  GrantAccessSchema,
@@ -54,10 +75,24 @@ import {
54
75
  GuardrailSettingsSchema,
55
76
  UUIDSchema,
56
77
  StringIdSchema,
78
+ MCPConnectorOAuthSchema,
57
79
  } from '../utils/validation';
58
80
  import { GuardrailManager } from '../guardrails/guardrail-manager';
59
81
  import { AppearanceManager } from '../settings/appearance-manager';
60
82
  import { PersonalityManager } from '../settings/personality-manager';
83
+ import { NotionSettingsManager } from '../settings/notion-manager';
84
+ import { testNotionConnection } from '../utils/notion-api';
85
+ import { BoxSettingsManager } from '../settings/box-manager';
86
+ import { OneDriveSettingsManager } from '../settings/onedrive-manager';
87
+ import { GoogleDriveSettingsManager } from '../settings/google-drive-manager';
88
+ import { DropboxSettingsManager } from '../settings/dropbox-manager';
89
+ import { SharePointSettingsManager } from '../settings/sharepoint-manager';
90
+ import { testBoxConnection } from '../utils/box-api';
91
+ import { testOneDriveConnection } from '../utils/onedrive-api';
92
+ import { testGoogleDriveConnection } from '../utils/google-drive-api';
93
+ import { testDropboxConnection } from '../utils/dropbox-api';
94
+ import { testSharePointConnection } from '../utils/sharepoint-api';
95
+ import { startConnectorOAuth } from '../mcp/oauth/connector-oauth';
61
96
 
62
97
  const normalizeMentionToken = (value: string): string =>
63
98
  value.toLowerCase().replace(/[^a-z0-9]/g, '');
@@ -124,7 +159,9 @@ const scoreAgentForTask = (role: AgentRole, text: string) => {
124
159
  return score;
125
160
  };
126
161
 
127
- const selectBestAgentsForTask = (text: string, roles: AgentRole[]) => {
162
+ const MAX_AUTO_AGENTS = 4;
163
+
164
+ const selectBestAgentsForTask = (text: string, roles: AgentRole[], maxAgents = MAX_AUTO_AGENTS) => {
128
165
  if (roles.length === 0) return roles;
129
166
  const scored = roles
130
167
  .map((role) => ({ role, score: scoreAgentForTask(role, text) }))
@@ -139,19 +176,19 @@ const selectBestAgentsForTask = (text: string, roles: AgentRole[]) => {
139
176
  const threshold = Math.max(1, maxScore - 2);
140
177
  const selected = withScore
141
178
  .filter((entry) => entry.score >= threshold)
142
- .slice(0, 4)
179
+ .slice(0, maxAgents)
143
180
  .map((entry) => entry.role);
144
- return selected.length > 0 ? selected : withScore.slice(0, 3).map((entry) => entry.role);
181
+ return selected.length > 0 ? selected : withScore.slice(0, maxAgents).map((entry) => entry.role);
145
182
  }
146
183
 
147
184
  const leads = roles
148
185
  .filter((role) => role.autonomyLevel === 'lead')
149
186
  .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
150
187
  if (leads.length > 0) {
151
- return leads.slice(0, 3);
188
+ return leads.slice(0, maxAgents);
152
189
  }
153
190
 
154
- return roles.slice(0, Math.min(3, roles.length));
191
+ return roles.slice(0, Math.min(maxAgents, roles.length));
155
192
  };
156
193
 
157
194
  const extractMentionedRoles = (
@@ -159,10 +196,9 @@ const extractMentionedRoles = (
159
196
  roles: AgentRole[]
160
197
  ) => {
161
198
  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
- }
199
+ const useSmartSelection = /\B@everybody\b/.test(normalizedText) ||
200
+ /\B@all\b/.test(normalizedText) ||
201
+ /\B@everyone\b/.test(normalizedText);
166
202
 
167
203
  const index = buildAgentMentionIndex(roles);
168
204
  const matches = new Map<string, AgentRole>();
@@ -180,11 +216,15 @@ const extractMentionedRoles = (
180
216
 
181
217
  if (matches.size > 0) {
182
218
  if (useSmartSelection) {
183
- const selected = selectBestAgentsForTask(text, roles);
184
219
  const merged = new Map<string, AgentRole>();
185
- selected.forEach((role) => merged.set(role.id, role));
186
220
  matches.forEach((role) => merged.set(role.id, role));
187
- return Array.from(merged.values());
221
+ const selected = selectBestAgentsForTask(text, roles, MAX_AUTO_AGENTS);
222
+ selected.forEach((role) => {
223
+ if (merged.size < MAX_AUTO_AGENTS) {
224
+ merged.set(role.id, role);
225
+ }
226
+ });
227
+ return Array.from(merged.values()).slice(0, MAX_AUTO_AGENTS);
188
228
  }
189
229
  return Array.from(matches.values());
190
230
  }
@@ -200,7 +240,7 @@ const extractMentionedRoles = (
200
240
  });
201
241
 
202
242
  if (useSmartSelection) {
203
- return selectBestAgentsForTask(text, roles);
243
+ return selectBestAgentsForTask(text, roles, MAX_AUTO_AGENTS);
204
244
  }
205
245
 
206
246
  return Array.from(matches.values());
@@ -303,6 +343,8 @@ import { getVoiceService } from '../voice/VoiceService';
303
343
 
304
344
  // Global notification service instance
305
345
  let notificationService: NotificationService | null = null;
346
+ const resolveCustomProviderId = (providerType: string) =>
347
+ providerType === 'kimi-coding' ? 'kimi-code' : providerType;
306
348
 
307
349
  /**
308
350
  * Get the notification service instance
@@ -329,6 +371,9 @@ rateLimiter.configure(IPC_CHANNELS.LLM_GET_OLLAMA_MODELS, RATE_LIMIT_CONFIGS.sta
329
371
  rateLimiter.configure(IPC_CHANNELS.LLM_GET_GEMINI_MODELS, RATE_LIMIT_CONFIGS.standard);
330
372
  rateLimiter.configure(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS, RATE_LIMIT_CONFIGS.standard);
331
373
  rateLimiter.configure(IPC_CHANNELS.LLM_GET_BEDROCK_MODELS, RATE_LIMIT_CONFIGS.standard);
374
+ rateLimiter.configure(IPC_CHANNELS.LLM_GET_GROQ_MODELS, RATE_LIMIT_CONFIGS.standard);
375
+ rateLimiter.configure(IPC_CHANNELS.LLM_GET_XAI_MODELS, RATE_LIMIT_CONFIGS.standard);
376
+ rateLimiter.configure(IPC_CHANNELS.LLM_GET_KIMI_MODELS, RATE_LIMIT_CONFIGS.standard);
332
377
  rateLimiter.configure(IPC_CHANNELS.SEARCH_SAVE_SETTINGS, RATE_LIMIT_CONFIGS.limited);
333
378
  rateLimiter.configure(IPC_CHANNELS.SEARCH_TEST_PROVIDER, RATE_LIMIT_CONFIGS.expensive);
334
379
  rateLimiter.configure(IPC_CHANNELS.GATEWAY_ADD_CHANNEL, RATE_LIMIT_CONFIGS.limited);
@@ -624,6 +669,92 @@ export async function setupIpcHandlers(
624
669
  }
625
670
  });
626
671
 
672
+ // File import handler - copy selected files into the workspace for attachment use
673
+ ipcMain.handle('file:importToWorkspace', async (_, data: { workspaceId: string; files: string[] }) => {
674
+ const validated = validateInput(FileImportSchema, data, 'file import');
675
+ const workspace = workspaceRepo.findById(validated.workspaceId);
676
+
677
+ if (!workspace) {
678
+ throw new Error(`Workspace not found: ${validated.workspaceId}`);
679
+ }
680
+
681
+ if (!workspace.permissions.write) {
682
+ throw new Error('Write permission not granted for workspace');
683
+ }
684
+
685
+ const sanitizeFileName = (fileName: string): string => {
686
+ const sanitized = fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').trim();
687
+ return sanitized.length > 0 ? sanitized : 'file';
688
+ };
689
+
690
+ const ensureUniqueName = (dir: string, baseName: string, usedNames: Set<string>): string => {
691
+ const ext = path.extname(baseName);
692
+ const stem = path.basename(baseName, ext);
693
+ let candidate = baseName;
694
+ let counter = 1;
695
+ while (usedNames.has(candidate) || fsSync.existsSync(path.join(dir, candidate))) {
696
+ candidate = `${stem}-${counter}${ext}`;
697
+ counter += 1;
698
+ }
699
+ usedNames.add(candidate);
700
+ return candidate;
701
+ };
702
+
703
+ let uploadRoot: string | null = null;
704
+ const usedNames = new Set<string>();
705
+
706
+ const ensureUploadRoot = async (): Promise<string> => {
707
+ if (uploadRoot) return uploadRoot;
708
+ uploadRoot = path.join(workspace.path, '.cowork', 'uploads', `${Date.now()}`);
709
+ await fs.mkdir(uploadRoot, { recursive: true });
710
+ return uploadRoot;
711
+ };
712
+
713
+ const results: Array<{ relativePath: string; fileName: string; size: number; mimeType?: string }> = [];
714
+
715
+ for (const filePath of validated.files) {
716
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
717
+ const stats = await fs.stat(absolutePath);
718
+
719
+ if (!stats.isFile()) {
720
+ throw new Error(`Not a file: ${filePath}`);
721
+ }
722
+
723
+ const sizeCheck = GuardrailManager.isFileSizeExceeded(stats.size);
724
+ if (sizeCheck.exceeded) {
725
+ throw new Error(`File "${path.basename(filePath)}" is ${sizeCheck.sizeMB.toFixed(1)}MB and exceeds the ${sizeCheck.limitMB}MB limit.`);
726
+ }
727
+
728
+ const mimeType = (mime.lookup(absolutePath) || undefined) as string | undefined;
729
+
730
+ if (isPathWithinWorkspace(absolutePath, workspace.path)) {
731
+ results.push({
732
+ relativePath: path.relative(workspace.path, absolutePath),
733
+ fileName: path.basename(absolutePath),
734
+ size: stats.size,
735
+ mimeType,
736
+ });
737
+ continue;
738
+ }
739
+
740
+ const safeName = sanitizeFileName(path.basename(absolutePath));
741
+ const targetRoot = await ensureUploadRoot();
742
+ const uniqueName = ensureUniqueName(targetRoot, safeName, usedNames);
743
+ const destination = path.join(targetRoot, uniqueName);
744
+
745
+ await fs.copyFile(absolutePath, destination);
746
+
747
+ results.push({
748
+ relativePath: path.relative(workspace.path, destination),
749
+ fileName: uniqueName,
750
+ size: stats.size,
751
+ mimeType,
752
+ });
753
+ }
754
+
755
+ return results;
756
+ });
757
+
627
758
  // Workspace handlers
628
759
  ipcMain.handle(IPC_CHANNELS.WORKSPACE_CREATE, async (_, data) => {
629
760
  const validated = validateInput(WorkspaceCreateSchema, data, 'workspace');
@@ -702,7 +833,7 @@ export async function setupIpcHandlers(
702
833
  try {
703
834
  const activeRoles = agentRoleRepo.findAll(false).filter((role) => role.isActive);
704
835
  const mentionedRoles = extractMentionedRoles(`${title}\n${prompt}`, activeRoles);
705
- const dispatchRoles = mentionedRoles.length > 0 ? mentionedRoles : activeRoles;
836
+ const dispatchRoles = mentionedRoles;
706
837
 
707
838
  if (dispatchRoles.length > 0) {
708
839
  const taskUpdate: Partial<Task> = {
@@ -1040,12 +1171,19 @@ export async function setupIpcHandlers(
1040
1171
  gemini: validated.gemini,
1041
1172
  openrouter: validated.openrouter,
1042
1173
  openai: openaiSettings,
1174
+ groq: validated.groq,
1175
+ xai: validated.xai,
1176
+ kimi: validated.kimi,
1177
+ customProviders: validated.customProviders ?? existingSettings.customProviders,
1043
1178
  // Preserve cached models from existing settings
1044
1179
  cachedGeminiModels: existingSettings.cachedGeminiModels,
1045
1180
  cachedOpenRouterModels: existingSettings.cachedOpenRouterModels,
1046
1181
  cachedOllamaModels: existingSettings.cachedOllamaModels,
1047
1182
  cachedBedrockModels: existingSettings.cachedBedrockModels,
1048
1183
  cachedOpenAIModels: existingSettings.cachedOpenAIModels,
1184
+ cachedGroqModels: existingSettings.cachedGroqModels,
1185
+ cachedXaiModels: existingSettings.cachedXaiModels,
1186
+ cachedKimiModels: existingSettings.cachedKimiModels,
1049
1187
  });
1050
1188
  // Clear cache so next task uses new settings
1051
1189
  LLMProviderFactory.clearCache();
@@ -1062,6 +1200,8 @@ export async function setupIpcHandlers(
1062
1200
  openaiAccessToken = settings.openai?.accessToken;
1063
1201
  openaiRefreshToken = settings.openai?.refreshToken;
1064
1202
  }
1203
+ const resolvedProviderType = resolveCustomProviderId(config.providerType);
1204
+ const customProviderConfig = config.customProviders?.[resolvedProviderType] || config.customProviders?.[config.providerType];
1065
1205
  const providerConfig: LLMProviderConfig = {
1066
1206
  type: config.providerType,
1067
1207
  model: LLMProviderFactory.getModelId(
@@ -1070,7 +1210,11 @@ export async function setupIpcHandlers(
1070
1210
  config.ollama?.model,
1071
1211
  config.gemini?.model,
1072
1212
  config.openrouter?.model,
1073
- config.openai?.model
1213
+ config.openai?.model,
1214
+ config.groq?.model,
1215
+ config.xai?.model,
1216
+ config.kimi?.model,
1217
+ config.customProviders
1074
1218
  ),
1075
1219
  anthropicApiKey: config.anthropic?.apiKey,
1076
1220
  awsRegion: config.bedrock?.region,
@@ -1082,9 +1226,18 @@ export async function setupIpcHandlers(
1082
1226
  ollamaApiKey: config.ollama?.apiKey,
1083
1227
  geminiApiKey: config.gemini?.apiKey,
1084
1228
  openrouterApiKey: config.openrouter?.apiKey,
1229
+ openrouterBaseUrl: config.openrouter?.baseUrl,
1085
1230
  openaiApiKey: config.openai?.apiKey,
1086
1231
  openaiAccessToken: openaiAccessToken,
1087
1232
  openaiRefreshToken: openaiRefreshToken,
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,214 @@ 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
- });
1145
- }
1146
- break;
1147
- }
1148
1314
 
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
- }));
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;
1163
1339
  }
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
- });
1340
+
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;
1171
1365
  }
1172
- break;
1173
- }
1174
1366
 
1175
- case 'ollama': {
1176
- // For Ollama, use the specific model from settings
1177
- currentModel = settings.ollama?.model || 'llama3.2';
1178
- // Use cached models if available, otherwise fall back to static list
1179
- const cachedOllama = LLMProviderFactory.getCachedModels('ollama');
1180
- if (cachedOllama && cachedOllama.length > 0) {
1181
- models = cachedOllama;
1182
- } else {
1183
- // Fall back to static models
1184
- models = Object.entries(OLLAMA_MODELS).map(([key, value]) => ({
1185
- key,
1186
- displayName: value.displayName,
1187
- description: `${value.size} parameter model`,
1188
- }));
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;
1189
1394
  }
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
- });
1395
+
1396
+ case 'groq': {
1397
+ currentModel = settings.groq?.model || 'llama-3.1-8b-instant';
1398
+ const cachedGroq = LLMProviderFactory.getCachedModels('groq');
1399
+ if (cachedGroq && cachedGroq.length > 0) {
1400
+ models = cachedGroq;
1401
+ } else {
1402
+ models = Object.values(GROQ_MODELS).map((value) => ({
1403
+ key: value.id,
1404
+ displayName: value.displayName,
1405
+ description: value.description,
1406
+ }));
1407
+ }
1408
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1409
+ models.unshift({
1410
+ key: currentModel,
1411
+ displayName: currentModel,
1412
+ description: 'Selected model',
1413
+ });
1414
+ }
1415
+ break;
1197
1416
  }
1198
- break;
1199
- }
1200
1417
 
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
- ];
1418
+ case 'xai': {
1419
+ currentModel = settings.xai?.model || 'grok-4-fast-non-reasoning';
1420
+ const cachedXai = LLMProviderFactory.getCachedModels('xai');
1421
+ if (cachedXai && cachedXai.length > 0) {
1422
+ models = cachedXai;
1423
+ } else {
1424
+ models = Object.values(XAI_MODELS).map((value) => ({
1425
+ key: value.id,
1426
+ displayName: value.displayName,
1427
+ description: value.description,
1428
+ }));
1429
+ }
1430
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1431
+ models.unshift({
1432
+ key: currentModel,
1433
+ displayName: currentModel,
1434
+ description: 'Selected model',
1435
+ });
1436
+ }
1437
+ break;
1218
1438
  }
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
- });
1439
+
1440
+ case 'kimi': {
1441
+ currentModel = settings.kimi?.model || 'kimi-k2.5';
1442
+ const cachedKimi = LLMProviderFactory.getCachedModels('kimi');
1443
+ if (cachedKimi && cachedKimi.length > 0) {
1444
+ models = cachedKimi;
1445
+ } else {
1446
+ models = Object.values(KIMI_MODELS).map((value) => ({
1447
+ key: value.id,
1448
+ displayName: value.displayName,
1449
+ description: value.description,
1450
+ }));
1451
+ }
1452
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1453
+ models.unshift({
1454
+ key: currentModel,
1455
+ displayName: currentModel,
1456
+ description: 'Selected model',
1457
+ });
1458
+ }
1459
+ break;
1226
1460
  }
1227
- break;
1228
- }
1229
1461
 
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
- }));
1462
+ default:
1463
+ // Fallback to Anthropic models
1464
+ models = Object.entries(MODELS).map(([key, value]) => ({
1465
+ key,
1466
+ displayName: value.displayName,
1467
+ description: 'Claude model',
1468
+ }));
1469
+ }
1237
1470
  }
1238
1471
 
1239
1472
  return {
@@ -1247,27 +1480,48 @@ export async function setupIpcHandlers(
1247
1480
  // Set the current model (persists selection across sessions)
1248
1481
  ipcMain.handle(IPC_CHANNELS.LLM_SET_MODEL, async (_, modelKey: string) => {
1249
1482
  const settings = LLMProviderFactory.loadSettings();
1483
+ const resolvedProviderType = resolveCustomProviderId(settings.providerType);
1250
1484
 
1251
1485
  // 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;
1486
+ if (CUSTOM_PROVIDER_IDS.has(resolvedProviderType as any)) {
1487
+ const existing = settings.customProviders?.[resolvedProviderType] || {};
1488
+ settings.customProviders = {
1489
+ ...(settings.customProviders || {}),
1490
+ [resolvedProviderType]: {
1491
+ ...existing,
1492
+ model: modelKey,
1493
+ },
1494
+ };
1495
+ } else {
1496
+ switch (settings.providerType) {
1497
+ case 'gemini':
1498
+ settings.gemini = { ...settings.gemini, model: modelKey };
1499
+ break;
1500
+ case 'openrouter':
1501
+ settings.openrouter = { ...settings.openrouter, model: modelKey };
1502
+ break;
1503
+ case 'ollama':
1504
+ settings.ollama = { ...settings.ollama, model: modelKey };
1505
+ break;
1506
+ case 'openai':
1507
+ settings.openai = { ...settings.openai, model: modelKey };
1508
+ break;
1509
+ case 'groq':
1510
+ settings.groq = { ...settings.groq, model: modelKey };
1511
+ break;
1512
+ case 'xai':
1513
+ settings.xai = { ...settings.xai, model: modelKey };
1514
+ break;
1515
+ case 'kimi':
1516
+ settings.kimi = { ...settings.kimi, model: modelKey };
1517
+ break;
1518
+ case 'anthropic':
1519
+ case 'bedrock':
1520
+ default:
1521
+ // For Anthropic/Bedrock, use the modelKey field
1522
+ settings.modelKey = modelKey as ModelKey;
1523
+ break;
1524
+ }
1271
1525
  }
1272
1526
 
1273
1527
  LLMProviderFactory.saveSettings(settings);
@@ -1302,9 +1556,9 @@ export async function setupIpcHandlers(
1302
1556
  return models;
1303
1557
  });
1304
1558
 
1305
- ipcMain.handle(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS, async (_, apiKey?: string) => {
1559
+ ipcMain.handle(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS, async (_, apiKey?: string, baseUrl?: string) => {
1306
1560
  checkRateLimit(IPC_CHANNELS.LLM_GET_OPENROUTER_MODELS);
1307
- const models = await LLMProviderFactory.getOpenRouterModels(apiKey);
1561
+ const models = await LLMProviderFactory.getOpenRouterModels(apiKey, baseUrl);
1308
1562
  // Cache the models for use in config status
1309
1563
  const cachedModels = models.map(m => ({
1310
1564
  key: m.id,
@@ -1329,6 +1583,42 @@ export async function setupIpcHandlers(
1329
1583
  return models;
1330
1584
  });
1331
1585
 
1586
+ ipcMain.handle(IPC_CHANNELS.LLM_GET_GROQ_MODELS, async (_, apiKey?: string, baseUrl?: string) => {
1587
+ checkRateLimit(IPC_CHANNELS.LLM_GET_GROQ_MODELS);
1588
+ const models = await LLMProviderFactory.getGroqModels(apiKey, baseUrl);
1589
+ const cachedModels = models.map(m => ({
1590
+ key: m.id,
1591
+ displayName: m.name,
1592
+ description: 'Groq model',
1593
+ }));
1594
+ LLMProviderFactory.saveCachedModels('groq', cachedModels);
1595
+ return models;
1596
+ });
1597
+
1598
+ ipcMain.handle(IPC_CHANNELS.LLM_GET_XAI_MODELS, async (_, apiKey?: string, baseUrl?: string) => {
1599
+ checkRateLimit(IPC_CHANNELS.LLM_GET_XAI_MODELS);
1600
+ const models = await LLMProviderFactory.getXAIModels(apiKey, baseUrl);
1601
+ const cachedModels = models.map(m => ({
1602
+ key: m.id,
1603
+ displayName: m.name,
1604
+ description: 'xAI model',
1605
+ }));
1606
+ LLMProviderFactory.saveCachedModels('xai', cachedModels);
1607
+ return models;
1608
+ });
1609
+
1610
+ ipcMain.handle(IPC_CHANNELS.LLM_GET_KIMI_MODELS, async (_, apiKey?: string, baseUrl?: string) => {
1611
+ checkRateLimit(IPC_CHANNELS.LLM_GET_KIMI_MODELS);
1612
+ const models = await LLMProviderFactory.getKimiModels(apiKey, baseUrl);
1613
+ const cachedModels = models.map(m => ({
1614
+ key: m.id,
1615
+ displayName: m.name,
1616
+ description: 'Kimi model',
1617
+ }));
1618
+ LLMProviderFactory.saveCachedModels('kimi', cachedModels);
1619
+ return models;
1620
+ });
1621
+
1332
1622
  // OpenAI OAuth handlers
1333
1623
  ipcMain.handle(IPC_CHANNELS.LLM_OPENAI_OAUTH_START, async () => {
1334
1624
  checkRateLimit(IPC_CHANNELS.LLM_OPENAI_OAUTH_START);
@@ -1464,6 +1754,228 @@ export async function setupIpcHandlers(
1464
1754
  };
1465
1755
  });
1466
1756
 
1757
+ // Notion Settings handlers
1758
+ ipcMain.handle(IPC_CHANNELS.NOTION_GET_SETTINGS, async () => {
1759
+ return NotionSettingsManager.loadSettings();
1760
+ });
1761
+
1762
+ ipcMain.handle(IPC_CHANNELS.NOTION_SAVE_SETTINGS, async (_, settings) => {
1763
+ checkRateLimit(IPC_CHANNELS.NOTION_SAVE_SETTINGS);
1764
+ const validated = validateInput(NotionSettingsSchema, settings, 'notion settings') as NotionSettingsData;
1765
+ NotionSettingsManager.saveSettings(validated);
1766
+ NotionSettingsManager.clearCache();
1767
+ return { success: true };
1768
+ });
1769
+
1770
+ ipcMain.handle(IPC_CHANNELS.NOTION_TEST_CONNECTION, async () => {
1771
+ checkRateLimit(IPC_CHANNELS.NOTION_TEST_CONNECTION);
1772
+ const settings = NotionSettingsManager.loadSettings();
1773
+ return testNotionConnection(settings);
1774
+ });
1775
+
1776
+ ipcMain.handle(IPC_CHANNELS.NOTION_GET_STATUS, async () => {
1777
+ checkRateLimit(IPC_CHANNELS.NOTION_GET_STATUS);
1778
+ const settings = NotionSettingsManager.loadSettings();
1779
+ if (!settings.apiKey) {
1780
+ return { configured: false, connected: false };
1781
+ }
1782
+ if (!settings.enabled) {
1783
+ return { configured: true, connected: false };
1784
+ }
1785
+ const result = await testNotionConnection(settings);
1786
+ return {
1787
+ configured: true,
1788
+ connected: result.success,
1789
+ name: result.name,
1790
+ error: result.success ? undefined : result.error,
1791
+ };
1792
+ });
1793
+
1794
+ // Box Settings handlers
1795
+ ipcMain.handle(IPC_CHANNELS.BOX_GET_SETTINGS, async () => {
1796
+ return BoxSettingsManager.loadSettings();
1797
+ });
1798
+
1799
+ ipcMain.handle(IPC_CHANNELS.BOX_SAVE_SETTINGS, async (_, settings) => {
1800
+ checkRateLimit(IPC_CHANNELS.BOX_SAVE_SETTINGS);
1801
+ const validated = validateInput(BoxSettingsSchema, settings, 'box settings') as BoxSettingsData;
1802
+ BoxSettingsManager.saveSettings(validated);
1803
+ BoxSettingsManager.clearCache();
1804
+ return { success: true };
1805
+ });
1806
+
1807
+ ipcMain.handle(IPC_CHANNELS.BOX_TEST_CONNECTION, async () => {
1808
+ checkRateLimit(IPC_CHANNELS.BOX_TEST_CONNECTION);
1809
+ const settings = BoxSettingsManager.loadSettings();
1810
+ return testBoxConnection(settings);
1811
+ });
1812
+
1813
+ ipcMain.handle(IPC_CHANNELS.BOX_GET_STATUS, async () => {
1814
+ checkRateLimit(IPC_CHANNELS.BOX_GET_STATUS);
1815
+ const settings = BoxSettingsManager.loadSettings();
1816
+ if (!settings.accessToken) {
1817
+ return { configured: false, connected: false };
1818
+ }
1819
+ if (!settings.enabled) {
1820
+ return { configured: true, connected: false };
1821
+ }
1822
+ const result = await testBoxConnection(settings);
1823
+ return {
1824
+ configured: true,
1825
+ connected: result.success,
1826
+ name: result.name,
1827
+ error: result.success ? undefined : result.error,
1828
+ };
1829
+ });
1830
+
1831
+ // OneDrive Settings handlers
1832
+ ipcMain.handle(IPC_CHANNELS.ONEDRIVE_GET_SETTINGS, async () => {
1833
+ return OneDriveSettingsManager.loadSettings();
1834
+ });
1835
+
1836
+ ipcMain.handle(IPC_CHANNELS.ONEDRIVE_SAVE_SETTINGS, async (_, settings) => {
1837
+ checkRateLimit(IPC_CHANNELS.ONEDRIVE_SAVE_SETTINGS);
1838
+ const validated = validateInput(OneDriveSettingsSchema, settings, 'onedrive settings') as OneDriveSettingsData;
1839
+ OneDriveSettingsManager.saveSettings(validated);
1840
+ OneDriveSettingsManager.clearCache();
1841
+ return { success: true };
1842
+ });
1843
+
1844
+ ipcMain.handle(IPC_CHANNELS.ONEDRIVE_TEST_CONNECTION, async () => {
1845
+ checkRateLimit(IPC_CHANNELS.ONEDRIVE_TEST_CONNECTION);
1846
+ const settings = OneDriveSettingsManager.loadSettings();
1847
+ return testOneDriveConnection(settings);
1848
+ });
1849
+
1850
+ ipcMain.handle(IPC_CHANNELS.ONEDRIVE_GET_STATUS, async () => {
1851
+ checkRateLimit(IPC_CHANNELS.ONEDRIVE_GET_STATUS);
1852
+ const settings = OneDriveSettingsManager.loadSettings();
1853
+ if (!settings.accessToken) {
1854
+ return { configured: false, connected: false };
1855
+ }
1856
+ if (!settings.enabled) {
1857
+ return { configured: true, connected: false };
1858
+ }
1859
+ const result = await testOneDriveConnection(settings);
1860
+ return {
1861
+ configured: true,
1862
+ connected: result.success,
1863
+ name: result.name,
1864
+ error: result.success ? undefined : result.error,
1865
+ };
1866
+ });
1867
+
1868
+ // Google Drive Settings handlers
1869
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_DRIVE_GET_SETTINGS, async () => {
1870
+ return GoogleDriveSettingsManager.loadSettings();
1871
+ });
1872
+
1873
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_DRIVE_SAVE_SETTINGS, async (_, settings) => {
1874
+ checkRateLimit(IPC_CHANNELS.GOOGLE_DRIVE_SAVE_SETTINGS);
1875
+ const validated = validateInput(GoogleDriveSettingsSchema, settings, 'google drive settings') as GoogleDriveSettingsData;
1876
+ GoogleDriveSettingsManager.saveSettings(validated);
1877
+ GoogleDriveSettingsManager.clearCache();
1878
+ return { success: true };
1879
+ });
1880
+
1881
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_DRIVE_TEST_CONNECTION, async () => {
1882
+ checkRateLimit(IPC_CHANNELS.GOOGLE_DRIVE_TEST_CONNECTION);
1883
+ const settings = GoogleDriveSettingsManager.loadSettings();
1884
+ return testGoogleDriveConnection(settings);
1885
+ });
1886
+
1887
+ ipcMain.handle(IPC_CHANNELS.GOOGLE_DRIVE_GET_STATUS, async () => {
1888
+ checkRateLimit(IPC_CHANNELS.GOOGLE_DRIVE_GET_STATUS);
1889
+ const settings = GoogleDriveSettingsManager.loadSettings();
1890
+ if (!settings.accessToken) {
1891
+ return { configured: false, connected: false };
1892
+ }
1893
+ if (!settings.enabled) {
1894
+ return { configured: true, connected: false };
1895
+ }
1896
+ const result = await testGoogleDriveConnection(settings);
1897
+ return {
1898
+ configured: true,
1899
+ connected: result.success,
1900
+ name: result.name,
1901
+ error: result.success ? undefined : result.error,
1902
+ };
1903
+ });
1904
+
1905
+ // Dropbox Settings handlers
1906
+ ipcMain.handle(IPC_CHANNELS.DROPBOX_GET_SETTINGS, async () => {
1907
+ return DropboxSettingsManager.loadSettings();
1908
+ });
1909
+
1910
+ ipcMain.handle(IPC_CHANNELS.DROPBOX_SAVE_SETTINGS, async (_, settings) => {
1911
+ checkRateLimit(IPC_CHANNELS.DROPBOX_SAVE_SETTINGS);
1912
+ const validated = validateInput(DropboxSettingsSchema, settings, 'dropbox settings') as DropboxSettingsData;
1913
+ DropboxSettingsManager.saveSettings(validated);
1914
+ DropboxSettingsManager.clearCache();
1915
+ return { success: true };
1916
+ });
1917
+
1918
+ ipcMain.handle(IPC_CHANNELS.DROPBOX_TEST_CONNECTION, async () => {
1919
+ checkRateLimit(IPC_CHANNELS.DROPBOX_TEST_CONNECTION);
1920
+ const settings = DropboxSettingsManager.loadSettings();
1921
+ return testDropboxConnection(settings);
1922
+ });
1923
+
1924
+ ipcMain.handle(IPC_CHANNELS.DROPBOX_GET_STATUS, async () => {
1925
+ checkRateLimit(IPC_CHANNELS.DROPBOX_GET_STATUS);
1926
+ const settings = DropboxSettingsManager.loadSettings();
1927
+ if (!settings.accessToken) {
1928
+ return { configured: false, connected: false };
1929
+ }
1930
+ if (!settings.enabled) {
1931
+ return { configured: true, connected: false };
1932
+ }
1933
+ const result = await testDropboxConnection(settings);
1934
+ return {
1935
+ configured: true,
1936
+ connected: result.success,
1937
+ name: result.name,
1938
+ error: result.success ? undefined : result.error,
1939
+ };
1940
+ });
1941
+
1942
+ // SharePoint Settings handlers
1943
+ ipcMain.handle(IPC_CHANNELS.SHAREPOINT_GET_SETTINGS, async () => {
1944
+ return SharePointSettingsManager.loadSettings();
1945
+ });
1946
+
1947
+ ipcMain.handle(IPC_CHANNELS.SHAREPOINT_SAVE_SETTINGS, async (_, settings) => {
1948
+ checkRateLimit(IPC_CHANNELS.SHAREPOINT_SAVE_SETTINGS);
1949
+ const validated = validateInput(SharePointSettingsSchema, settings, 'sharepoint settings') as SharePointSettingsData;
1950
+ SharePointSettingsManager.saveSettings(validated);
1951
+ SharePointSettingsManager.clearCache();
1952
+ return { success: true };
1953
+ });
1954
+
1955
+ ipcMain.handle(IPC_CHANNELS.SHAREPOINT_TEST_CONNECTION, async () => {
1956
+ checkRateLimit(IPC_CHANNELS.SHAREPOINT_TEST_CONNECTION);
1957
+ const settings = SharePointSettingsManager.loadSettings();
1958
+ return testSharePointConnection(settings);
1959
+ });
1960
+
1961
+ ipcMain.handle(IPC_CHANNELS.SHAREPOINT_GET_STATUS, async () => {
1962
+ checkRateLimit(IPC_CHANNELS.SHAREPOINT_GET_STATUS);
1963
+ const settings = SharePointSettingsManager.loadSettings();
1964
+ if (!settings.accessToken) {
1965
+ return { configured: false, connected: false };
1966
+ }
1967
+ if (!settings.enabled) {
1968
+ return { configured: true, connected: false };
1969
+ }
1970
+ const result = await testSharePointConnection(settings);
1971
+ return {
1972
+ configured: true,
1973
+ connected: result.success,
1974
+ name: result.name,
1975
+ error: result.success ? undefined : result.error,
1976
+ };
1977
+ });
1978
+
1467
1979
  // Gateway / Channel handlers
1468
1980
  ipcMain.handle(IPC_CHANNELS.GATEWAY_GET_CHANNELS, async () => {
1469
1981
  if (!gateway) return [];
@@ -2426,6 +2938,7 @@ function setupMCPHandlers(): void {
2426
2938
  rateLimiter.configure(IPC_CHANNELS.MCP_CONNECT_SERVER, RATE_LIMIT_CONFIGS.expensive);
2427
2939
  rateLimiter.configure(IPC_CHANNELS.MCP_TEST_SERVER, RATE_LIMIT_CONFIGS.expensive);
2428
2940
  rateLimiter.configure(IPC_CHANNELS.MCP_REGISTRY_INSTALL, RATE_LIMIT_CONFIGS.expensive);
2941
+ rateLimiter.configure(IPC_CHANNELS.MCP_CONNECTOR_OAUTH_START, RATE_LIMIT_CONFIGS.expensive);
2429
2942
 
2430
2943
  // Initialize MCP settings manager
2431
2944
  MCPSettingsManager.initialize();
@@ -2553,6 +3066,13 @@ function setupMCPHandlers(): void {
2553
3066
  return MCPRegistryManager.updateServer(validatedId);
2554
3067
  });
2555
3068
 
3069
+ // MCP Connector OAuth (Salesforce/Jira)
3070
+ ipcMain.handle(IPC_CHANNELS.MCP_CONNECTOR_OAUTH_START, async (_, payload) => {
3071
+ checkRateLimit(IPC_CHANNELS.MCP_CONNECTOR_OAUTH_START);
3072
+ const validated = validateInput(MCPConnectorOAuthSchema, payload, 'connector oauth');
3073
+ return startConnectorOAuth(validated);
3074
+ });
3075
+
2556
3076
  // MCP Host handlers
2557
3077
  ipcMain.handle(IPC_CHANNELS.MCP_HOST_START, async () => {
2558
3078
  const hostServer = MCPHostServer.getInstance();