cowork-os 0.3.25 → 0.3.27

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 (39) hide show
  1. package/dist/electron/electron/agent/daemon.js +24 -2
  2. package/dist/electron/electron/agent/executor.js +146 -0
  3. package/dist/electron/electron/agent/tools/builtin-settings.js +2 -0
  4. package/dist/electron/electron/agent/tools/gmail-tools.js +199 -0
  5. package/dist/electron/electron/agent/tools/google-calendar-tools.js +211 -0
  6. package/dist/electron/electron/agent/tools/google-drive-tools.js +12 -12
  7. package/dist/electron/electron/agent/tools/registry.js +214 -0
  8. package/dist/electron/electron/database/repositories.js +36 -2
  9. package/dist/electron/electron/gateway/channels/discord.js +4 -0
  10. package/dist/electron/electron/gateway/channels/google-chat.js +2 -0
  11. package/dist/electron/electron/gateway/channels/line.js +2 -0
  12. package/dist/electron/electron/gateway/channels/matrix-client.js +12 -0
  13. package/dist/electron/electron/gateway/channels/matrix.js +27 -0
  14. package/dist/electron/electron/gateway/channels/mattermost.js +2 -0
  15. package/dist/electron/electron/gateway/channels/signal.js +2 -0
  16. package/dist/electron/electron/gateway/channels/slack.js +9 -4
  17. package/dist/electron/electron/gateway/channels/teams.js +3 -0
  18. package/dist/electron/electron/gateway/channels/telegram.js +2 -0
  19. package/dist/electron/electron/gateway/channels/twitch.js +2 -0
  20. package/dist/electron/electron/gateway/channels/whatsapp.js +3 -0
  21. package/dist/electron/electron/gateway/index.js +37 -0
  22. package/dist/electron/electron/gateway/router.js +7 -1
  23. package/dist/electron/electron/gateway/security.js +22 -9
  24. package/dist/electron/electron/ipc/handlers.js +24 -19
  25. package/dist/electron/electron/preload.js +17 -10
  26. package/dist/electron/electron/settings/{google-drive-manager.js → google-workspace-manager.js} +10 -9
  27. package/dist/electron/electron/utils/gmail-api.js +99 -0
  28. package/dist/electron/electron/utils/google-calendar-api.js +92 -0
  29. package/dist/electron/electron/utils/google-workspace-api.js +196 -0
  30. package/dist/electron/electron/utils/google-workspace-auth.js +91 -0
  31. package/dist/electron/electron/utils/google-workspace-oauth.js +184 -0
  32. package/dist/electron/electron/utils/validation.js +8 -3
  33. package/dist/electron/shared/types.js +9 -4
  34. package/dist/renderer/assets/index-CAE-LL8U.js +3401 -0
  35. package/dist/renderer/assets/index-Cnu5QTSE.css +1 -0
  36. package/dist/renderer/cowork-os-logo.png +0 -0
  37. package/dist/renderer/index.html +13 -0
  38. package/package.json +3 -2
  39. package/dist/electron/electron/utils/google-drive-api.js +0 -152
@@ -182,6 +182,7 @@ class AgentDaemon extends events_1.EventEmitter {
182
182
  error: error.message || 'Failed to initialize task executor',
183
183
  completedAt: Date.now(),
184
184
  });
185
+ this.clearRetryState(task.id);
185
186
  this.logEvent(task.id, 'error', { error: error.message });
186
187
  // Notify queue manager so it can start next task
187
188
  this.queueManager.onTaskFinished(task.id);
@@ -204,6 +205,7 @@ class AgentDaemon extends events_1.EventEmitter {
204
205
  error: error.message,
205
206
  completedAt: Date.now(),
206
207
  });
208
+ this.clearRetryState(task.id);
207
209
  this.logEvent(task.id, 'error', { error: error.message });
208
210
  this.activeTasks.delete(task.id);
209
211
  // Notify queue manager so it can start next task
@@ -383,6 +385,7 @@ class AgentDaemon extends events_1.EventEmitter {
383
385
  // Check if task is queued (not yet started)
384
386
  if (this.queueManager.cancelQueuedTask(taskId)) {
385
387
  this.taskRepo.update(taskId, { status: 'cancelled', completedAt: Date.now() });
388
+ this.clearRetryState(taskId);
386
389
  this.logEvent(taskId, 'task_cancelled', {
387
390
  message: 'Task removed from queue',
388
391
  });
@@ -398,6 +401,7 @@ class AgentDaemon extends events_1.EventEmitter {
398
401
  // (handles orphaned tasks that are in runningTaskIds but have no executor)
399
402
  this.queueManager.onTaskFinished(taskId);
400
403
  // Always emit cancelled event so UI updates
404
+ this.clearRetryState(taskId);
401
405
  this.logEvent(taskId, 'task_cancelled', {
402
406
  message: 'Task was stopped by user',
403
407
  });
@@ -431,10 +435,15 @@ class AgentDaemon extends events_1.EventEmitter {
431
435
  const handle = setTimeout(async () => {
432
436
  this.pendingRetries.delete(taskId);
433
437
  const task = this.taskRepo.findById(taskId);
434
- if (!task)
438
+ if (!task) {
439
+ this.retryCounts.delete(taskId);
435
440
  return;
436
- if (task.status === 'cancelled' || task.status === 'completed')
441
+ }
442
+ if (task.status !== 'queued')
443
+ return;
444
+ if (this.activeTasks.has(taskId) || this.queueManager.isRunning(taskId) || this.queueManager.isQueued(taskId)) {
437
445
  return;
446
+ }
438
447
  await this.startTask(task);
439
448
  }, delayMs);
440
449
  this.pendingRetries.set(taskId, handle);
@@ -904,6 +913,9 @@ class AgentDaemon extends events_1.EventEmitter {
904
913
  */
905
914
  updateTaskStatus(taskId, status) {
906
915
  this.taskRepo.update(taskId, { status });
916
+ if (status === 'completed' || status === 'failed' || status === 'cancelled') {
917
+ this.clearRetryState(taskId);
918
+ }
907
919
  }
908
920
  /**
909
921
  * Get task by ID
@@ -948,6 +960,14 @@ class AgentDaemon extends events_1.EventEmitter {
948
960
  updateTask(taskId, updates) {
949
961
  this.taskRepo.update(taskId, updates);
950
962
  }
963
+ clearRetryState(taskId) {
964
+ const pending = this.pendingRetries.get(taskId);
965
+ if (pending) {
966
+ clearTimeout(pending);
967
+ this.pendingRetries.delete(taskId);
968
+ }
969
+ this.retryCounts.delete(taskId);
970
+ }
951
971
  /**
952
972
  * Mark task as completed
953
973
  * Note: We keep the executor in memory for follow-up messages (with TTL-based cleanup)
@@ -957,6 +977,7 @@ class AgentDaemon extends events_1.EventEmitter {
957
977
  status: 'completed',
958
978
  completedAt: Date.now(),
959
979
  });
980
+ this.clearRetryState(taskId);
960
981
  // Mark executor as completed for TTL-based cleanup
961
982
  const cached = this.activeTasks.get(taskId);
962
983
  if (cached) {
@@ -1076,6 +1097,7 @@ class AgentDaemon extends events_1.EventEmitter {
1076
1097
  status: 'failed',
1077
1098
  error: 'Task timed out - exceeded maximum allowed execution time',
1078
1099
  });
1100
+ this.clearRetryState(taskId);
1079
1101
  // Emit timeout event
1080
1102
  this.logEvent(taskId, 'step_timeout', {
1081
1103
  message: 'Task exceeded maximum execution time and was automatically cancelled',
@@ -949,6 +949,7 @@ class TaskExecutor {
949
949
  this.lastNonVerificationOutput = null;
950
950
  this.toolResultMemoryLimit = 8;
951
951
  this.dispatchedMentionedAgents = false;
952
+ this.lastAssistantText = null;
952
953
  // Plan revision tracking to prevent infinite revision loops
953
954
  this.planRevisionCount = 0;
954
955
  this.maxPlanRevisions = 5;
@@ -1288,6 +1289,69 @@ class TaskExecutor {
1288
1289
  * This auto-fills parameters when the LLM fails to provide them but context is available
1289
1290
  */
1290
1291
  inferMissingParameters(toolName, input) {
1292
+ if (toolName === 'create_document') {
1293
+ let modified = false;
1294
+ let inference = '';
1295
+ input = input || {};
1296
+ if (!input.filename) {
1297
+ if (input.path) {
1298
+ input.filename = path.basename(String(input.path));
1299
+ modified = true;
1300
+ inference = 'Normalized path -> filename';
1301
+ }
1302
+ else if (input.name) {
1303
+ input.filename = String(input.name);
1304
+ modified = true;
1305
+ inference = 'Normalized name -> filename';
1306
+ }
1307
+ }
1308
+ if (!input.format) {
1309
+ const ext = input.filename ? path.extname(String(input.filename)).toLowerCase() : '';
1310
+ if (ext === '.pdf') {
1311
+ input.format = 'pdf';
1312
+ modified = true;
1313
+ inference = `${inference ? `${inference}; ` : ''}Inferred format="pdf" from filename`;
1314
+ }
1315
+ else if (ext === '.docx') {
1316
+ input.format = 'docx';
1317
+ modified = true;
1318
+ inference = `${inference ? `${inference}; ` : ''}Inferred format="docx" from filename`;
1319
+ }
1320
+ else {
1321
+ input.format = 'docx';
1322
+ modified = true;
1323
+ inference = `${inference ? `${inference}; ` : ''}Defaulted format="docx"`;
1324
+ }
1325
+ }
1326
+ if (!input.content) {
1327
+ const fallback = this.getContentFallback();
1328
+ if (fallback) {
1329
+ input.content = fallback;
1330
+ modified = true;
1331
+ inference = `${inference ? `${inference}; ` : ''}Inferred content from latest assistant output`;
1332
+ }
1333
+ }
1334
+ return { input, modified, inference: modified ? inference : undefined };
1335
+ }
1336
+ if (toolName === 'write_file') {
1337
+ let modified = false;
1338
+ let inference = '';
1339
+ input = input || {};
1340
+ if (!input.path && input.filename) {
1341
+ input.path = String(input.filename);
1342
+ modified = true;
1343
+ inference = 'Normalized filename -> path';
1344
+ }
1345
+ if (!input.content) {
1346
+ const fallback = this.getContentFallback();
1347
+ if (fallback) {
1348
+ input.content = fallback;
1349
+ modified = true;
1350
+ inference = `${inference ? `${inference}; ` : ''}Inferred content from latest assistant output`;
1351
+ }
1352
+ }
1353
+ return { input, modified, inference: modified ? inference : undefined };
1354
+ }
1291
1355
  // Handle edit_document - infer sourcePath from recently created documents
1292
1356
  if (toolName === 'edit_document') {
1293
1357
  let modified = false;
@@ -1388,6 +1452,44 @@ class TaskExecutor {
1388
1452
  }
1389
1453
  return { input, modified: false };
1390
1454
  }
1455
+ getContentFallback() {
1456
+ const candidates = [
1457
+ this.lastAssistantText,
1458
+ this.lastNonVerificationOutput,
1459
+ this.lastAssistantOutput,
1460
+ ];
1461
+ const placeholders = new Set([
1462
+ 'I understand. Let me continue.',
1463
+ ]);
1464
+ for (const candidate of candidates) {
1465
+ if (!candidate)
1466
+ continue;
1467
+ const trimmed = candidate.trim();
1468
+ if (trimmed.length < 20)
1469
+ continue;
1470
+ if (placeholders.has(trimmed))
1471
+ continue;
1472
+ return trimmed;
1473
+ }
1474
+ return undefined;
1475
+ }
1476
+ getToolInputValidationError(toolName, input) {
1477
+ if (toolName === 'create_document') {
1478
+ if (!input?.filename)
1479
+ return 'create_document requires a filename';
1480
+ if (!input?.format)
1481
+ return 'create_document requires a format (docx or pdf)';
1482
+ if (!input?.content)
1483
+ return 'create_document requires content';
1484
+ }
1485
+ if (toolName === 'write_file') {
1486
+ if (!input?.path)
1487
+ return 'write_file requires a path';
1488
+ if (!input?.content)
1489
+ return 'write_file requires content';
1490
+ }
1491
+ return null;
1492
+ }
1391
1493
  async handleCanvasPushFallback(content, assistantText) {
1392
1494
  if (content.name !== 'canvas_push') {
1393
1495
  return;
@@ -3241,6 +3343,12 @@ SCHEDULING & REMINDERS:
3241
3343
  .filter((item) => item.type === 'text' && item.text)
3242
3344
  .map((item) => item.text)
3243
3345
  .join('\n');
3346
+ if (assistantText && assistantText.trim().length > 0) {
3347
+ this.lastAssistantText = assistantText.trim();
3348
+ }
3349
+ if (assistantText && assistantText.trim().length > 0) {
3350
+ this.lastAssistantText = assistantText.trim();
3351
+ }
3244
3352
  if (response.content) {
3245
3353
  for (const content of response.content) {
3246
3354
  if (content.type === 'text' && content.text) {
@@ -3350,6 +3458,25 @@ SCHEDULING & REMINDERS:
3350
3458
  }
3351
3459
  // If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
3352
3460
  await this.handleCanvasPushFallback(content, assistantText);
3461
+ const validationError = this.getToolInputValidationError(content.name, content.input);
3462
+ if (validationError) {
3463
+ this.daemon.logEvent(this.task.id, 'tool_warning', {
3464
+ tool: content.name,
3465
+ error: validationError,
3466
+ input: content.input,
3467
+ });
3468
+ toolResults.push({
3469
+ type: 'tool_result',
3470
+ tool_use_id: content.id,
3471
+ content: JSON.stringify({
3472
+ error: validationError,
3473
+ suggestion: 'Include all required fields in the tool call (e.g., content for create_document/write_file).',
3474
+ invalid_input: true,
3475
+ }),
3476
+ is_error: true,
3477
+ });
3478
+ continue;
3479
+ }
3353
3480
  // Check for duplicate tool calls (prevents stuck loops)
3354
3481
  const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
3355
3482
  if (duplicateCheck.isDuplicate) {
@@ -4097,6 +4224,25 @@ SCHEDULING & REMINDERS:
4097
4224
  }
4098
4225
  // If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
4099
4226
  await this.handleCanvasPushFallback(content, assistantText);
4227
+ const validationError = this.getToolInputValidationError(content.name, content.input);
4228
+ if (validationError) {
4229
+ this.daemon.logEvent(this.task.id, 'tool_warning', {
4230
+ tool: content.name,
4231
+ error: validationError,
4232
+ input: content.input,
4233
+ });
4234
+ toolResults.push({
4235
+ type: 'tool_result',
4236
+ tool_use_id: content.id,
4237
+ content: JSON.stringify({
4238
+ error: validationError,
4239
+ suggestion: 'Include all required fields in the tool call (e.g., content for create_document/write_file).',
4240
+ invalid_input: true,
4241
+ }),
4242
+ is_error: true,
4243
+ });
4244
+ continue;
4245
+ }
4100
4246
  // Check for duplicate tool calls (prevents stuck loops)
4101
4247
  const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
4102
4248
  if (duplicateCheck.isDuplicate) {
@@ -113,6 +113,8 @@ const TOOL_CATEGORIES = {
113
113
  box_action: 'webfetch',
114
114
  onedrive_action: 'webfetch',
115
115
  google_drive_action: 'webfetch',
116
+ gmail_action: 'webfetch',
117
+ calendar_action: 'webfetch',
116
118
  dropbox_action: 'webfetch',
117
119
  sharepoint_action: 'webfetch',
118
120
  // Browser tools
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GmailTools = void 0;
4
+ const google_workspace_manager_1 = require("../../settings/google-workspace-manager");
5
+ const gmail_api_1 = require("../../utils/gmail-api");
6
+ function encodeMessage(raw) {
7
+ return Buffer.from(raw)
8
+ .toString('base64')
9
+ .replace(/\+/g, '-')
10
+ .replace(/\//g, '_')
11
+ .replace(/=+$/, '');
12
+ }
13
+ function buildRawEmail(input) {
14
+ const headers = [];
15
+ if (input.to)
16
+ headers.push(`To: ${input.to}`);
17
+ if (input.cc)
18
+ headers.push(`Cc: ${input.cc}`);
19
+ if (input.bcc)
20
+ headers.push(`Bcc: ${input.bcc}`);
21
+ if (input.subject)
22
+ headers.push(`Subject: ${input.subject}`);
23
+ headers.push('MIME-Version: 1.0');
24
+ headers.push('Content-Type: text/plain; charset="UTF-8"');
25
+ headers.push('Content-Transfer-Encoding: 7bit');
26
+ const body = input.body ?? '';
27
+ const message = `${headers.join('\r\n')}\r\n\r\n${body}`;
28
+ return encodeMessage(message);
29
+ }
30
+ class GmailTools {
31
+ constructor(workspace, daemon, taskId) {
32
+ this.workspace = workspace;
33
+ this.daemon = daemon;
34
+ this.taskId = taskId;
35
+ }
36
+ setWorkspace(workspace) {
37
+ this.workspace = workspace;
38
+ }
39
+ static isEnabled() {
40
+ return google_workspace_manager_1.GoogleWorkspaceSettingsManager.loadSettings().enabled;
41
+ }
42
+ formatAuthError(error) {
43
+ const message = String(error?.message ?? '');
44
+ const status = error?.status;
45
+ if (status === 401) {
46
+ return 'Google Workspace authorization failed (401). Reconnect in Settings > Integrations > Google Workspace.';
47
+ }
48
+ if (/token refresh failed|refresh token not configured|access token not configured|access token expired/i.test(message)) {
49
+ return `Google Workspace authorization error: ${message}`;
50
+ }
51
+ return null;
52
+ }
53
+ async requireApproval(summary, details) {
54
+ const approved = await this.daemon.requestApproval(this.taskId, 'external_service', summary, details);
55
+ if (!approved) {
56
+ throw new Error('User denied Gmail action');
57
+ }
58
+ }
59
+ async executeAction(input) {
60
+ const settings = google_workspace_manager_1.GoogleWorkspaceSettingsManager.loadSettings();
61
+ if (!settings.enabled) {
62
+ throw new Error('Google Workspace integration is disabled. Enable it in Settings > Integrations > Google Workspace.');
63
+ }
64
+ const action = input.action;
65
+ if (!action) {
66
+ throw new Error('Missing required "action" parameter');
67
+ }
68
+ let result;
69
+ try {
70
+ switch (action) {
71
+ case 'get_profile': {
72
+ result = await (0, gmail_api_1.gmailRequest)(settings, {
73
+ method: 'GET',
74
+ path: '/users/me/profile',
75
+ });
76
+ break;
77
+ }
78
+ case 'list_messages': {
79
+ result = await (0, gmail_api_1.gmailRequest)(settings, {
80
+ method: 'GET',
81
+ path: '/users/me/messages',
82
+ query: {
83
+ q: input.query,
84
+ maxResults: input.page_size,
85
+ pageToken: input.page_token,
86
+ includeSpamTrash: input.include_spam_trash,
87
+ labelIds: input.label_ids,
88
+ },
89
+ });
90
+ break;
91
+ }
92
+ case 'get_message': {
93
+ if (!input.message_id)
94
+ throw new Error('Missing message_id for get_message');
95
+ result = await (0, gmail_api_1.gmailRequest)(settings, {
96
+ method: 'GET',
97
+ path: `/users/me/messages/${input.message_id}`,
98
+ query: {
99
+ format: input.format,
100
+ metadataHeaders: input.metadata_headers ? input.metadata_headers.join(',') : undefined,
101
+ },
102
+ });
103
+ break;
104
+ }
105
+ case 'get_thread': {
106
+ if (!input.thread_id)
107
+ throw new Error('Missing thread_id for get_thread');
108
+ result = await (0, gmail_api_1.gmailRequest)(settings, {
109
+ method: 'GET',
110
+ path: `/users/me/threads/${input.thread_id}`,
111
+ query: {
112
+ format: input.format,
113
+ metadataHeaders: input.metadata_headers ? input.metadata_headers.join(',') : undefined,
114
+ },
115
+ });
116
+ break;
117
+ }
118
+ case 'list_labels': {
119
+ result = await (0, gmail_api_1.gmailRequest)(settings, {
120
+ method: 'GET',
121
+ path: '/users/me/labels',
122
+ });
123
+ break;
124
+ }
125
+ case 'send_message': {
126
+ if (!input.raw && !input.to) {
127
+ throw new Error('Missing to for send_message');
128
+ }
129
+ if (!input.raw && !input.body && !input.subject) {
130
+ throw new Error('Missing body or subject for send_message');
131
+ }
132
+ await this.requireApproval('Send a Gmail message', {
133
+ action: 'send_message',
134
+ to: input.to,
135
+ subject: input.subject,
136
+ });
137
+ const raw = input.raw || buildRawEmail(input);
138
+ const payload = { raw };
139
+ if (input.thread_id) {
140
+ payload.threadId = input.thread_id;
141
+ }
142
+ result = await (0, gmail_api_1.gmailRequest)(settings, {
143
+ method: 'POST',
144
+ path: '/users/me/messages/send',
145
+ body: payload,
146
+ });
147
+ break;
148
+ }
149
+ case 'trash_message': {
150
+ if (!input.message_id)
151
+ throw new Error('Missing message_id for trash_message');
152
+ await this.requireApproval('Trash a Gmail message', {
153
+ action: 'trash_message',
154
+ message_id: input.message_id,
155
+ });
156
+ result = await (0, gmail_api_1.gmailRequest)(settings, {
157
+ method: 'POST',
158
+ path: `/users/me/messages/${input.message_id}/trash`,
159
+ });
160
+ break;
161
+ }
162
+ default:
163
+ throw new Error(`Unsupported action: ${action}`);
164
+ }
165
+ }
166
+ catch (error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+ const authMessage = this.formatAuthError(error);
169
+ const finalMessage = authMessage ?? message;
170
+ this.daemon.logEvent(this.taskId, 'tool_error', {
171
+ tool: 'gmail_action',
172
+ action,
173
+ message: finalMessage,
174
+ status: error?.status,
175
+ });
176
+ if (authMessage) {
177
+ throw new Error(authMessage);
178
+ }
179
+ if (error instanceof Error) {
180
+ throw error;
181
+ }
182
+ throw new Error(message);
183
+ }
184
+ this.daemon.logEvent(this.taskId, 'tool_result', {
185
+ tool: 'gmail_action',
186
+ action,
187
+ status: result?.status,
188
+ hasData: result?.data ? true : false,
189
+ });
190
+ return {
191
+ success: true,
192
+ action,
193
+ status: result?.status,
194
+ data: result?.data,
195
+ raw: result?.raw,
196
+ };
197
+ }
198
+ }
199
+ exports.GmailTools = GmailTools;