shortcutxl 0.3.67 → 0.3.68

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 (56) hide show
  1. package/BINARY-INVENTORY.json +813 -813
  2. package/CHANGELOG.md +2 -2
  3. package/dist/ai/types.d.ts +1 -0
  4. package/dist/ai/utils/validation.js +6 -2
  5. package/dist/ai/validation.d.ts +1 -0
  6. package/dist/ai/validation.js +6 -2
  7. package/dist/app/background/tool-summaries.js +2 -1
  8. package/dist/app/prompts/com-api.js +7 -1
  9. package/dist/app/prompts/spreadjs-api-reference.json +19 -1
  10. package/dist/app/providers/register-shortcut-provider.js +46 -1
  11. package/dist/app/providers/shortcut-llm-proxy-client.d.ts +2 -0
  12. package/dist/app/providers/shortcut-llm-proxy-client.js +15 -3
  13. package/dist/app/tools/execute-tool.d.ts +5 -0
  14. package/dist/app/tools/execute-tool.js +25 -11
  15. package/dist/app/tools/llm-analysis.js +2 -1
  16. package/dist/app/tools/take-screenshot.js +2 -1
  17. package/dist/cli.js +23508 -23384
  18. package/dist/contracts/host-tool.d.ts +1 -0
  19. package/dist/contracts/host-tool.js +14 -0
  20. package/dist/core/prompts/agent-guidelines.d.ts +1 -1
  21. package/dist/core/prompts/agent-guidelines.js +32 -12
  22. package/dist/embedded-agent/anthropic-messages-transport.js +24 -0
  23. package/dist/embedded-agent/compose.js +14 -9
  24. package/dist/embedded-agent/host-tools/execute-code/allowed-functions.json +12 -0
  25. package/dist/embedded-agent/host-tools/execute-code/contract.d.ts +1 -1
  26. package/dist/embedded-agent/host-tools/execute-code/contract.js +34 -6
  27. package/dist/embedded-agent/host-tools/execute-code/syntax-repair.d.ts +2 -0
  28. package/dist/embedded-agent/host-tools/execute-code/syntax-repair.js +59 -0
  29. package/dist/embedded-agent/host-tools/execute-tool/contract.d.ts +2 -1
  30. package/dist/embedded-agent/host-tools/execute-tool/contract.js +31 -9
  31. package/dist/embedded-agent/host-tools/execute-tool/index.d.ts +1 -1
  32. package/dist/embedded-agent/host-tools/execute-tool/index.js +1 -1
  33. package/dist/embedded-agent/host-tools/get-tool-info/contract.js +27 -16
  34. package/dist/embedded-agent/host-tools/task/contract.js +4 -4
  35. package/dist/embedded-agent/model-registry.d.ts +0 -1
  36. package/dist/embedded-agent/model-registry.js +3 -3
  37. package/dist/embedded-agent/prompt/uploads.d.ts +0 -16
  38. package/dist/embedded-agent/prompt/uploads.js +0 -8
  39. package/dist/main.js +3 -1
  40. package/dist/model-ids.d.ts +4 -0
  41. package/dist/model-ids.js +6 -1
  42. package/dist/utils/structured-clone-safe.js +33 -7
  43. package/package.json +1 -1
  44. package/user-docs/dist/shortcutxl-docs.pdf +0 -0
  45. package/xll/ShortcutXL.xll +0 -0
  46. package/xll/python/Lib/site-packages/httpx-0.28.1.dist-info/RECORD +1 -1
  47. package/xll/python/Lib/site-packages/idna-3.18.dist-info/RECORD +1 -1
  48. package/xll/python/Lib/site-packages/pip-26.1.2.dist-info/RECORD +3 -3
  49. package/xll/python/Lib/site-packages/pywin32-311.dist-info/RECORD +2 -2
  50. package/xll/python/Scripts/httpx.exe +0 -0
  51. package/xll/python/Scripts/idna.exe +0 -0
  52. package/xll/python/Scripts/pip.exe +0 -0
  53. package/xll/python/Scripts/pip3.13.exe +0 -0
  54. package/xll/python/Scripts/pip3.exe +0 -0
  55. package/xll/python/Scripts/pywin32_postinstall.exe +0 -0
  56. package/xll/python/Scripts/pywin32_testall.exe +0 -0
package/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## [0.3.67]
3
+ ## [0.3.68]
4
4
 
5
+ - **Model availability fix** - Introducing GLM 5.2 and Pivot models for eligible individual CLI accounts, not teams plans.
5
6
  - **Persistent prompt history** — Your prompt history now persists across CLI sessions and can be accessed by up/down arrows.
6
- - **GLM 5.2** — Added the GLM 5.2 model with zero data retention.
7
7
 
8
8
  ## [0.3.65]
9
9
 
@@ -148,6 +148,7 @@ export interface Tool<TParameters extends TSchema = TSchema> {
148
148
  name: string;
149
149
  description: string;
150
150
  parameters: TParameters;
151
+ normalizeArguments?: (args: unknown, toolCall: ToolCall) => unknown;
151
152
  }
152
153
  export interface Context {
153
154
  systemPrompt?: string;
@@ -23,6 +23,10 @@ if (!isBrowserExtension) {
23
23
  console.warn('AJV validation disabled due to CSP restrictions');
24
24
  }
25
25
  }
26
+ function getValidationArgs(tool, toolCall) {
27
+ const args = structuredClone(toolCall.arguments);
28
+ return tool.normalizeArguments ? tool.normalizeArguments(args, toolCall) : args;
29
+ }
26
30
  /**
27
31
  * Finds a tool by name and validates the tool call arguments against its TypeBox schema
28
32
  * @param tools Array of tool definitions
@@ -49,12 +53,12 @@ export function validateToolArguments(tool, toolCall) {
49
53
  if (!ajv || isBrowserExtension) {
50
54
  // Trust the LLM's output without validation
51
55
  // Browser extensions can't use AJV due to Manifest V3 CSP restrictions
52
- return toolCall.arguments;
56
+ return getValidationArgs(tool, toolCall);
53
57
  }
54
58
  // Compile the schema
55
59
  const validate = ajv.compile(tool.parameters);
56
60
  // Clone arguments so AJV can safely mutate for type coercion
57
- const args = structuredClone(toolCall.arguments);
61
+ const args = getValidationArgs(tool, toolCall);
58
62
  // Validate the arguments (AJV mutates args in-place for type coercion)
59
63
  if (validate(args)) {
60
64
  return args;
@@ -3,6 +3,7 @@ import type { ToolCall } from './types.js';
3
3
  interface ValidatedTool {
4
4
  name: string;
5
5
  parameters: TSchema;
6
+ normalizeArguments?: (args: unknown, toolCall: ToolCall) => unknown;
6
7
  }
7
8
  export declare function validateToolArguments<TArgs = unknown>(tool: ValidatedTool, toolCall: ToolCall): TArgs;
8
9
  export {};
@@ -20,12 +20,16 @@ if (!isBrowserExtension) {
20
20
  console.warn('AJV validation disabled due to CSP restrictions');
21
21
  }
22
22
  }
23
+ function getValidationArgs(tool, toolCall) {
24
+ const args = structuredClone(toolCall.arguments);
25
+ return tool.normalizeArguments ? tool.normalizeArguments(args, toolCall) : args;
26
+ }
23
27
  export function validateToolArguments(tool, toolCall) {
24
28
  if (!ajv || isBrowserExtension) {
25
- return toolCall.arguments;
29
+ return getValidationArgs(tool, toolCall);
26
30
  }
27
31
  const validate = ajv.compile(tool.parameters);
28
- const args = structuredClone(toolCall.arguments);
32
+ const args = getValidationArgs(tool, toolCall);
29
33
  if (validate(args)) {
30
34
  return args;
31
35
  }
@@ -36,7 +36,8 @@ export async function generateToolSummaryDetailed(input, shortcutLlmProxy, optio
36
36
  ],
37
37
  max_output_tokens: maxOutputTokens,
38
38
  effort_level: 'low',
39
- thinking_type: 'off'
39
+ thinking_type: 'off',
40
+ llmOperation: 'summarize'
40
41
  }, {
41
42
  signal
42
43
  });
@@ -127,7 +127,13 @@ export function getExcelComApiGuidelines(filterTag) {
127
127
  ===========================
128
128
  ## Shortcut Wrapper API
129
129
  ===========================
130
- Workbook, Worksheet, and helpers are pre-injected in execute_code (no import needed).`;
130
+ Workbook, Worksheet, and helpers are pre-injected in execute_code (no import needed).
131
+
132
+ ### ALWAYS start a task like this (app is already injected — do NOT import win32com or Dispatch Excel):
133
+ names = [wb.Name for wb in app.Workbooks] # find the target workbook name
134
+ wb = Workbook(app.Workbooks("YourFile.xlsx")) # wrap the COM object — NOT a filename string: Workbook("YourFile.xlsx") fails
135
+ print(wb.getSheetNames()) # use getSheetNames() — NOT wb.SheetNames
136
+ ws = wb.getSheet("Sheet1") # use getSheet(name) — NOT wb.Worksheets(...)`;
131
137
  const sections = [COM_GUIDELINES, header];
132
138
  for (const [name, cls] of Object.entries(ref.interfaces)) {
133
139
  sections.push(renderSection(name, cls, filterTag));
@@ -4,7 +4,7 @@
4
4
  "functions": {
5
5
  "getAPIInfo": {
6
6
  "signature": "getAPIInfo(functionName: string): string;",
7
- "docstring": "Retrieves extended API documentation for less common functions. Returns docstring, function signature, parameters, return types, and all parameter type definitions.\n\nMerging/unmerging cells: addSpanAt, removeSpanAt\nCopying: cutAndPasteRange (move/cut-paste within same workbook), copySheet (full sheet duplication within same workbook)\nSheet dimensions: setTotalRowCount, setTotalColumnCount, expandSheetToFit\n - CRITICAL: SpreadJS initializes sheets with 10K rows and 100 columns. Writing to cells beyond these limits will SILENTLY FAIL unless you expand first!\n - Example: sheet.expandSheetToFit(\"A60000\"); sheet.setCell(\"A60000\", \"data\");\nNamed ranges: getNamedRangeInfo, addNamedRange, removeNamedRange\n@example general.getAPIInfo(\"addSpanAt\");",
7
+ "docstring": "Retrieves extended API documentation for less common functions. Returns docstring, function signature, parameters, return types, and all parameter type definitions.\n\nMerging/unmerging cells: addSpanAt, removeSpanAt\nSizing (all in px coordinates): setColumnWidthAt, setRowHeightAt, autoFitColumnsAt\nCopying: cutAndPasteRange (move/cut-paste within same workbook), copySheet (full sheet duplication within same workbook)\nSheet dimensions: setTotalRowCount, setTotalColumnCount, expandSheetToFit\n - CRITICAL: SpreadJS initializes sheets with 10K rows and 100 columns. Writing to cells beyond these limits will SILENTLY FAIL unless you expand first!\n - Example: sheet.expandSheetToFit(\"A60000\"); sheet.setCell(\"A60000\", \"data\");\nNamed ranges: getNamedRangeInfo, addNamedRange, removeNamedRange\n@example general.getAPIInfo(\"addSpanAt\");",
8
8
  "usedTypes": [],
9
9
  "tags": ["action", "ask"]
10
10
  },
@@ -221,6 +221,12 @@
221
221
  "usedTypes": [],
222
222
  "tags": ["action", "ask"]
223
223
  },
224
+ "autoFitColumnsAt": {
225
+ "signature": "autoFitColumnsAt(columnRange: string): void;",
226
+ "docstring": "Auto-fit the columns specified by columnRange e.g. autoFitColumnsAt(\"A:G\");\n@example sheet.autoFitColumnsAt(\"A:G\");",
227
+ "usedTypes": [],
228
+ "tags": ["hidden", "approval"]
229
+ },
224
230
  "clearFormatting": {
225
231
  "signature": "clearFormatting(range: string, properties?: (keyof Style)[]): void;",
226
232
  "docstring": "Clear formatting from a range of cells. Clears all formatting if no properties specified.\nPass specific Style keys to clear only those properties.",
@@ -245,12 +251,24 @@
245
251
  "usedTypes": [],
246
252
  "tags": ["action", "approval"]
247
253
  },
254
+ "setColumnWidthAt": {
255
+ "signature": "setColumnWidthAt(columnLetter: string, width: number): void;",
256
+ "docstring": "Set explicit width for the given column in pixels. Default column width is 72px.\n@example setColumnWidthAt(\"A\", 100);",
257
+ "usedTypes": [],
258
+ "tags": ["action", "approval"]
259
+ },
248
260
  "getDependentCells": {
249
261
  "signature": "getDependentCells(address: string): string;",
250
262
  "docstring": "Get all cells that depend on the given cell",
251
263
  "usedTypes": [],
252
264
  "tags": ["action", "ask"]
253
265
  },
266
+ "setRowHeightAt": {
267
+ "signature": "setRowHeightAt(rowAddress: string, height: number): void;",
268
+ "docstring": "Set explicit height for the given row in pixels. Default row height is 21px.\n@example setRowHeightAt(\"1\", 100);",
269
+ "usedTypes": [],
270
+ "tags": ["hidden", "approval"]
271
+ },
254
272
  "addSpanAt": {
255
273
  "signature": "addSpanAt(address: string, rowCount: number, colCount: number): void;",
256
274
  "docstring": "Merge a range starting from the cell at address (e.g., \"A1\")",
@@ -33,6 +33,50 @@ const BUILTIN_STREAM_BY_TRANSPORT = {
33
33
  'openai-responses': streamSimpleOpenAIResponses,
34
34
  'google-vertex': streamSimpleGoogleVertexGateway
35
35
  };
36
+ function headerValue(value) {
37
+ const trimmed = value?.trim();
38
+ return trimmed ? trimmed : undefined;
39
+ }
40
+ function isMainAgentStreamOptions(options) {
41
+ if (!options)
42
+ return false;
43
+ return (typeof options.convertToLlm === 'function' ||
44
+ options.userMessageId !== undefined ||
45
+ options.agentMode !== undefined ||
46
+ options.initialFileId !== undefined ||
47
+ options.fileAttachmentIds !== undefined ||
48
+ options.interactionMode !== undefined ||
49
+ options.speed !== undefined);
50
+ }
51
+ function createShortcutApiV2AttributionHeaders(options) {
52
+ const attribution = options;
53
+ const headers = {
54
+ 'X-Agent-Runtime': 'shortcutxl_cli'
55
+ };
56
+ const userMessageId = headerValue(attribution?.userMessageId);
57
+ if (userMessageId)
58
+ headers['X-Turn-Id'] = userMessageId;
59
+ const agentMode = headerValue(attribution?.agentMode);
60
+ if (agentMode)
61
+ headers['X-Agent-Mode'] = agentMode;
62
+ const interactionMode = headerValue(attribution?.interactionMode);
63
+ if (interactionMode)
64
+ headers['X-Interaction-Mode'] = interactionMode;
65
+ const llmOperation = headerValue(attribution?.llmOperation) ??
66
+ (isMainAgentStreamOptions(attribution) ? 'chat' : undefined);
67
+ if (llmOperation)
68
+ headers['X-LLM-Operation'] = llmOperation;
69
+ const initialFileId = headerValue(attribution?.initialFileId);
70
+ if (initialFileId)
71
+ headers['X-Initial-File-Id'] = initialFileId;
72
+ if (attribution?.fileAttachmentIds?.length) {
73
+ headers['X-File-Attachment-Ids'] = attribution.fileAttachmentIds.join(',');
74
+ }
75
+ if (attribution?.speed === 'fast' || attribution?.speed === 'standard') {
76
+ headers['X-Shortcut-Speed'] = attribution.speed;
77
+ }
78
+ return headers;
79
+ }
36
80
  /**
37
81
  * Dispatch a Shortcut model to its provider-native stream function, routed at
38
82
  * the api-v2 gateway with Shortcut auth. `options.apiKey` is the Shortcut
@@ -53,7 +97,8 @@ export function createShortcutApiV2Dispatcher(getExtraHeaders) {
53
97
  const shortcutToken = options?.apiKey ?? '';
54
98
  const authHeaders = {
55
99
  Authorization: `Bearer ${shortcutToken}`,
56
- ...getExtraHeaders()
100
+ ...getExtraHeaders(),
101
+ ...createShortcutApiV2AttributionHeaders(options)
57
102
  };
58
103
  // Present the model to the native stream function under its real transport,
59
104
  // with the base URL shaped the way that transport's SDK expects (the OpenAI
@@ -14,6 +14,7 @@ export interface ShortcutLlmAttribution {
14
14
  sessionId?: string;
15
15
  userMessageId?: string;
16
16
  }
17
+ export type ShortcutLlmProxyOperation = 'code_preview' | 'compaction' | 'file_naming' | 'formula_trace' | 'llm_analysis' | 'sandbox_llm' | 'screenshot' | 'sentiment_analysis' | 'subagent' | 'suggestions' | 'summarize';
17
18
  /**
18
19
  * A helper LLM request. `messages` uses the simplified role/content shape
19
20
  * callers already build (`system`/`human`, string or OpenAI-style content
@@ -28,6 +29,7 @@ export interface ShortcutLlmProxyRequest {
28
29
  max_tokens?: number;
29
30
  max_output_tokens?: number;
30
31
  speed?: 'fast' | 'standard';
32
+ llmOperation?: ShortcutLlmProxyOperation;
31
33
  }
32
34
  export interface ShortcutLlmMessage {
33
35
  role: 'system' | 'human' | 'user' | 'ai' | 'assistant';
@@ -43,6 +43,8 @@ export function mapShortcutLlmProxyError(error) {
43
43
  }
44
44
  /** Parse an OpenAI-style data URI (`data:<mime>;base64,<data>`) into parts. */
45
45
  function parseDataUri(url) {
46
+ // [\s\S]* matches everything (incl. newlines) without the `s` dotAll flag,
47
+ // which requires an es2018+ target; keeps this parser target-agnostic.
46
48
  const match = /^data:([^;]+);base64,([\s\S]*)$/.exec(url);
47
49
  if (!match)
48
50
  return undefined;
@@ -108,6 +110,18 @@ function collectAssistantText(events) {
108
110
  return text;
109
111
  })();
110
112
  }
113
+ function createShortcutLlmProxyHeaders(request, attribution) {
114
+ const headers = {};
115
+ if (attribution?.sessionId)
116
+ headers['X-Session-Id'] = attribution.sessionId;
117
+ if (attribution?.userMessageId)
118
+ headers['X-Turn-Id'] = attribution.userMessageId;
119
+ if (request.llmOperation)
120
+ headers['X-LLM-Operation'] = request.llmOperation;
121
+ if (request.speed)
122
+ headers['X-Shortcut-Speed'] = request.speed;
123
+ return Object.keys(headers).length > 0 ? headers : undefined;
124
+ }
111
125
  /**
112
126
  * Create a Shortcut helper LLM client backed by the model registry's native
113
127
  * transport path (api-v2). All helper calls share the main agent's transport.
@@ -129,9 +143,7 @@ export function createShortcutLlmProxyClient(options) {
129
143
  throw new ShortcutLlmProxyError('missing-transport', `No stream transport registered for model "${request.model}" (api ${model.api}).`);
130
144
  }
131
145
  const attribution = options.getAttribution?.();
132
- const headers = attribution?.sessionId
133
- ? { 'X-Session-Id': attribution.sessionId }
134
- : undefined;
146
+ const headers = createShortcutLlmProxyHeaders(request, attribution);
135
147
  const stream = apiProvider.streamSimple(model, toContext(request), {
136
148
  apiKey,
137
149
  maxTokens: request.max_tokens ?? request.max_output_tokens,
@@ -12,6 +12,11 @@ declare const schema: import("@sinclair/typebox").TObject<{
12
12
  tool_name: import("@sinclair/typebox").TString;
13
13
  parameters: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>;
14
14
  }>;
15
+ /**
16
+ * Auto-fix when LLM passes target tool params flat instead of nested in 'parameters'.
17
+ * Example: { tool_name: "loop", action: "start" } → { tool_name: "loop", parameters: { action: "start" } }
18
+ */
19
+ export declare function normalizeExecuteToolArgs(raw: unknown): unknown;
15
20
  export declare function createExecuteToolTool(registry: ExecutableToolEntry[]): ToolDefinition<typeof schema>;
16
21
  export {};
17
22
  //# sourceMappingURL=execute-tool.d.ts.map
@@ -7,6 +7,7 @@
7
7
  * Modeled after backend/agent/tools/execute_tool.py.
8
8
  */
9
9
  import { Type } from '@sinclair/typebox';
10
+ import { parseStringifiedParameters } from '../../contracts/host-tool.js';
10
11
  import { EXECUTE_TOOL } from '../../tool-names.js';
11
12
  // ---------------------------------------------------------------------------
12
13
  // Schema
@@ -19,31 +20,43 @@ const schema = Type.Object({
19
20
  description: 'Dictionary of parameters to pass to the tool. Keys must match the parameter names from get_tool_info.'
20
21
  })
21
22
  });
22
- // ---------------------------------------------------------------------------
23
- // Auto-nesting fix
24
- // ---------------------------------------------------------------------------
25
23
  /**
26
24
  * Auto-fix when LLM passes target tool params flat instead of nested in 'parameters'.
27
25
  * Example: { tool_name: "loop", action: "start" } → { tool_name: "loop", parameters: { action: "start" } }
28
26
  */
29
- function autoNestFlatParameters(raw) {
30
- if ('parameters' in raw) {
27
+ export function normalizeExecuteToolArgs(raw) {
28
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
31
29
  return raw;
30
+ const value = raw;
31
+ const normalized = { ...value };
32
+ if (typeof normalized.tool_name === 'string') {
33
+ normalized.tool_name = normalized.tool_name.trim();
34
+ }
35
+ if ('parameters' in normalized) {
36
+ normalized.parameters = parseStringifiedParameters(normalized.parameters);
37
+ return normalized;
32
38
  }
33
39
  const knownFields = new Set(['tool_name']);
34
40
  const extra = {};
35
- for (const [key, value] of Object.entries(raw)) {
41
+ for (const [key, paramValue] of Object.entries(normalized)) {
36
42
  if (!knownFields.has(key)) {
37
- extra[key] = value;
43
+ extra[key] = paramValue;
38
44
  }
39
45
  }
40
- if (Object.keys(extra).length > 0 && typeof raw.tool_name === 'string') {
46
+ if (Object.keys(extra).length > 0 && typeof normalized.tool_name === 'string') {
41
47
  return {
42
- tool_name: raw.tool_name,
48
+ tool_name: normalized.tool_name,
43
49
  parameters: extra
44
50
  };
45
51
  }
46
- return raw;
52
+ return normalized;
53
+ }
54
+ function normalizeParams(raw) {
55
+ const normalized = normalizeExecuteToolArgs(raw);
56
+ if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) {
57
+ return raw;
58
+ }
59
+ return normalized;
47
60
  }
48
61
  // ---------------------------------------------------------------------------
49
62
  // Description
@@ -64,8 +77,9 @@ export function createExecuteToolTool(registry) {
64
77
  isReadOnly: false,
65
78
  description: DESCRIPTION,
66
79
  parameters: schema,
80
+ normalizeArguments: normalizeExecuteToolArgs,
67
81
  async execute(toolCallId, rawParams, signal, onUpdate, ctx) {
68
- const params = autoNestFlatParameters(rawParams);
82
+ const params = normalizeParams(rawParams);
69
83
  const { tool_name, parameters } = params;
70
84
  // Find the tool
71
85
  const entry = registry.find((e) => e.definition.name === tool_name);
@@ -219,7 +219,8 @@ export function createLlmAnalysisTool() {
219
219
  model: MODELS[selectedModel],
220
220
  messages,
221
221
  effort_level: 'low',
222
- thinking_type: 'off'
222
+ thinking_type: 'off',
223
+ llmOperation: 'llm_analysis'
223
224
  }, { signal });
224
225
  }
225
226
  catch (e) {
@@ -121,7 +121,8 @@ async function defaultAnalyzeWithVisionLLM(screenshots, query, model, shortcutLl
121
121
  messages,
122
122
  max_tokens: SCREENSHOT_LLM_MAX_TOKENS,
123
123
  effort_level: 'low',
124
- thinking_type: 'off'
124
+ thinking_type: 'off',
125
+ llmOperation: 'screenshot'
125
126
  }, { signal: signal ?? AbortSignal.timeout(SCREENSHOT_LLM_TIMEOUT_MS) });
126
127
  }
127
128
  catch (e) {