cowork-os 0.3.21 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +293 -6
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/daemon.js +25 -0
  48. package/dist/electron/electron/agent/executor.js +181 -26
  49. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  50. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  51. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  52. package/dist/electron/electron/agent/llm/index.js +11 -1
  53. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  54. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  55. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  56. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  57. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  58. package/dist/electron/electron/agent/llm/provider-factory.js +318 -4
  59. package/dist/electron/electron/agent/llm/types.js +66 -1
  60. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  61. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  62. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  63. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  64. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  65. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  66. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  67. package/dist/electron/electron/agent/tools/registry.js +541 -0
  68. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  69. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  70. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  71. package/dist/electron/electron/gateway/index.js +1 -0
  72. package/dist/electron/electron/gateway/router.js +123 -143
  73. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  74. package/dist/electron/electron/ipc/handlers.js +627 -158
  75. package/dist/electron/electron/main.js +63 -0
  76. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  77. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  78. package/dist/electron/electron/memory/MemoryService.js +1 -1
  79. package/dist/electron/electron/preload.js +74 -1
  80. package/dist/electron/electron/settings/box-manager.js +54 -0
  81. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  82. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  83. package/dist/electron/electron/settings/notion-manager.js +56 -0
  84. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  85. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  86. package/dist/electron/electron/utils/box-api.js +153 -0
  87. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  88. package/dist/electron/electron/utils/env-migration.js +19 -0
  89. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  90. package/dist/electron/electron/utils/notion-api.js +103 -0
  91. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  92. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  93. package/dist/electron/electron/utils/validation.js +82 -3
  94. package/dist/electron/electron/utils/x-cli.js +1 -1
  95. package/dist/electron/shared/channelMessages.js +284 -3
  96. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  97. package/dist/electron/shared/types.js +88 -1
  98. package/package.json +12 -2
  99. package/src/electron/agent/executor.ts +205 -28
  100. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  101. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  102. package/src/electron/agent/llm/groq-provider.ts +39 -0
  103. package/src/electron/agent/llm/index.ts +5 -0
  104. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  105. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  106. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  107. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  108. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  109. package/src/electron/agent/llm/provider-factory.ts +414 -6
  110. package/src/electron/agent/llm/types.ts +90 -1
  111. package/src/electron/agent/llm/xai-provider.ts +39 -0
  112. package/src/electron/agent/tools/box-tools.ts +239 -0
  113. package/src/electron/agent/tools/builtin-settings.ts +34 -0
  114. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  115. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  116. package/src/electron/agent/tools/notion-tools.ts +330 -0
  117. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  118. package/src/electron/agent/tools/registry.ts +565 -0
  119. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  120. package/src/electron/agent/tools/shell-tools.ts +11 -3
  121. package/src/electron/agent/tools/x-tools.ts +1 -1
  122. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  123. package/src/electron/gateway/index.ts +1 -0
  124. package/src/electron/gateway/router.ts +134 -149
  125. package/src/electron/ipc/canvas-handlers.ts +10 -0
  126. package/src/electron/ipc/handlers.ts +673 -153
  127. package/src/electron/main.ts +35 -0
  128. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  129. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  130. package/src/electron/memory/MemoryService.ts +5 -1
  131. package/src/electron/preload.ts +167 -4
  132. package/src/electron/settings/box-manager.ts +58 -0
  133. package/src/electron/settings/dropbox-manager.ts +58 -0
  134. package/src/electron/settings/google-drive-manager.ts +58 -0
  135. package/src/electron/settings/notion-manager.ts +60 -0
  136. package/src/electron/settings/onedrive-manager.ts +58 -0
  137. package/src/electron/settings/sharepoint-manager.ts +58 -0
  138. package/src/electron/utils/box-api.ts +184 -0
  139. package/src/electron/utils/dropbox-api.ts +171 -0
  140. package/src/electron/utils/env-migration.ts +22 -0
  141. package/src/electron/utils/google-drive-api.ts +183 -0
  142. package/src/electron/utils/notion-api.ts +126 -0
  143. package/src/electron/utils/onedrive-api.ts +137 -0
  144. package/src/electron/utils/sharepoint-api.ts +132 -0
  145. package/src/electron/utils/validation.ts +102 -1
  146. package/src/electron/utils/x-cli.ts +1 -1
  147. package/src/renderer/App.tsx +20 -2
  148. package/src/renderer/components/BoxSettings.tsx +203 -0
  149. package/src/renderer/components/BrowserView.tsx +101 -0
  150. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  151. package/src/renderer/components/CanvasPreview.tsx +68 -1
  152. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  153. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  154. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  155. package/src/renderer/components/DropboxSettings.tsx +202 -0
  156. package/src/renderer/components/GoogleDriveSettings.tsx +201 -0
  157. package/src/renderer/components/MCPSettings.tsx +56 -0
  158. package/src/renderer/components/MainContent.tsx +270 -34
  159. package/src/renderer/components/NotionSettings.tsx +231 -0
  160. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  161. package/src/renderer/components/OnboardingModal.tsx +70 -1
  162. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  163. package/src/renderer/components/Settings.tsx +611 -8
  164. package/src/renderer/components/SharePointSettings.tsx +224 -0
  165. package/src/renderer/components/Sidebar.tsx +25 -9
  166. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  167. package/src/renderer/styles/index.css +438 -25
  168. package/src/shared/channelMessages.ts +367 -4
  169. package/src/shared/llm-provider-catalog.ts +217 -0
  170. package/src/shared/types.ts +226 -1
@@ -9,6 +9,7 @@ import {
9
9
  LLMProviderFactory,
10
10
  LLMMessage,
11
11
  LLMToolResult,
12
+ LLMToolUse,
12
13
  } from './llm';
13
14
  import {
14
15
  ContextManager,
@@ -21,6 +22,7 @@ import { calculateCost, formatCost } from './llm/pricing';
21
22
  import { getCustomSkillLoader } from './custom-skill-loader';
22
23
  import { MemoryService } from '../memory/MemoryService';
23
24
  import { InputSanitizer, OutputFilter } from './security';
25
+ import { BuiltinToolsSettingsManager } from './tools/builtin-settings';
24
26
 
25
27
  class AwaitingUserInputError extends Error {
26
28
  constructor(message: string) {
@@ -35,7 +37,7 @@ const LLM_TIMEOUT_MS = 2 * 60 * 1000;
35
37
  // Per-step timeout (5 minutes max per step)
36
38
  const STEP_TIMEOUT_MS = 5 * 60 * 1000;
37
39
 
38
- // Per-tool execution timeout (45 seconds - balance responsiveness with heavier tools)
40
+ // Default per-tool execution timeout (overrideable per tool)
39
41
  const TOOL_TIMEOUT_MS = 30 * 1000;
40
42
 
41
43
  // Maximum consecutive failures for the same tool before giving up
@@ -1014,6 +1016,7 @@ export class TaskExecutor {
1014
1016
  private modelKey: string;
1015
1017
  private conversationHistory: LLMMessage[] = [];
1016
1018
  private systemPrompt: string = '';
1019
+ private lastUserMessage: string;
1017
1020
 
1018
1021
  // Plan revision tracking to prevent infinite revision loops
1019
1022
  private planRevisionCount: number = 0;
@@ -1040,6 +1043,7 @@ export class TaskExecutor {
1040
1043
  private workspace: Workspace,
1041
1044
  private daemon: AgentDaemon
1042
1045
  ) {
1046
+ this.lastUserMessage = task.prompt;
1043
1047
  this.requiresTestRun = this.detectTestRequirement(`${task.title}\n${task.prompt}`);
1044
1048
  // Get base settings
1045
1049
  const settings = LLMProviderFactory.loadSettings();
@@ -1062,7 +1066,11 @@ export class TaskExecutor {
1062
1066
  settings.ollama?.model,
1063
1067
  settings.gemini?.model,
1064
1068
  settings.openrouter?.model,
1065
- settings.openai?.model
1069
+ settings.openai?.model,
1070
+ settings.groq?.model,
1071
+ settings.xai?.model,
1072
+ settings.kimi?.model,
1073
+ settings.customProviders
1066
1074
  );
1067
1075
  this.modelKey = effectiveModelKey;
1068
1076
 
@@ -1220,6 +1228,23 @@ export class TaskExecutor {
1220
1228
  this.globalTurnCount++; // Track global turns across all steps
1221
1229
  }
1222
1230
 
1231
+ private getToolTimeoutMs(toolName: string, input: unknown): number {
1232
+ const settingsTimeout = BuiltinToolsSettingsManager.getToolTimeoutMs(toolName);
1233
+ const normalizedSettingsTimeout = settingsTimeout && settingsTimeout > 0 ? settingsTimeout : null;
1234
+
1235
+ if (toolName === 'run_command') {
1236
+ const inputTimeout = typeof (input as { timeout?: unknown })?.timeout === 'number'
1237
+ ? (input as { timeout?: number }).timeout
1238
+ : undefined;
1239
+ if (typeof inputTimeout === 'number' && Number.isFinite(inputTimeout) && inputTimeout > 0) {
1240
+ return Math.round(inputTimeout);
1241
+ }
1242
+ return normalizedSettingsTimeout ?? TOOL_TIMEOUT_MS;
1243
+ }
1244
+
1245
+ return normalizedSettingsTimeout ?? TOOL_TIMEOUT_MS;
1246
+ }
1247
+
1223
1248
  /**
1224
1249
  * Check if a file operation should be blocked (redundant read or duplicate creation)
1225
1250
  * @returns Object with blocked flag, reason, and suggestion if blocked, plus optional cached result
@@ -1461,6 +1486,37 @@ export class TaskExecutor {
1461
1486
  return { input, modified: false };
1462
1487
  }
1463
1488
 
1489
+ private async handleCanvasPushFallback(content: LLMToolUse, assistantText: string): Promise<void> {
1490
+ if (content.name !== 'canvas_push') {
1491
+ return;
1492
+ }
1493
+
1494
+ const inputContent = content.input?.content;
1495
+ const hasContent = typeof inputContent === 'string' && inputContent.trim().length > 0;
1496
+ const filename = content.input?.filename;
1497
+ const isHtmlTarget = !filename || filename === 'index.html';
1498
+ if (hasContent || !isHtmlTarget) {
1499
+ return;
1500
+ }
1501
+
1502
+ const extracted = this.extractHtmlFromText(assistantText);
1503
+ const generated = extracted || await this.generateCanvasHtml(this.lastUserMessage || this.task.prompt);
1504
+ if (!generated) {
1505
+ return;
1506
+ }
1507
+
1508
+ content.input = {
1509
+ ...(content.input || {}),
1510
+ content: generated,
1511
+ };
1512
+ this.daemon.logEvent(this.task.id, 'parameter_inference', {
1513
+ tool: content.name,
1514
+ inference: extracted
1515
+ ? 'Recovered HTML from assistant text'
1516
+ : 'Auto-generated HTML from latest user request',
1517
+ });
1518
+ }
1519
+
1464
1520
  /**
1465
1521
  * Get available tools, filtering out disabled ones
1466
1522
  * This prevents the LLM from trying to use tools that have been disabled by the circuit breaker
@@ -2954,6 +3010,8 @@ SCHEDULING & REMINDERS:
2954
3010
  // Compact messages if context is getting too large
2955
3011
  messages = this.contextManager.compactMessages(messages, systemPromptTokens);
2956
3012
 
3013
+ const availableTools = this.getAvailableTools();
3014
+
2957
3015
  // Use retry wrapper for resilient API calls
2958
3016
  const response = await this.callLLMWithRetry(
2959
3017
  () => withTimeout(
@@ -2961,7 +3019,7 @@ SCHEDULING & REMINDERS:
2961
3019
  model: this.modelId,
2962
3020
  maxTokens: 4096,
2963
3021
  system: this.systemPrompt,
2964
- tools: this.getAvailableTools(),
3022
+ tools: availableTools,
2965
3023
  messages,
2966
3024
  signal: this.abortController.signal,
2967
3025
  }),
@@ -2984,6 +3042,10 @@ SCHEDULING & REMINDERS:
2984
3042
 
2985
3043
  // Log any text responses from the assistant and check if asking a question
2986
3044
  let assistantAskedQuestion = false;
3045
+ const assistantText = (response.content || [])
3046
+ .filter((item: any) => item.type === 'text' && item.text)
3047
+ .map((item: any) => item.text)
3048
+ .join('\n');
2987
3049
  if (response.content) {
2988
3050
  for (const content of response.content) {
2989
3051
  if (content.type === 'text' && content.text) {
@@ -3031,6 +3093,8 @@ SCHEDULING & REMINDERS:
3031
3093
  const toolResults: LLMToolResult[] = [];
3032
3094
  let hasDisabledToolAttempt = false;
3033
3095
  let hasDuplicateToolAttempt = false;
3096
+ let hasUnavailableToolAttempt = false;
3097
+ const availableToolNames = new Set(availableTools.map(tool => tool.name));
3034
3098
 
3035
3099
  for (const content of response.content || []) {
3036
3100
  if (content.type === 'tool_use') {
@@ -3056,6 +3120,40 @@ SCHEDULING & REMINDERS:
3056
3120
  continue;
3057
3121
  }
3058
3122
 
3123
+ // Validate tool availability before attempting any inference
3124
+ if (!availableToolNames.has(content.name)) {
3125
+ console.log(`[TaskExecutor] Tool not available in this context: ${content.name}`);
3126
+ this.daemon.logEvent(this.task.id, 'tool_error', {
3127
+ tool: content.name,
3128
+ error: 'Tool not available in current context or permissions',
3129
+ blocked: true,
3130
+ });
3131
+ toolResults.push({
3132
+ type: 'tool_result',
3133
+ tool_use_id: content.id,
3134
+ content: JSON.stringify({
3135
+ error: `Tool "${content.name}" is not available in this context. Please choose a different tool or check permissions/integrations.`,
3136
+ unavailable: true,
3137
+ }),
3138
+ is_error: true,
3139
+ });
3140
+ hasUnavailableToolAttempt = true;
3141
+ continue;
3142
+ }
3143
+
3144
+ // Infer missing parameters for weaker models (normalize inputs before deduplication)
3145
+ const inference = this.inferMissingParameters(content.name, content.input);
3146
+ if (inference.modified) {
3147
+ content.input = inference.input;
3148
+ this.daemon.logEvent(this.task.id, 'parameter_inference', {
3149
+ tool: content.name,
3150
+ inference: inference.inference,
3151
+ });
3152
+ }
3153
+
3154
+ // If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
3155
+ await this.handleCanvasPushFallback(content, assistantText);
3156
+
3059
3157
  // Check for duplicate tool calls (prevents stuck loops)
3060
3158
  const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
3061
3159
  if (duplicateCheck.isDuplicate) {
@@ -3129,16 +3227,6 @@ SCHEDULING & REMINDERS:
3129
3227
  continue;
3130
3228
  }
3131
3229
 
3132
- // Infer missing parameters for weaker models
3133
- const inference = this.inferMissingParameters(content.name, content.input);
3134
- if (inference.modified) {
3135
- content.input = inference.input;
3136
- this.daemon.logEvent(this.task.id, 'parameter_inference', {
3137
- tool: content.name,
3138
- inference: inference.inference,
3139
- });
3140
- }
3141
-
3142
3230
  this.daemon.logEvent(this.task.id, 'tool_call', {
3143
3231
  tool: content.name,
3144
3232
  input: content.input,
@@ -3146,12 +3234,13 @@ SCHEDULING & REMINDERS:
3146
3234
 
3147
3235
  try {
3148
3236
  // Execute tool with timeout to prevent hanging
3237
+ const toolTimeoutMs = this.getToolTimeoutMs(content.name, content.input);
3149
3238
  const result = await withTimeout(
3150
3239
  this.toolRegistry.executeTool(
3151
3240
  content.name,
3152
3241
  content.input as any
3153
3242
  ),
3154
- TOOL_TIMEOUT_MS,
3243
+ toolTimeoutMs,
3155
3244
  `Tool ${content.name}`
3156
3245
  );
3157
3246
 
@@ -3266,7 +3355,7 @@ SCHEDULING & REMINDERS:
3266
3355
  // If all tool attempts were for disabled or duplicate tools, don't continue looping
3267
3356
  // This prevents infinite retry loops
3268
3357
  const allToolsFailed = toolResults.every(r => r.is_error);
3269
- if ((hasDisabledToolAttempt || hasDuplicateToolAttempt) && allToolsFailed) {
3358
+ if ((hasDisabledToolAttempt || hasDuplicateToolAttempt || hasUnavailableToolAttempt) && allToolsFailed) {
3270
3359
  console.log('[TaskExecutor] All tool calls failed, were disabled, or duplicates - stopping iteration');
3271
3360
  if (hasDuplicateToolAttempt) {
3272
3361
  // Duplicate detection triggered - step is likely complete
@@ -3383,6 +3472,60 @@ SCHEDULING & REMINDERS:
3383
3472
  }
3384
3473
  }
3385
3474
 
3475
+ private extractHtmlFromText(text: string): string | null {
3476
+ if (!text) return null;
3477
+ const fenceMatch = text.match(/```html([\s\S]*?)```/i);
3478
+ const raw = fenceMatch ? fenceMatch[1].trim() : text;
3479
+ const doctypeIndex = raw.indexOf('<!DOCTYPE html');
3480
+ if (doctypeIndex >= 0) {
3481
+ const endIndex = raw.lastIndexOf('</html>');
3482
+ if (endIndex > doctypeIndex) {
3483
+ return raw.slice(doctypeIndex, endIndex + '</html>'.length).trim();
3484
+ }
3485
+ }
3486
+ const htmlIndex = raw.indexOf('<html');
3487
+ if (htmlIndex >= 0) {
3488
+ const endIndex = raw.lastIndexOf('</html>');
3489
+ if (endIndex > htmlIndex) {
3490
+ return raw.slice(htmlIndex, endIndex + '</html>'.length).trim();
3491
+ }
3492
+ }
3493
+ return null;
3494
+ }
3495
+
3496
+ private async generateCanvasHtml(prompt: string): Promise<string | null> {
3497
+ const system = [
3498
+ 'You generate a single self-contained HTML document for an in-app canvas.',
3499
+ 'Output ONLY the HTML document (no markdown, no commentary).',
3500
+ 'Use inline CSS and JS. Do not reference external assets or remote URLs.',
3501
+ 'Keep it reasonably compact and interactive where appropriate.',
3502
+ ].join(' ');
3503
+
3504
+ try {
3505
+ const response = await this.provider.createMessage({
3506
+ model: this.modelId,
3507
+ maxTokens: 1800,
3508
+ system,
3509
+ messages: [
3510
+ {
3511
+ role: 'user',
3512
+ content: `Build an interactive HTML demo for this request:\n${prompt}`,
3513
+ },
3514
+ ],
3515
+ });
3516
+
3517
+ const text = (response.content || [])
3518
+ .filter((c) => c.type === 'text')
3519
+ .map((c) => c.text)
3520
+ .join('\n');
3521
+
3522
+ return this.extractHtmlFromText(text);
3523
+ } catch (error) {
3524
+ console.error('[TaskExecutor] Failed to auto-generate canvas HTML:', error);
3525
+ return null;
3526
+ }
3527
+ }
3528
+
3386
3529
  /**
3387
3530
  * Send a follow-up message to continue the conversation
3388
3531
  */
@@ -3393,6 +3536,7 @@ SCHEDULING & REMINDERS:
3393
3536
  let resumeAttempted = false;
3394
3537
  this.waitingForUserInput = false;
3395
3538
  this.paused = false;
3539
+ this.lastUserMessage = message;
3396
3540
  this.toolRegistry.setCanvasSessionCutoff(shouldStartNewCanvasSession ? Date.now() : null);
3397
3541
  this.daemon.updateTaskStatus(this.task.id, 'executing');
3398
3542
  this.daemon.logEvent(this.task.id, 'executing', { message: 'Processing follow-up message' });
@@ -3602,6 +3746,9 @@ SCHEDULING & REMINDERS:
3602
3746
  // Compact messages if context is getting too large
3603
3747
  messages = this.contextManager.compactMessages(messages, systemPromptTokens);
3604
3748
 
3749
+ const availableTools = this.getAvailableTools();
3750
+ const availableToolNames = new Set(availableTools.map(tool => tool.name));
3751
+
3605
3752
  // Use retry wrapper for resilient API calls
3606
3753
  const response = await this.callLLMWithRetry(
3607
3754
  () => withTimeout(
@@ -3609,7 +3756,7 @@ SCHEDULING & REMINDERS:
3609
3756
  model: this.modelId,
3610
3757
  maxTokens: 4096,
3611
3758
  system: this.systemPrompt,
3612
- tools: this.getAvailableTools(),
3759
+ tools: availableTools,
3613
3760
  messages,
3614
3761
  signal: this.abortController.signal,
3615
3762
  }),
@@ -3630,6 +3777,10 @@ SCHEDULING & REMINDERS:
3630
3777
  // Log any text responses from the assistant and check if asking a question
3631
3778
  let assistantAskedQuestion = false;
3632
3779
  let hasTextInThisResponse = false;
3780
+ const assistantText = (response.content || [])
3781
+ .filter((item: any) => item.type === 'text' && item.text)
3782
+ .map((item: any) => item.text)
3783
+ .join('\n');
3633
3784
  if (response.content) {
3634
3785
  for (const content of response.content) {
3635
3786
  if (content.type === 'text' && content.text && content.text.trim().length > 0) {
@@ -3679,6 +3830,7 @@ SCHEDULING & REMINDERS:
3679
3830
  const toolResults: LLMToolResult[] = [];
3680
3831
  let hasDisabledToolAttempt = false;
3681
3832
  let hasDuplicateToolAttempt = false;
3833
+ let hasUnavailableToolAttempt = false;
3682
3834
 
3683
3835
  for (const content of response.content || []) {
3684
3836
  if (content.type === 'tool_use') {
@@ -3704,6 +3856,40 @@ SCHEDULING & REMINDERS:
3704
3856
  continue;
3705
3857
  }
3706
3858
 
3859
+ // Validate tool availability before attempting any inference
3860
+ if (!availableToolNames.has(content.name)) {
3861
+ console.log(`[TaskExecutor] Tool not available in this context: ${content.name}`);
3862
+ this.daemon.logEvent(this.task.id, 'tool_error', {
3863
+ tool: content.name,
3864
+ error: 'Tool not available in current context or permissions',
3865
+ blocked: true,
3866
+ });
3867
+ toolResults.push({
3868
+ type: 'tool_result',
3869
+ tool_use_id: content.id,
3870
+ content: JSON.stringify({
3871
+ error: `Tool "${content.name}" is not available in this context. Please choose a different tool or check permissions/integrations.`,
3872
+ unavailable: true,
3873
+ }),
3874
+ is_error: true,
3875
+ });
3876
+ hasUnavailableToolAttempt = true;
3877
+ continue;
3878
+ }
3879
+
3880
+ // Infer missing parameters for weaker models (normalize inputs before deduplication)
3881
+ const inference = this.inferMissingParameters(content.name, content.input);
3882
+ if (inference.modified) {
3883
+ content.input = inference.input;
3884
+ this.daemon.logEvent(this.task.id, 'parameter_inference', {
3885
+ tool: content.name,
3886
+ inference: inference.inference,
3887
+ });
3888
+ }
3889
+
3890
+ // If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
3891
+ await this.handleCanvasPushFallback(content, assistantText);
3892
+
3707
3893
  // Check for duplicate tool calls (prevents stuck loops)
3708
3894
  const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
3709
3895
  if (duplicateCheck.isDuplicate) {
@@ -3775,16 +3961,6 @@ SCHEDULING & REMINDERS:
3775
3961
  continue;
3776
3962
  }
3777
3963
 
3778
- // Infer missing parameters for weaker models
3779
- const inference = this.inferMissingParameters(content.name, content.input);
3780
- if (inference.modified) {
3781
- content.input = inference.input;
3782
- this.daemon.logEvent(this.task.id, 'parameter_inference', {
3783
- tool: content.name,
3784
- inference: inference.inference,
3785
- });
3786
- }
3787
-
3788
3964
  this.daemon.logEvent(this.task.id, 'tool_call', {
3789
3965
  tool: content.name,
3790
3966
  input: content.input,
@@ -3792,12 +3968,13 @@ SCHEDULING & REMINDERS:
3792
3968
 
3793
3969
  try {
3794
3970
  // Execute tool with timeout to prevent hanging
3971
+ const toolTimeoutMs = this.getToolTimeoutMs(content.name, content.input);
3795
3972
  const result = await withTimeout(
3796
3973
  this.toolRegistry.executeTool(
3797
3974
  content.name,
3798
3975
  content.input as any
3799
3976
  ),
3800
- TOOL_TIMEOUT_MS,
3977
+ toolTimeoutMs,
3801
3978
  `Tool ${content.name}`
3802
3979
  );
3803
3980
 
@@ -3873,7 +4050,7 @@ SCHEDULING & REMINDERS:
3873
4050
 
3874
4051
  // If all tool attempts were for disabled or duplicate tools, don't continue looping
3875
4052
  const allToolsFailed = toolResults.every(r => r.is_error);
3876
- if ((hasDisabledToolAttempt || hasDuplicateToolAttempt) && allToolsFailed) {
4053
+ if ((hasDisabledToolAttempt || hasDuplicateToolAttempt || hasUnavailableToolAttempt) && allToolsFailed) {
3877
4054
  console.log('[TaskExecutor] All tool calls failed, were disabled, or duplicates - stopping iteration');
3878
4055
  continueLoop = false;
3879
4056
  } else {
@@ -0,0 +1,214 @@
1
+ import {
2
+ LLMProvider,
3
+ LLMProviderType,
4
+ LLMRequest,
5
+ LLMResponse,
6
+ LLMContent,
7
+ LLMMessage,
8
+ LLMTool,
9
+ } from './types';
10
+
11
+ const ANTHROPIC_VERSION = '2023-06-01';
12
+
13
+ function joinUrl(baseUrl: string, path: string): string {
14
+ const trimmedBase = baseUrl.replace(/\/+$/, '');
15
+ const trimmedPath = path.startsWith('/') ? path : `/${path}`;
16
+ return `${trimmedBase}${trimmedPath}`;
17
+ }
18
+
19
+ export interface AnthropicCompatibleProviderOptions {
20
+ type: LLMProviderType;
21
+ providerName: string;
22
+ apiKey: string;
23
+ baseUrl: string;
24
+ defaultModel: string;
25
+ }
26
+
27
+ export class AnthropicCompatibleProvider implements LLMProvider {
28
+ readonly type: LLMProviderType;
29
+ private apiKey: string;
30
+ private baseUrl: string;
31
+ private defaultModel: string;
32
+ private providerName: string;
33
+
34
+ constructor(options: AnthropicCompatibleProviderOptions) {
35
+ this.type = options.type;
36
+ this.apiKey = options.apiKey;
37
+ this.baseUrl = options.baseUrl;
38
+ this.defaultModel = options.defaultModel;
39
+ this.providerName = options.providerName;
40
+ }
41
+
42
+ async createMessage(request: LLMRequest): Promise<LLMResponse> {
43
+ const messages = this.convertMessages(request.messages);
44
+ const tools = request.tools ? this.convertTools(request.tools) : undefined;
45
+ const model = request.model || this.defaultModel;
46
+
47
+ try {
48
+ console.log(`[${this.providerName}] Calling API with model: ${model}`);
49
+ const response = await fetch(joinUrl(this.baseUrl, '/messages'), {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'x-api-key': this.apiKey,
54
+ 'anthropic-version': ANTHROPIC_VERSION,
55
+ },
56
+ body: JSON.stringify({
57
+ model,
58
+ max_tokens: request.maxTokens,
59
+ system: request.system,
60
+ messages,
61
+ ...(tools && { tools }),
62
+ }),
63
+ signal: request.signal,
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
68
+ throw new Error(
69
+ `${this.providerName} API error: ${response.status} ${response.statusText}` +
70
+ (errorData.error?.message ? ` - ${errorData.error.message}` : '')
71
+ );
72
+ }
73
+
74
+ const data = await response.json() as any;
75
+ return this.convertResponse(data);
76
+ } catch (error: any) {
77
+ if (error.name === 'AbortError' || error.message?.includes('aborted')) {
78
+ console.log(`[${this.providerName}] Request aborted`);
79
+ throw new Error('Request cancelled');
80
+ }
81
+
82
+ console.error(`[${this.providerName}] API error:`, {
83
+ message: error.message,
84
+ status: error.status,
85
+ });
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ async testConnection(): Promise<{ success: boolean; error?: string }> {
91
+ try {
92
+ const response = await fetch(joinUrl(this.baseUrl, '/messages'), {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ 'x-api-key': this.apiKey,
97
+ 'anthropic-version': ANTHROPIC_VERSION,
98
+ },
99
+ body: JSON.stringify({
100
+ model: this.defaultModel,
101
+ max_tokens: 10,
102
+ messages: [{ role: 'user', content: 'Hi' }],
103
+ }),
104
+ });
105
+
106
+ if (!response.ok) {
107
+ const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
108
+ return {
109
+ success: false,
110
+ error: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
111
+ };
112
+ }
113
+
114
+ return { success: true };
115
+ } catch (error: any) {
116
+ return {
117
+ success: false,
118
+ error: error.message || `Failed to connect to ${this.providerName} API`,
119
+ };
120
+ }
121
+ }
122
+
123
+ private convertMessages(messages: LLMMessage[]): Array<{ role: string; content: any }> {
124
+ return messages.map((msg) => {
125
+ if (typeof msg.content === 'string') {
126
+ return {
127
+ role: msg.role,
128
+ content: msg.content,
129
+ };
130
+ }
131
+
132
+ const content = msg.content.map((item) => {
133
+ if (item.type === 'tool_result') {
134
+ return {
135
+ type: 'tool_result' as const,
136
+ tool_use_id: item.tool_use_id,
137
+ content: item.content,
138
+ ...(item.is_error && { is_error: true }),
139
+ };
140
+ }
141
+ if (item.type === 'tool_use') {
142
+ return {
143
+ type: 'tool_use' as const,
144
+ id: item.id,
145
+ name: item.name,
146
+ input: item.input,
147
+ };
148
+ }
149
+ return {
150
+ type: 'text' as const,
151
+ text: item.text,
152
+ };
153
+ });
154
+
155
+ return {
156
+ role: msg.role,
157
+ content,
158
+ };
159
+ });
160
+ }
161
+
162
+ private convertTools(tools: LLMTool[]): Array<{ name: string; description: string; input_schema: any }> {
163
+ return tools.map((tool) => ({
164
+ name: tool.name,
165
+ description: tool.description,
166
+ input_schema: tool.input_schema,
167
+ }));
168
+ }
169
+
170
+ private convertResponse(response: any): LLMResponse {
171
+ const content: LLMContent[] = (response.content || [])
172
+ .filter((block: any) => block.type === 'text' || block.type === 'tool_use')
173
+ .map((block: any) => {
174
+ if (block.type === 'tool_use') {
175
+ return {
176
+ type: 'tool_use' as const,
177
+ id: block.id,
178
+ name: block.name,
179
+ input: block.input as Record<string, any>,
180
+ };
181
+ }
182
+ return {
183
+ type: 'text' as const,
184
+ text: block.text || '',
185
+ };
186
+ });
187
+
188
+ return {
189
+ content: content.length > 0 ? content : [{ type: 'text', text: '' }],
190
+ stopReason: this.mapStopReason(response.stop_reason),
191
+ usage: response.usage
192
+ ? {
193
+ inputTokens: response.usage.input_tokens || 0,
194
+ outputTokens: response.usage.output_tokens || 0,
195
+ }
196
+ : undefined,
197
+ };
198
+ }
199
+
200
+ private mapStopReason(reason?: string): LLMResponse['stopReason'] {
201
+ switch (reason) {
202
+ case 'end_turn':
203
+ return 'end_turn';
204
+ case 'tool_use':
205
+ return 'tool_use';
206
+ case 'max_tokens':
207
+ return 'max_tokens';
208
+ case 'stop_sequence':
209
+ return 'stop_sequence';
210
+ default:
211
+ return 'end_turn';
212
+ }
213
+ }
214
+ }