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
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Google Calendar API helpers
3
+ */
4
+
5
+ import { GoogleWorkspaceSettingsData } from '../../shared/types';
6
+ import { getGoogleWorkspaceAccessToken, refreshGoogleWorkspaceAccessToken } from './google-workspace-auth';
7
+
8
+ export const GOOGLE_CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
9
+ const DEFAULT_TIMEOUT_MS = 20000;
10
+
11
+ function parseJsonSafe(text: string): any | undefined {
12
+ const trimmed = text.trim();
13
+ if (!trimmed) return undefined;
14
+ try {
15
+ return JSON.parse(trimmed);
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ function formatCalendarError(status: number, data: any, fallback?: string): string {
22
+ const message =
23
+ data?.error?.message ||
24
+ data?.message ||
25
+ fallback ||
26
+ 'Google Calendar API error';
27
+ return `Google Calendar API error ${status}: ${message}`;
28
+ }
29
+
30
+ export interface GoogleCalendarRequestOptions {
31
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
32
+ path: string;
33
+ query?: Record<string, string | number | boolean | undefined>;
34
+ body?: any;
35
+ timeoutMs?: number;
36
+ }
37
+
38
+ export interface GoogleCalendarRequestResult {
39
+ status: number;
40
+ data?: any;
41
+ raw?: string;
42
+ }
43
+
44
+ export async function googleCalendarRequest(
45
+ settings: GoogleWorkspaceSettingsData,
46
+ options: GoogleCalendarRequestOptions
47
+ ): Promise<GoogleCalendarRequestResult> {
48
+ const params = new URLSearchParams();
49
+ if (options.query) {
50
+ for (const [key, value] of Object.entries(options.query)) {
51
+ if (value === undefined || value === null) continue;
52
+ params.set(key, String(value));
53
+ }
54
+ }
55
+ const queryString = params.toString();
56
+ const url = `${GOOGLE_CALENDAR_API_BASE}${options.path}${queryString ? `?${queryString}` : ''}`;
57
+
58
+ const timeoutMs = options.timeoutMs ?? settings.timeoutMs ?? DEFAULT_TIMEOUT_MS;
59
+
60
+ const requestOnce = async (accessToken: string): Promise<GoogleCalendarRequestResult> => {
61
+ const headers: Record<string, string> = {
62
+ Authorization: `Bearer ${accessToken}`,
63
+ };
64
+
65
+ if (options.method !== 'GET' && options.method !== 'DELETE') {
66
+ headers['Content-Type'] = 'application/json';
67
+ }
68
+
69
+ const controller = new AbortController();
70
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
71
+
72
+ try {
73
+ const response = await fetch(url, {
74
+ method: options.method,
75
+ headers,
76
+ body: options.body ? JSON.stringify(options.body) : undefined,
77
+ signal: controller.signal,
78
+ });
79
+
80
+ const rawText = typeof response.text === 'function' ? await response.text() : '';
81
+ const data = rawText ? parseJsonSafe(rawText) : undefined;
82
+
83
+ if (!response.ok) {
84
+ throw Object.assign(new Error(formatCalendarError(response.status, data, response.statusText)), {
85
+ status: response.status,
86
+ data,
87
+ });
88
+ }
89
+
90
+ return {
91
+ status: response.status,
92
+ data: data ?? undefined,
93
+ raw: rawText || undefined,
94
+ };
95
+ } catch (error: any) {
96
+ if (error?.name === 'AbortError') {
97
+ throw new Error('Google Calendar API request timed out');
98
+ }
99
+ throw error;
100
+ } finally {
101
+ clearTimeout(timeout);
102
+ }
103
+ };
104
+
105
+ try {
106
+ const accessToken = await getGoogleWorkspaceAccessToken(settings);
107
+ return await requestOnce(accessToken);
108
+ } catch (error: any) {
109
+ if (error?.status === 401 && settings.refreshToken) {
110
+ const refreshedToken = await refreshGoogleWorkspaceAccessToken(settings);
111
+ return await requestOnce(refreshedToken);
112
+ }
113
+ throw error;
114
+ }
115
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Google Workspace API helpers (Drive)
3
+ */
4
+
5
+ import { GoogleWorkspaceConnectionTestResult, GoogleWorkspaceSettingsData } from '../../shared/types';
6
+ import { getGoogleWorkspaceAccessToken, refreshGoogleWorkspaceAccessToken } from './google-workspace-auth';
7
+ import { gmailRequest } from './gmail-api';
8
+
9
+ export const GOOGLE_DRIVE_API_BASE = 'https://www.googleapis.com/drive/v3';
10
+ export const GOOGLE_DRIVE_UPLOAD_BASE = 'https://www.googleapis.com/upload/drive/v3';
11
+ const DEFAULT_TIMEOUT_MS = 20000;
12
+
13
+ function parseJsonSafe(text: string): any | undefined {
14
+ const trimmed = text.trim();
15
+ if (!trimmed) return undefined;
16
+ try {
17
+ return JSON.parse(trimmed);
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ function formatDriveError(status: number, data: any, fallback?: string): string {
24
+ const message =
25
+ data?.error?.message ||
26
+ data?.message ||
27
+ fallback ||
28
+ 'Google Drive API error';
29
+ return `Google Drive API error ${status}: ${message}`;
30
+ }
31
+
32
+ export interface GoogleDriveRequestOptions {
33
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
34
+ path: string;
35
+ query?: Record<string, string | number | boolean | undefined>;
36
+ body?: Record<string, any>;
37
+ timeoutMs?: number;
38
+ }
39
+
40
+ export interface GoogleDriveRequestResult {
41
+ status: number;
42
+ data?: any;
43
+ raw?: string;
44
+ }
45
+
46
+ export async function googleDriveRequest(
47
+ settings: GoogleWorkspaceSettingsData,
48
+ options: GoogleDriveRequestOptions
49
+ ): Promise<GoogleDriveRequestResult> {
50
+ const params = new URLSearchParams();
51
+ if (options.query) {
52
+ for (const [key, value] of Object.entries(options.query)) {
53
+ if (value === undefined || value === null) continue;
54
+ params.set(key, String(value));
55
+ }
56
+ }
57
+ const queryString = params.toString();
58
+ const url = `${GOOGLE_DRIVE_API_BASE}${options.path}${queryString ? `?${queryString}` : ''}`;
59
+
60
+ const timeoutMs = options.timeoutMs ?? settings.timeoutMs ?? DEFAULT_TIMEOUT_MS;
61
+
62
+ const requestOnce = async (accessToken: string): Promise<GoogleDriveRequestResult> => {
63
+ const headers: Record<string, string> = {
64
+ Authorization: `Bearer ${accessToken}`,
65
+ };
66
+
67
+ if (options.method !== 'GET' && options.method !== 'DELETE') {
68
+ headers['Content-Type'] = 'application/json';
69
+ }
70
+
71
+ const controller = new AbortController();
72
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
73
+
74
+ try {
75
+ const response = await fetch(url, {
76
+ method: options.method,
77
+ headers,
78
+ body: options.body ? JSON.stringify(options.body) : undefined,
79
+ signal: controller.signal,
80
+ });
81
+
82
+ const rawText = typeof response.text === 'function' ? await response.text() : '';
83
+ const data = rawText ? parseJsonSafe(rawText) : undefined;
84
+
85
+ if (!response.ok) {
86
+ throw Object.assign(new Error(formatDriveError(response.status, data, response.statusText)), {
87
+ status: response.status,
88
+ data,
89
+ });
90
+ }
91
+
92
+ return {
93
+ status: response.status,
94
+ data: data ?? undefined,
95
+ raw: rawText || undefined,
96
+ };
97
+ } catch (error: any) {
98
+ if (error?.name === 'AbortError') {
99
+ throw new Error('Google Drive API request timed out');
100
+ }
101
+ throw error;
102
+ } finally {
103
+ clearTimeout(timeout);
104
+ }
105
+ };
106
+
107
+ try {
108
+ const accessToken = await getGoogleWorkspaceAccessToken(settings);
109
+ return await requestOnce(accessToken);
110
+ } catch (error: any) {
111
+ if (error?.status === 401 && settings.refreshToken) {
112
+ const refreshedToken = await refreshGoogleWorkspaceAccessToken(settings);
113
+ return await requestOnce(refreshedToken);
114
+ }
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ export async function googleDriveUpload(
120
+ settings: GoogleWorkspaceSettingsData,
121
+ fileId: string,
122
+ data: Uint8Array,
123
+ contentType: string
124
+ ): Promise<GoogleDriveRequestResult> {
125
+ const url = `${GOOGLE_DRIVE_UPLOAD_BASE}/files/${fileId}?uploadType=media`;
126
+
127
+ const timeoutMs = settings.timeoutMs ?? DEFAULT_TIMEOUT_MS;
128
+
129
+ const requestOnce = async (accessToken: string): Promise<GoogleDriveRequestResult> => {
130
+ const headers: Record<string, string> = {
131
+ Authorization: `Bearer ${accessToken}`,
132
+ 'Content-Type': contentType,
133
+ };
134
+
135
+ const controller = new AbortController();
136
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
137
+
138
+ try {
139
+ const response = await fetch(url, {
140
+ method: 'PATCH',
141
+ headers,
142
+ body: data,
143
+ signal: controller.signal,
144
+ });
145
+
146
+ const rawText = typeof response.text === 'function' ? await response.text() : '';
147
+ const dataJson = rawText ? parseJsonSafe(rawText) : undefined;
148
+
149
+ if (!response.ok) {
150
+ throw Object.assign(new Error(formatDriveError(response.status, dataJson, response.statusText)), {
151
+ status: response.status,
152
+ data: dataJson,
153
+ });
154
+ }
155
+
156
+ return {
157
+ status: response.status,
158
+ data: dataJson ?? undefined,
159
+ raw: rawText || undefined,
160
+ };
161
+ } catch (error: any) {
162
+ if (error?.name === 'AbortError') {
163
+ throw new Error('Google Drive upload request timed out');
164
+ }
165
+ throw error;
166
+ } finally {
167
+ clearTimeout(timeout);
168
+ }
169
+ };
170
+
171
+ try {
172
+ const accessToken = await getGoogleWorkspaceAccessToken(settings);
173
+ return await requestOnce(accessToken);
174
+ } catch (error: any) {
175
+ if (error?.status === 401 && settings.refreshToken) {
176
+ const refreshedToken = await refreshGoogleWorkspaceAccessToken(settings);
177
+ return await requestOnce(refreshedToken);
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ function extractUserInfo(data: any): { name?: string; userId?: string; email?: string } {
184
+ if (!data || typeof data !== 'object') return {};
185
+ const user = data.user || data;
186
+ const name = user.displayName || user.name || undefined;
187
+ const userId = user.permissionId || user.userId || user.id || undefined;
188
+ const email = user.emailAddress || user.email || undefined;
189
+ return { name, userId, email };
190
+ }
191
+
192
+ export async function testGoogleWorkspaceConnection(settings: GoogleWorkspaceSettingsData): Promise<GoogleWorkspaceConnectionTestResult> {
193
+ try {
194
+ const profile = await gmailRequest(settings, {
195
+ method: 'GET',
196
+ path: '/users/me/profile',
197
+ });
198
+ const email = profile.data?.emailAddress as string | undefined;
199
+ return {
200
+ success: true,
201
+ name: email,
202
+ userId: profile.data?.historyId as string | undefined,
203
+ email,
204
+ };
205
+ } catch {
206
+ // Fall back to Drive if Gmail scope is unavailable
207
+ }
208
+
209
+ try {
210
+ const result = await googleDriveRequest(settings, {
211
+ method: 'GET',
212
+ path: '/about',
213
+ query: { fields: 'user' },
214
+ });
215
+ const extracted = extractUserInfo(result.data);
216
+ return {
217
+ success: true,
218
+ name: extracted.name,
219
+ userId: extracted.userId,
220
+ email: extracted.email,
221
+ };
222
+ } catch (error: any) {
223
+ return {
224
+ success: false,
225
+ error: error?.message || 'Failed to connect to Google Workspace',
226
+ };
227
+ }
228
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Google Workspace OAuth helpers (token refresh)
3
+ */
4
+
5
+ import { GoogleWorkspaceSettingsData } from '../../shared/types';
6
+ import { GoogleWorkspaceSettingsManager } from '../settings/google-workspace-manager';
7
+
8
+ const GOOGLE_OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
9
+ const TOKEN_REFRESH_BUFFER_MS = 2 * 60 * 1000;
10
+
11
+ function parseJsonSafe(text: string): any | undefined {
12
+ const trimmed = text.trim();
13
+ if (!trimmed) return undefined;
14
+ try {
15
+ return JSON.parse(trimmed);
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ function parseScopeList(scope?: string): string[] | undefined {
22
+ if (!scope) return undefined;
23
+ return scope
24
+ .split(/\s+/)
25
+ .map((s) => s.trim())
26
+ .filter(Boolean);
27
+ }
28
+
29
+ export async function refreshGoogleWorkspaceAccessToken(
30
+ settings: GoogleWorkspaceSettingsData
31
+ ): Promise<string> {
32
+ if (!settings.refreshToken) {
33
+ throw new Error('Google Workspace refresh token not configured. Reconnect in Settings > Integrations > Google Workspace.');
34
+ }
35
+ if (!settings.clientId) {
36
+ throw new Error('Google Workspace client ID not configured. Add it in Settings > Integrations > Google Workspace.');
37
+ }
38
+
39
+ const params = new URLSearchParams({
40
+ client_id: settings.clientId,
41
+ grant_type: 'refresh_token',
42
+ refresh_token: settings.refreshToken,
43
+ });
44
+
45
+ if (settings.clientSecret) {
46
+ params.set('client_secret', settings.clientSecret);
47
+ }
48
+
49
+ const response = await fetch(GOOGLE_OAUTH_TOKEN_URL, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
52
+ body: params.toString(),
53
+ });
54
+
55
+ const rawText = typeof response.text === 'function' ? await response.text() : '';
56
+ const data = rawText ? parseJsonSafe(rawText) : undefined;
57
+
58
+ if (!response.ok) {
59
+ const message = data?.error_description || data?.error || response.statusText || 'Token refresh failed';
60
+ throw new Error(`Google Workspace token refresh failed: ${message}`);
61
+ }
62
+
63
+ const accessToken = data?.access_token as string | undefined;
64
+ if (!accessToken) {
65
+ throw new Error('Google Workspace token refresh did not return an access_token');
66
+ }
67
+
68
+ const expiresIn = typeof data?.expires_in === 'number' ? data.expires_in : undefined;
69
+ const nextSettings: GoogleWorkspaceSettingsData = {
70
+ ...settings,
71
+ accessToken,
72
+ tokenExpiresAt: expiresIn ? Date.now() + expiresIn * 1000 : settings.tokenExpiresAt,
73
+ };
74
+
75
+ if (data?.refresh_token) {
76
+ nextSettings.refreshToken = data.refresh_token;
77
+ }
78
+
79
+ const scopes = parseScopeList(data?.scope);
80
+ if (scopes) {
81
+ nextSettings.scopes = scopes;
82
+ }
83
+
84
+ GoogleWorkspaceSettingsManager.saveSettings(nextSettings);
85
+ GoogleWorkspaceSettingsManager.clearCache();
86
+
87
+ return accessToken;
88
+ }
89
+
90
+ export async function getGoogleWorkspaceAccessToken(
91
+ settings: GoogleWorkspaceSettingsData
92
+ ): Promise<string> {
93
+ if (!settings.accessToken && !settings.refreshToken) {
94
+ throw new Error('Google Workspace access token not configured. Connect in Settings > Integrations > Google Workspace.');
95
+ }
96
+
97
+ const now = Date.now();
98
+ if (settings.accessToken) {
99
+ if (!settings.tokenExpiresAt || now < settings.tokenExpiresAt - TOKEN_REFRESH_BUFFER_MS) {
100
+ return settings.accessToken;
101
+ }
102
+ }
103
+
104
+ if (settings.refreshToken) {
105
+ return refreshGoogleWorkspaceAccessToken(settings);
106
+ }
107
+
108
+ throw new Error('Google Workspace access token expired. Reconnect in Settings > Integrations > Google Workspace.');
109
+ }
@@ -0,0 +1,232 @@
1
+ import { shell } from 'electron';
2
+ import http from 'http';
3
+ import { randomBytes, createHash } from 'crypto';
4
+ import { URL } from 'url';
5
+
6
+ export interface GoogleWorkspaceOAuthRequest {
7
+ clientId: string;
8
+ clientSecret?: string;
9
+ scopes?: string[];
10
+ }
11
+
12
+ export interface GoogleWorkspaceOAuthResult {
13
+ accessToken: string;
14
+ refreshToken?: string;
15
+ expiresIn?: number;
16
+ tokenType?: string;
17
+ scopes?: string[];
18
+ }
19
+
20
+ const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
21
+ const OAUTH_CALLBACK_PORT = 18766;
22
+ const GOOGLE_OAUTH_AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
23
+ const GOOGLE_OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
24
+
25
+ function base64Url(buffer: Buffer): string {
26
+ return buffer
27
+ .toString('base64')
28
+ .replace(/\+/g, '-')
29
+ .replace(/\//g, '_')
30
+ .replace(/=+$/, '');
31
+ }
32
+
33
+ function createCodeVerifier(): string {
34
+ return base64Url(randomBytes(32));
35
+ }
36
+
37
+ function createCodeChallenge(verifier: string): string {
38
+ const hash = createHash('sha256').update(verifier).digest();
39
+ return base64Url(hash);
40
+ }
41
+
42
+ function parseJsonSafe(text: string): any | undefined {
43
+ const trimmed = text.trim();
44
+ if (!trimmed) return undefined;
45
+ try {
46
+ return JSON.parse(trimmed);
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+
52
+ function parseScopeList(scope?: string): string[] | undefined {
53
+ if (!scope) return undefined;
54
+ return scope
55
+ .split(/\s+/)
56
+ .map((s) => s.trim())
57
+ .filter(Boolean);
58
+ }
59
+
60
+ async function startOAuthCallbackServer(timeoutMs = DEFAULT_TIMEOUT_MS): Promise<{
61
+ redirectUri: string;
62
+ state: string;
63
+ waitForCode: () => Promise<{ code: string; state: string }>;
64
+ }> {
65
+ const state = base64Url(randomBytes(16));
66
+
67
+ return new Promise((resolve, reject) => {
68
+ const server = http.createServer();
69
+
70
+ let resolveCode: (value: { code: string; state: string }) => void = () => {};
71
+ let rejectCode: (error: Error) => void = () => {};
72
+
73
+ const codePromise = new Promise<{ code: string; state: string }>((innerResolve, innerReject) => {
74
+ resolveCode = innerResolve;
75
+ rejectCode = innerReject;
76
+ });
77
+
78
+ const timeout = setTimeout(() => {
79
+ server.close();
80
+ rejectCode(new Error('OAuth timed out. Please try again.'));
81
+ }, timeoutMs);
82
+
83
+ server.on('request', (req, res) => {
84
+ if (!req.url) {
85
+ res.writeHead(400);
86
+ res.end('Invalid request');
87
+ return;
88
+ }
89
+
90
+ const url = new URL(req.url, 'http://127.0.0.1');
91
+ if (url.pathname !== '/oauth/callback') {
92
+ res.writeHead(404);
93
+ res.end('Not found');
94
+ return;
95
+ }
96
+
97
+ const code = url.searchParams.get('code');
98
+ const returnedState = url.searchParams.get('state');
99
+ const error = url.searchParams.get('error');
100
+ const errorDescription = url.searchParams.get('error_description');
101
+
102
+ res.writeHead(200, { 'Content-Type': 'text/html' });
103
+ res.end(`<!DOCTYPE html><html><body style="font-family: sans-serif; padding: 24px;">
104
+ <h2>Authorization complete</h2>
105
+ <p>You can close this window and return to CoWork OS.</p>
106
+ </body></html>`);
107
+
108
+ clearTimeout(timeout);
109
+ server.close();
110
+
111
+ if (error) {
112
+ rejectCode(new Error(errorDescription || error));
113
+ return;
114
+ }
115
+
116
+ if (!code || !returnedState) {
117
+ rejectCode(new Error('Missing OAuth code or state'));
118
+ return;
119
+ }
120
+
121
+ if (returnedState !== state) {
122
+ rejectCode(new Error('OAuth state mismatch'));
123
+ return;
124
+ }
125
+
126
+ resolveCode({ code, state: returnedState });
127
+ });
128
+
129
+ server.on('error', (error: NodeJS.ErrnoException) => {
130
+ clearTimeout(timeout);
131
+ const portMessage = error.code === 'EADDRINUSE'
132
+ ? `Port ${OAUTH_CALLBACK_PORT} is already in use. Close the conflicting app and try again.`
133
+ : error.message;
134
+ reject(new Error(`OAuth callback server failed: ${portMessage}`));
135
+ });
136
+
137
+ server.listen(OAUTH_CALLBACK_PORT, '127.0.0.1', () => {
138
+ const address = server.address();
139
+ if (!address || typeof address === 'string') {
140
+ clearTimeout(timeout);
141
+ server.close();
142
+ reject(new Error('Failed to start OAuth callback server'));
143
+ return;
144
+ }
145
+
146
+ const redirectUri = `http://127.0.0.1:${address.port}/oauth/callback`;
147
+ resolve({
148
+ redirectUri,
149
+ state,
150
+ waitForCode: () => codePromise,
151
+ });
152
+ });
153
+ });
154
+ }
155
+
156
+ export async function startGoogleWorkspaceOAuth(
157
+ request: GoogleWorkspaceOAuthRequest
158
+ ): Promise<GoogleWorkspaceOAuthResult> {
159
+ if (!request.clientId) {
160
+ throw new Error('Google Workspace OAuth requires a client ID');
161
+ }
162
+
163
+ const scopes = (request.scopes && request.scopes.length > 0)
164
+ ? request.scopes
165
+ : [
166
+ 'https://www.googleapis.com/auth/drive',
167
+ 'https://www.googleapis.com/auth/gmail.readonly',
168
+ 'https://www.googleapis.com/auth/gmail.send',
169
+ 'https://www.googleapis.com/auth/calendar',
170
+ ];
171
+
172
+ const { redirectUri, waitForCode, state } = await startOAuthCallbackServer();
173
+ const codeVerifier = createCodeVerifier();
174
+ const codeChallenge = createCodeChallenge(codeVerifier);
175
+
176
+ const authUrl = new URL(GOOGLE_OAUTH_AUTHORIZE_URL);
177
+ authUrl.searchParams.set('response_type', 'code');
178
+ authUrl.searchParams.set('client_id', request.clientId);
179
+ authUrl.searchParams.set('redirect_uri', redirectUri);
180
+ authUrl.searchParams.set('scope', scopes.join(' '));
181
+ authUrl.searchParams.set('state', state);
182
+ authUrl.searchParams.set('code_challenge', codeChallenge);
183
+ authUrl.searchParams.set('code_challenge_method', 'S256');
184
+ authUrl.searchParams.set('access_type', 'offline');
185
+ authUrl.searchParams.set('prompt', 'consent');
186
+
187
+ await shell.openExternal(authUrl.toString());
188
+
189
+ const { code } = await waitForCode();
190
+
191
+ const params = new URLSearchParams({
192
+ grant_type: 'authorization_code',
193
+ code,
194
+ client_id: request.clientId,
195
+ redirect_uri: redirectUri,
196
+ code_verifier: codeVerifier,
197
+ });
198
+
199
+ if (request.clientSecret) {
200
+ params.set('client_secret', request.clientSecret);
201
+ }
202
+
203
+ const tokenResponse = await fetch(GOOGLE_OAUTH_TOKEN_URL, {
204
+ method: 'POST',
205
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
206
+ body: params.toString(),
207
+ });
208
+
209
+ const rawText = typeof tokenResponse.text === 'function' ? await tokenResponse.text() : '';
210
+ const tokenData = rawText ? parseJsonSafe(rawText) : undefined;
211
+
212
+ if (!tokenResponse.ok) {
213
+ const message = tokenData?.error_description || tokenData?.error || tokenResponse.statusText || 'OAuth failed';
214
+ throw new Error(`Google Workspace OAuth failed: ${message}`);
215
+ }
216
+
217
+ const accessToken = tokenData?.access_token as string | undefined;
218
+ if (!accessToken) {
219
+ throw new Error('Google Workspace OAuth did not return an access_token');
220
+ }
221
+
222
+ const expiresIn = typeof tokenData?.expires_in === 'number' ? tokenData.expires_in : undefined;
223
+ const scopesGranted = parseScopeList(tokenData?.scope);
224
+
225
+ return {
226
+ accessToken,
227
+ refreshToken: tokenData?.refresh_token,
228
+ expiresIn,
229
+ tokenType: tokenData?.token_type,
230
+ scopes: scopesGranted,
231
+ };
232
+ }