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,258 @@
1
+ import { Workspace } from '../../../shared/types';
2
+ import { AgentDaemon } from '../daemon';
3
+ import { GoogleWorkspaceSettingsManager } from '../../settings/google-workspace-manager';
4
+ import { googleCalendarRequest } from '../../utils/google-calendar-api';
5
+
6
+ type CalendarAction =
7
+ | 'list_calendars'
8
+ | 'list_events'
9
+ | 'get_event'
10
+ | 'create_event'
11
+ | 'update_event'
12
+ | 'delete_event';
13
+
14
+ interface GoogleCalendarActionInput {
15
+ action: CalendarAction;
16
+ calendar_id?: string;
17
+ event_id?: string;
18
+ query?: string;
19
+ time_min?: string;
20
+ time_max?: string;
21
+ max_results?: number;
22
+ page_token?: string;
23
+ single_events?: boolean;
24
+ order_by?: 'startTime' | 'updated';
25
+ summary?: string;
26
+ description?: string;
27
+ location?: string;
28
+ start?: string | { dateTime?: string; date?: string; timeZone?: string };
29
+ end?: string | { dateTime?: string; date?: string; timeZone?: string };
30
+ attendees?: Array<string | { email: string }>;
31
+ time_zone?: string;
32
+ payload?: Record<string, any>;
33
+ }
34
+
35
+ function buildAttendees(attendees?: Array<string | { email: string }>): Array<{ email: string }> | undefined {
36
+ if (!attendees || attendees.length === 0) return undefined;
37
+ return attendees.map((attendee) => {
38
+ if (typeof attendee === 'string') {
39
+ return { email: attendee };
40
+ }
41
+ return { email: attendee.email };
42
+ }).filter((attendee) => attendee.email);
43
+ }
44
+
45
+ function buildEventPayload(input: GoogleCalendarActionInput): Record<string, any> {
46
+ if (input.payload) {
47
+ return input.payload;
48
+ }
49
+
50
+ if (!input.summary || !input.start || !input.end) {
51
+ throw new Error('Missing summary/start/end for calendar event');
52
+ }
53
+
54
+ const event: Record<string, any> = {
55
+ summary: input.summary,
56
+ };
57
+
58
+ if (input.description) event.description = input.description;
59
+ if (input.location) event.location = input.location;
60
+
61
+ const timeZone = input.time_zone;
62
+
63
+ const buildDateField = (value: string | { dateTime?: string; date?: string; timeZone?: string }) => {
64
+ if (typeof value === 'string') {
65
+ return {
66
+ dateTime: value,
67
+ timeZone,
68
+ };
69
+ }
70
+ return {
71
+ ...value,
72
+ timeZone: value.timeZone || timeZone,
73
+ };
74
+ };
75
+
76
+ event.start = buildDateField(input.start);
77
+ event.end = buildDateField(input.end);
78
+
79
+ const attendees = buildAttendees(input.attendees);
80
+ if (attendees) {
81
+ event.attendees = attendees;
82
+ }
83
+
84
+ return event;
85
+ }
86
+
87
+ export class GoogleCalendarTools {
88
+ constructor(
89
+ private workspace: Workspace,
90
+ private daemon: AgentDaemon,
91
+ private taskId: string
92
+ ) {}
93
+
94
+ setWorkspace(workspace: Workspace): void {
95
+ this.workspace = workspace;
96
+ }
97
+
98
+ static isEnabled(): boolean {
99
+ return GoogleWorkspaceSettingsManager.loadSettings().enabled;
100
+ }
101
+
102
+ private formatAuthError(error: unknown): string | null {
103
+ const message = String((error as any)?.message ?? '');
104
+ const status = (error as any)?.status;
105
+ if (status === 401) {
106
+ return 'Google Workspace authorization failed (401). Reconnect in Settings > Integrations > Google Workspace.';
107
+ }
108
+ if (/token refresh failed|refresh token not configured|access token not configured|access token expired/i.test(message)) {
109
+ return `Google Workspace authorization error: ${message}`;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ private async requireApproval(summary: string, details: Record<string, unknown>): Promise<void> {
115
+ const approved = await this.daemon.requestApproval(
116
+ this.taskId,
117
+ 'external_service',
118
+ summary,
119
+ details
120
+ );
121
+
122
+ if (!approved) {
123
+ throw new Error('User denied Google Calendar action');
124
+ }
125
+ }
126
+
127
+ async executeAction(input: GoogleCalendarActionInput): Promise<any> {
128
+ const settings = GoogleWorkspaceSettingsManager.loadSettings();
129
+ if (!settings.enabled) {
130
+ throw new Error('Google Workspace integration is disabled. Enable it in Settings > Integrations > Google Workspace.');
131
+ }
132
+
133
+ const action = input.action;
134
+ if (!action) {
135
+ throw new Error('Missing required "action" parameter');
136
+ }
137
+
138
+ const calendarId = input.calendar_id || 'primary';
139
+ let result;
140
+
141
+ try {
142
+ switch (action) {
143
+ case 'list_calendars': {
144
+ result = await googleCalendarRequest(settings, {
145
+ method: 'GET',
146
+ path: '/users/me/calendarList',
147
+ query: {
148
+ maxResults: input.max_results,
149
+ pageToken: input.page_token,
150
+ },
151
+ });
152
+ break;
153
+ }
154
+ case 'list_events': {
155
+ result = await googleCalendarRequest(settings, {
156
+ method: 'GET',
157
+ path: `/calendars/${encodeURIComponent(calendarId)}/events`,
158
+ query: {
159
+ q: input.query,
160
+ timeMin: input.time_min,
161
+ timeMax: input.time_max,
162
+ maxResults: input.max_results,
163
+ pageToken: input.page_token,
164
+ singleEvents: input.single_events ?? true,
165
+ orderBy: input.order_by,
166
+ },
167
+ });
168
+ break;
169
+ }
170
+ case 'get_event': {
171
+ if (!input.event_id) throw new Error('Missing event_id for get_event');
172
+ result = await googleCalendarRequest(settings, {
173
+ method: 'GET',
174
+ path: `/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(input.event_id)}`,
175
+ });
176
+ break;
177
+ }
178
+ case 'create_event': {
179
+ const eventPayload = buildEventPayload(input);
180
+ await this.requireApproval('Create a Google Calendar event', {
181
+ action: 'create_event',
182
+ calendar_id: calendarId,
183
+ summary: eventPayload.summary,
184
+ });
185
+ result = await googleCalendarRequest(settings, {
186
+ method: 'POST',
187
+ path: `/calendars/${encodeURIComponent(calendarId)}/events`,
188
+ body: eventPayload,
189
+ });
190
+ break;
191
+ }
192
+ case 'update_event': {
193
+ if (!input.event_id) throw new Error('Missing event_id for update_event');
194
+ const updatePayload = buildEventPayload(input);
195
+ await this.requireApproval('Update a Google Calendar event', {
196
+ action: 'update_event',
197
+ calendar_id: calendarId,
198
+ event_id: input.event_id,
199
+ summary: updatePayload.summary,
200
+ });
201
+ result = await googleCalendarRequest(settings, {
202
+ method: 'PATCH',
203
+ path: `/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(input.event_id)}`,
204
+ body: updatePayload,
205
+ });
206
+ break;
207
+ }
208
+ case 'delete_event': {
209
+ if (!input.event_id) throw new Error('Missing event_id for delete_event');
210
+ await this.requireApproval('Delete a Google Calendar event', {
211
+ action: 'delete_event',
212
+ calendar_id: calendarId,
213
+ event_id: input.event_id,
214
+ });
215
+ result = await googleCalendarRequest(settings, {
216
+ method: 'DELETE',
217
+ path: `/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(input.event_id)}`,
218
+ });
219
+ break;
220
+ }
221
+ default:
222
+ throw new Error(`Unsupported action: ${action}`);
223
+ }
224
+ } catch (error) {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ const authMessage = this.formatAuthError(error);
227
+ const finalMessage = authMessage ?? message;
228
+ this.daemon.logEvent(this.taskId, 'tool_error', {
229
+ tool: 'calendar_action',
230
+ action,
231
+ message: finalMessage,
232
+ status: (error as any)?.status,
233
+ });
234
+ if (authMessage) {
235
+ throw new Error(authMessage);
236
+ }
237
+ if (error instanceof Error) {
238
+ throw error;
239
+ }
240
+ throw new Error(message);
241
+ }
242
+
243
+ this.daemon.logEvent(this.taskId, 'tool_result', {
244
+ tool: 'calendar_action',
245
+ action,
246
+ status: result?.status,
247
+ hasData: result?.data ? true : false,
248
+ });
249
+
250
+ return {
251
+ success: true,
252
+ action,
253
+ status: result?.status,
254
+ data: result?.data,
255
+ raw: result?.raw,
256
+ };
257
+ }
258
+ }
@@ -0,0 +1,228 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import mime from 'mime-types';
4
+ import { Workspace } from '../../../shared/types';
5
+ import { AgentDaemon } from '../daemon';
6
+ import { GoogleWorkspaceSettingsManager } from '../../settings/google-workspace-manager';
7
+ import { googleDriveRequest, googleDriveUpload } from '../../utils/google-workspace-api';
8
+
9
+ type GoogleDriveAction =
10
+ | 'get_current_user'
11
+ | 'list_files'
12
+ | 'get_file'
13
+ | 'create_folder'
14
+ | 'upload_file'
15
+ | 'delete_file';
16
+
17
+ interface GoogleDriveActionInput {
18
+ action: GoogleDriveAction;
19
+ query?: string;
20
+ page_size?: number;
21
+ page_token?: string;
22
+ fields?: string;
23
+ file_id?: string;
24
+ parent_id?: string;
25
+ name?: string;
26
+ file_path?: string;
27
+ }
28
+
29
+ const DEFAULT_LIST_FIELDS =
30
+ 'nextPageToken, files(id,name,mimeType,modifiedTime,parents,webViewLink,size)';
31
+ const DEFAULT_FILE_FIELDS =
32
+ 'id,name,mimeType,modifiedTime,parents,webViewLink,size';
33
+
34
+ export class GoogleDriveTools {
35
+ constructor(
36
+ private workspace: Workspace,
37
+ private daemon: AgentDaemon,
38
+ private taskId: string
39
+ ) {}
40
+
41
+ setWorkspace(workspace: Workspace): void {
42
+ this.workspace = workspace;
43
+ }
44
+
45
+ static isEnabled(): boolean {
46
+ return GoogleWorkspaceSettingsManager.loadSettings().enabled;
47
+ }
48
+
49
+ private async requireApproval(summary: string, details: Record<string, unknown>): Promise<void> {
50
+ const approved = await this.daemon.requestApproval(
51
+ this.taskId,
52
+ 'external_service',
53
+ summary,
54
+ details
55
+ );
56
+
57
+ if (!approved) {
58
+ throw new Error('User denied Google Drive action');
59
+ }
60
+ }
61
+
62
+ private resolveFilePath(inputPath: string): string {
63
+ if (!this.workspace.permissions.read) {
64
+ throw new Error('Read permission not granted for uploads');
65
+ }
66
+
67
+ const workspaceRoot = path.resolve(this.workspace.path);
68
+ const allowedPaths = this.workspace.permissions.allowedPaths || [];
69
+ const canReadOutside = this.workspace.isTemp || this.workspace.permissions.unrestrictedFileAccess;
70
+
71
+ const isPathAllowed = (absolutePath: string): boolean => {
72
+ if (allowedPaths.length === 0) return false;
73
+ const normalizedPath = path.normalize(absolutePath);
74
+ return allowedPaths.some((allowed) => {
75
+ const normalizedAllowed = path.normalize(allowed);
76
+ return normalizedPath === normalizedAllowed || normalizedPath.startsWith(normalizedAllowed + path.sep);
77
+ });
78
+ };
79
+
80
+ const candidate = path.isAbsolute(inputPath)
81
+ ? path.normalize(inputPath)
82
+ : path.resolve(workspaceRoot, inputPath);
83
+
84
+ const relative = path.relative(workspaceRoot, candidate);
85
+ const isInsideWorkspace = !(relative.startsWith('..') || path.isAbsolute(relative));
86
+ if (!isInsideWorkspace && !canReadOutside && !isPathAllowed(candidate)) {
87
+ throw new Error('File path must be inside the workspace or in Allowed Paths');
88
+ }
89
+ if (!fs.existsSync(candidate)) {
90
+ throw new Error(`File not found: ${inputPath}`);
91
+ }
92
+ const stats = fs.statSync(candidate);
93
+ if (!stats.isFile()) {
94
+ throw new Error(`Path is not a file: ${inputPath}`);
95
+ }
96
+ return candidate;
97
+ }
98
+
99
+ async executeAction(input: GoogleDriveActionInput): Promise<any> {
100
+ const settings = GoogleWorkspaceSettingsManager.loadSettings();
101
+ if (!settings.enabled) {
102
+ throw new Error('Google Workspace integration is disabled. Enable it in Settings > Integrations > Google Workspace.');
103
+ }
104
+
105
+ const action = input.action;
106
+ if (!action) {
107
+ throw new Error('Missing required "action" parameter');
108
+ }
109
+
110
+ let result;
111
+
112
+ switch (action) {
113
+ case 'get_current_user': {
114
+ result = await googleDriveRequest(settings, {
115
+ method: 'GET',
116
+ path: '/about',
117
+ query: { fields: 'user' },
118
+ });
119
+ break;
120
+ }
121
+ case 'list_files': {
122
+ const query = input.query || 'trashed = false';
123
+ result = await googleDriveRequest(settings, {
124
+ method: 'GET',
125
+ path: '/files',
126
+ query: {
127
+ q: query,
128
+ pageSize: input.page_size,
129
+ pageToken: input.page_token,
130
+ fields: input.fields || DEFAULT_LIST_FIELDS,
131
+ },
132
+ });
133
+ break;
134
+ }
135
+ case 'get_file': {
136
+ if (!input.file_id) throw new Error('Missing file_id for get_file');
137
+ result = await googleDriveRequest(settings, {
138
+ method: 'GET',
139
+ path: `/files/${input.file_id}`,
140
+ query: {
141
+ fields: input.fields || DEFAULT_FILE_FIELDS,
142
+ },
143
+ });
144
+ break;
145
+ }
146
+ case 'create_folder': {
147
+ if (!input.name) throw new Error('Missing name for create_folder');
148
+ await this.requireApproval('Create a Google Drive folder', {
149
+ action: 'create_folder',
150
+ parent_id: input.parent_id || 'root',
151
+ name: input.name,
152
+ });
153
+ result = await googleDriveRequest(settings, {
154
+ method: 'POST',
155
+ path: '/files',
156
+ query: { fields: DEFAULT_FILE_FIELDS },
157
+ body: {
158
+ name: input.name,
159
+ mimeType: 'application/vnd.google-apps.folder',
160
+ parents: input.parent_id ? [input.parent_id] : undefined,
161
+ },
162
+ });
163
+ break;
164
+ }
165
+ case 'upload_file': {
166
+ if (!input.file_path) throw new Error('Missing file_path for upload_file');
167
+ const resolved = this.resolveFilePath(input.file_path);
168
+ const data = fs.readFileSync(resolved);
169
+ const fileName = input.name || path.basename(resolved);
170
+ const contentType = (mime.lookup(fileName) || 'application/octet-stream') as string;
171
+ await this.requireApproval(`Upload file to Google Drive: ${fileName}`, {
172
+ action: 'upload_file',
173
+ parent_id: input.parent_id || 'root',
174
+ file: fileName,
175
+ });
176
+ const created = await googleDriveRequest(settings, {
177
+ method: 'POST',
178
+ path: '/files',
179
+ query: { fields: DEFAULT_FILE_FIELDS },
180
+ body: {
181
+ name: fileName,
182
+ parents: input.parent_id ? [input.parent_id] : undefined,
183
+ },
184
+ });
185
+ const fileId = created.data?.id;
186
+ if (!fileId) {
187
+ throw new Error('Failed to create Google Drive file record');
188
+ }
189
+ const uploaded = await googleDriveUpload(settings, fileId, data, contentType);
190
+ result = {
191
+ status: uploaded.status,
192
+ data: uploaded.data || created.data,
193
+ raw: uploaded.raw,
194
+ };
195
+ break;
196
+ }
197
+ case 'delete_file': {
198
+ if (!input.file_id) throw new Error('Missing file_id for delete_file');
199
+ await this.requireApproval('Delete a Google Drive file', {
200
+ action: 'delete_file',
201
+ file_id: input.file_id,
202
+ });
203
+ result = await googleDriveRequest(settings, {
204
+ method: 'DELETE',
205
+ path: `/files/${input.file_id}`,
206
+ });
207
+ break;
208
+ }
209
+ default:
210
+ throw new Error(`Unsupported action: ${action}`);
211
+ }
212
+
213
+ this.daemon.logEvent(this.taskId, 'tool_result', {
214
+ tool: 'google_drive_action',
215
+ action,
216
+ status: result?.status,
217
+ hasData: result?.data ? true : false,
218
+ });
219
+
220
+ return {
221
+ success: true,
222
+ action,
223
+ status: result?.status,
224
+ data: result?.data,
225
+ raw: result?.raw,
226
+ };
227
+ }
228
+ }
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'fs';
2
+ import * as fsPromises from 'fs/promises';
2
3
  import * as path from 'path';
3
4
  import { Workspace } from '../../../shared/types';
4
5
  import { AgentDaemon } from '../daemon';
@@ -32,6 +33,7 @@ export class GrepTools {
32
33
  description:
33
34
  'Powerful regex-based content search across files. ' +
34
35
  'Supports full regex syntax (e.g., "async function.*fetch", "class\\s+\\w+"). ' +
36
+ 'Searches text files only; binary formats like PDF/DOCX are skipped. ' +
35
37
  'Use this to find code patterns, function definitions, imports, etc. ' +
36
38
  'PREFERRED over search_files for content search.',
37
39
  input_schema: {
@@ -99,6 +101,7 @@ export class GrepTools {
99
101
  filesSearched: number;
100
102
  truncated: boolean;
101
103
  error?: string;
104
+ warning?: string;
102
105
  }> {
103
106
  const {
104
107
  pattern,
@@ -115,6 +118,18 @@ export class GrepTools {
115
118
  });
116
119
 
117
120
  try {
121
+ if ((await this.isDocumentHeavyWorkspace()) && (!globPattern || /\.(pdf|docx)\b/i.test(globPattern))) {
122
+ return {
123
+ success: true,
124
+ pattern,
125
+ matches: [],
126
+ totalMatches: 0,
127
+ filesSearched: 0,
128
+ truncated: false,
129
+ warning: 'Workspace appears document-heavy (PDF/DOCX). The grep tool only searches text files. Use read_file for those documents.',
130
+ };
131
+ }
132
+
118
133
  // Compile regex
119
134
  let regex: RegExp;
120
135
  try {
@@ -327,7 +342,8 @@ export class GrepTools {
327
342
 
328
343
  // Apply glob filter if specified
329
344
  if (globRegex) {
330
- if (!globRegex.test(relativePath) && !globRegex.test(entry.name)) {
345
+ const normalizedRelative = relativePath.split(path.sep).join('/');
346
+ if (!globRegex.test(normalizedRelative) && !globRegex.test(entry.name)) {
331
347
  continue;
332
348
  }
333
349
  }
@@ -395,18 +411,8 @@ export class GrepTools {
395
411
  * Convert glob pattern to regex
396
412
  */
397
413
  private globToRegex(pattern: string): RegExp {
398
- // Handle brace expansion
399
414
  const expandedPatterns = this.expandBraces(pattern);
400
-
401
- const regexParts = expandedPatterns.map((p) => {
402
- let regex = p
403
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
404
- .replace(/\\\*\\\*/g, '.*')
405
- .replace(/\\\*/g, '[^/]*')
406
- .replace(/\\\?/g, '[^/]');
407
- return regex;
408
- });
409
-
415
+ const regexParts = expandedPatterns.map((p) => this.globPatternToRegex(p));
410
416
  const combined = regexParts.length > 1 ? `(${regexParts.join('|')})` : regexParts[0];
411
417
  return new RegExp(`^${combined}$`, 'i');
412
418
  }
@@ -429,4 +435,83 @@ export class GrepTools {
429
435
 
430
436
  return results;
431
437
  }
438
+
439
+ /**
440
+ * Heuristic: detect workspaces dominated by PDF/DOCX files
441
+ */
442
+ private async isDocumentHeavyWorkspace(): Promise<boolean> {
443
+ try {
444
+ const entries = await fsPromises.readdir(this.workspace.path, { withFileTypes: true });
445
+ let fileCount = 0;
446
+ let docCount = 0;
447
+ const maxEntries = 200;
448
+
449
+ for (const entry of entries) {
450
+ if (fileCount >= maxEntries) break;
451
+ if (!entry.isFile()) continue;
452
+ fileCount++;
453
+ const ext = path.extname(entry.name).toLowerCase();
454
+ if (ext === '.pdf' || ext === '.docx') {
455
+ docCount++;
456
+ }
457
+ }
458
+
459
+ if (fileCount < 5) return false;
460
+ return docCount / fileCount >= 0.5;
461
+ } catch {
462
+ return false;
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Convert a glob pattern to a regex string (without delimiters)
468
+ */
469
+ private globPatternToRegex(pattern: string): string {
470
+ let regex = '';
471
+ let i = 0;
472
+
473
+ while (i < pattern.length) {
474
+ const char = pattern[i];
475
+
476
+ if (char === '*') {
477
+ const isDoubleStar = pattern[i + 1] === '*';
478
+ if (isDoubleStar) {
479
+ i += 2;
480
+ if (pattern[i] === '/') {
481
+ regex += '(?:.*/)?';
482
+ i += 1;
483
+ } else {
484
+ regex += '.*';
485
+ }
486
+ } else {
487
+ regex += '[^/]*';
488
+ i += 1;
489
+ }
490
+ continue;
491
+ }
492
+
493
+ if (char === '?') {
494
+ regex += '[^/]';
495
+ i += 1;
496
+ continue;
497
+ }
498
+
499
+ if ('+^${}()|[]\\.'.includes(char)) {
500
+ regex += `\\${char}`;
501
+ i += 1;
502
+ continue;
503
+ }
504
+
505
+ if (char === '/') {
506
+ regex += '/';
507
+ i += 1;
508
+ continue;
509
+ }
510
+
511
+ regex += char;
512
+ i += 1;
513
+ }
514
+
515
+ return regex;
516
+ }
432
517
  }
@@ -64,9 +64,19 @@ export class ImageTools {
64
64
  });
65
65
  }
66
66
  } else {
67
- this.daemon.logEvent(this.taskId, 'error', {
67
+ const payload: Record<string, any> = {
68
68
  action: 'generate_image',
69
69
  error: result.error,
70
+ };
71
+ if (result.error?.includes('Gemini API key not configured')) {
72
+ payload.actionHint = {
73
+ type: 'open_settings',
74
+ label: 'Set up Gemini API key',
75
+ target: 'gemini',
76
+ };
77
+ }
78
+ this.daemon.logEvent(this.taskId, 'error', {
79
+ ...payload,
70
80
  });
71
81
  }
72
82