@remcp/runtime 0.2.153 → 0.2.155

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.
@@ -0,0 +1,346 @@
1
+ import os from 'node:os';
2
+ import process from 'node:process';
3
+ import { fileToolHandlers } from '../tools/files.mjs';
4
+ import { liveConfig, runtimeConfig } from '../config.mjs';
5
+ import { assertAllowedCommand } from '../policy.mjs';
6
+ import { multi, resolveSafePath, text } from '../util.mjs';
7
+ import { clamp, desktopSessionAvailable, jsonResult, optionalString, requireEnum } from './common.mjs';
8
+ import * as linux from './desktop-linux.mjs';
9
+ import * as macos from './desktop-macos.mjs';
10
+ import * as windows from './desktop-windows.mjs';
11
+ import { browserAction, browserCapabilityAvailable, browserSnapshot } from './browser.mjs';
12
+ import { findOcrText, ocrImage } from './ocr.mjs';
13
+
14
+
15
+ import { clipboard, displayInventory, keyboard, listWindows, notification, pointer, resultValue, screenshotRegion, scroll, sleep, uiAction, uiSnapshot, windowAction } from './desktop-core.mjs';
16
+ import { dragDrop, launchApp, openPath, revealPath, typeText, uiFind, waitForUi } from './desktop-ui-actions.mjs';
17
+ import { clickOcrText } from './desktop-vision.mjs';
18
+ import { computerSnapshot } from './desktop-computer-snapshot.mjs';
19
+
20
+ export function browserComputerAction(args, action) {
21
+ const mapped = {
22
+ click:'click',
23
+ invoke:'click',
24
+ toggle:'click',
25
+ focus:'focus',
26
+ set_value:'set_value',
27
+ select:'select',
28
+ type:'type',
29
+ scroll:'scroll',
30
+ }[action];
31
+ if (!mapped) throw new Error(`Browser target does not support computer_action action: ${action}`);
32
+ const visibleText = optionalString(args.browser_text)
33
+ || optionalString(args.name)
34
+ || ((['click','invoke','toggle','focus'].includes(action) && optionalString(args.text)) ? optionalString(args.text) : null);
35
+ return browserAction({
36
+ endpoint: args.endpoint,
37
+ target_id: args.target_id,
38
+ title: args.title,
39
+ url_contains: args.url_contains,
40
+ timeout_ms: args.timeout_ms,
41
+ action: mapped,
42
+ selector: args.selector,
43
+ ...(visibleText ? { browser_text: visibleText } : {}),
44
+ ...(args.value != null ? { value: args.value } : {}),
45
+ ...(args.option != null ? { option: args.option } : {}),
46
+ ...(action === 'type' && args.text != null ? { text: args.text, clear:args.clear === true } : {}),
47
+ ...(action === 'scroll' ? {
48
+ delta_x:args.delta_x,
49
+ delta_y:args.delta_y,
50
+ delta:args.delta,
51
+ direction:args.direction,
52
+ wheel_times:args.wheel_times,
53
+ } : {}),
54
+ });
55
+ }
56
+
57
+ export function computerActionWindowTextArgs(args = {}) {
58
+ const {
59
+ id: _id,
60
+ label: _label,
61
+ name: _name,
62
+ role: _role,
63
+ automation_id: _automationId,
64
+ automationId: _automationIdAlias,
65
+ selector: _selector,
66
+ browser_text: _browserText,
67
+ ...windowArgs
68
+ } = args;
69
+ return {
70
+ ...windowArgs,
71
+ ...(args.window_title && !args.title ? { title:args.window_title } : {}),
72
+ };
73
+ }
74
+
75
+ export async function computerAction(args = {}) {
76
+ const legacy = ['ui','pointer','window','type','keyboard','scroll','drag','clipboard','launch_app','open_path','reveal_path','notification'];
77
+ const universal = [
78
+ 'click','double_click','right_click','move','down','up','invoke','focus','set_value','select','toggle',
79
+ 'expand','collapse','scroll_into_view','set_range_value','add_to_selection','remove_from_selection',
80
+ 'minimize','maximize','restore','move_resize','resize','close','wait','batch','multi_select','multi_edit',
81
+ ];
82
+ const action = requireEnum(args.action, 'action', [...legacy, ...universal]);
83
+ const explicitTarget = optionalString(args.target || args.target_type);
84
+ const target = explicitTarget ? requireEnum(explicitTarget, 'target', ['auto','ui','ui_element','browser','ocr','coordinates','window']) : 'auto';
85
+ const nested = { ...args, action: args.operation || args.subaction };
86
+ const hasUi = Boolean(args.label != null || args.id || args.name || args.role || args.automation_id || args.automationId);
87
+ const hasBrowser = Boolean(args.selector || args.browser_text || args.name);
88
+ const ocrNeedle = optionalString(args.ocr_text || args.name || args.browser_text);
89
+ const hasOcr = Boolean(ocrNeedle);
90
+ const hasCoordinates = Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y));
91
+ const automatic = target === 'auto';
92
+
93
+ if (action === 'batch') {
94
+ const steps = Array.isArray(args.steps) ? args.steps : [];
95
+ if (!steps.length) throw new Error('computer_action batch requires a non-empty steps array');
96
+ if (steps.length > 50) throw new Error('computer_action batch supports at most 50 steps');
97
+ const stopOnError = args.stop_on_error !== false;
98
+ const results = [];
99
+ for (let index = 0; index < steps.length; index += 1) {
100
+ const step = steps[index];
101
+ if (!step || typeof step !== 'object' || Array.isArray(step)) throw new Error(`batch step ${index} must be an object`);
102
+ if (step.action === 'batch') throw new Error('nested computer_action batches are not allowed');
103
+ try {
104
+ const result = await computerAction(step);
105
+ results.push({ index, ok:true, action:step.action, result:resultValue(result) ?? result?.content?.[0]?.text ?? null });
106
+ } catch (error) {
107
+ const failure = { index, ok:false, action:step.action || null, error:String(error?.message || error).slice(0, 1000) };
108
+ results.push(failure);
109
+ if (stopOnError) return jsonResult({ action:'batch', ok:false, stopped_at:index, completed:index, results });
110
+ }
111
+ }
112
+ return jsonResult({ action:'batch', ok:results.every(row => row.ok), completed:results.length, results });
113
+ }
114
+
115
+ if (action === 'multi_select') {
116
+ const targets = Array.isArray(args.targets) ? args.targets : [];
117
+ if (!targets.length) throw new Error('multi_select requires a non-empty targets array');
118
+ if (targets.length > 100) throw new Error('multi_select supports at most 100 targets');
119
+ const results = [];
120
+ for (let index = 0; index < targets.length; index += 1) {
121
+ const targetSpec = targets[index];
122
+ const operation = index === 0 && args.replace_selection !== false ? 'select' : 'add_to_selection';
123
+ const result = await uiAction({ ...targetSpec, action:operation });
124
+ results.push({ index, action:operation, result:resultValue(result) ?? result?.content?.[0]?.text ?? null });
125
+ }
126
+ return jsonResult({ action:'multi_select', count:results.length, results });
127
+ }
128
+
129
+ if (action === 'multi_edit') {
130
+ const edits = Array.isArray(args.edits) ? args.edits : [];
131
+ if (!edits.length) throw new Error('multi_edit requires a non-empty edits array');
132
+ if (edits.length > 100) throw new Error('multi_edit supports at most 100 edits');
133
+ const results = [];
134
+ for (let index = 0; index < edits.length; index += 1) {
135
+ const edit = edits[index];
136
+ if (!edit || typeof edit !== 'object' || edit.text == null) throw new Error(`multi_edit item ${index} requires target fields plus text`);
137
+ const result = await typeText({ ...edit, method:edit.method || 'auto', clear:edit.clear !== false });
138
+ results.push({ index, result:resultValue(result) ?? result?.content?.[0]?.text ?? null });
139
+ }
140
+ return jsonResult({ action:'multi_edit', count:results.length, results });
141
+ }
142
+
143
+ if (action === 'wait') {
144
+ const requested = args.wait_ms != null ? Number(args.wait_ms) : Number(args.seconds) * 1000;
145
+ if (!Number.isFinite(requested) || requested < 0) throw new Error('computer_action wait requires wait_ms or seconds');
146
+ const waitMs = Math.min(120_000, Math.round(requested));
147
+ await sleep(waitMs);
148
+ return jsonResult({ action:'wait', waited_ms:waitMs, capped:requested > 120_000 });
149
+ }
150
+
151
+ if (action === 'ui') return uiAction(nested);
152
+ if (action === 'pointer') return pointer(nested);
153
+ if (action === 'window') return windowAction(nested);
154
+
155
+ const windowTarget = () => ({
156
+ ...args,
157
+ ...(args.window_id ? { id:args.window_id } : {}),
158
+ ...(args.window_title && !args.title ? { title:args.window_title } : {}),
159
+ });
160
+ const windowTextTarget = () => computerActionWindowTextArgs(args);
161
+
162
+ if (action === 'type') {
163
+ if (target === 'browser') return browserComputerAction(args, 'type');
164
+ if (target === 'ocr' || target === 'coordinates') {
165
+ throw new Error('computer_action type requires a native UI, browser, or window target');
166
+ }
167
+ if (target === 'window') return typeText(windowTextTarget());
168
+ if (!automatic) return typeText({ ...args, method:args.method || 'accessibility' });
169
+
170
+ const failures = [];
171
+ if (hasUi) {
172
+ try { return await typeText({ ...args, method:'accessibility' }); }
173
+ catch (error) { failures.push(`accessibility: ${String(error?.message || error).slice(0, 300)}`); }
174
+ }
175
+ if (hasBrowser) {
176
+ if (await browserCapabilityAvailable(args.endpoint, 300)) {
177
+ try { return await browserComputerAction(args, 'type'); }
178
+ catch (error) { failures.push(`browser DOM: ${String(error?.message || error).slice(0, 300)}`); }
179
+ } else {
180
+ failures.push('browser DOM: no local CDP endpoint is available');
181
+ }
182
+ }
183
+ if (args.window_id || args.pid != null || args.app || args.window_title || args.title) {
184
+ try { return await typeText(windowTextTarget()); }
185
+ catch (error) { failures.push(`window: ${String(error?.message || error).slice(0, 300)}`); }
186
+ }
187
+ if (failures.length) throw new Error(`computer_action type targeting failed: ${failures.join('; ')}`);
188
+ throw new Error('computer_action type requires a native UI, browser, or window target');
189
+ }
190
+
191
+ if (action === 'keyboard') {
192
+ if (target === 'browser' && args.key && !args.shortcut && !Array.isArray(args.keys)) {
193
+ return browserAction({ ...args, action:'press' });
194
+ }
195
+ return keyboard(args);
196
+ }
197
+
198
+ if (action === 'scroll') {
199
+ if (target === 'browser') return browserComputerAction(args, 'scroll');
200
+ if (target === 'ocr') throw new Error('computer_action scroll does not support OCR targets; use browser/UI/window/coordinates');
201
+
202
+ if (automatic) {
203
+ let uiError = null;
204
+ if (hasUi) {
205
+ try { return await scroll(args); }
206
+ catch (error) { uiError = error; }
207
+ }
208
+ if (hasBrowser) {
209
+ if (await browserCapabilityAvailable(args.endpoint, 300)) return browserComputerAction(args, 'scroll');
210
+ if (uiError) throw new Error(`computer_action scroll targeting failed: accessibility: ${String(uiError?.message || uiError).slice(0, 300)}; browser DOM: no local CDP endpoint is available`);
211
+ throw new Error('computer_action scroll browser target is unavailable because no local CDP endpoint is available');
212
+ }
213
+ if (args.window_id || args.pid != null || args.app || args.window_title || args.title) {
214
+ await windowAction({ ...windowTarget(), action:'focus' });
215
+ }
216
+ return scroll(args);
217
+ }
218
+
219
+ if (target === 'window') {
220
+ await windowAction({ ...windowTarget(), action:'focus' });
221
+ return scroll(args);
222
+ }
223
+ return scroll(args);
224
+ }
225
+
226
+ if (action === 'drag') {
227
+ if (target === 'browser' || target === 'ocr') {
228
+ throw new Error('computer_action drag currently requires native UI element ids or desktop coordinates; browser/OCR drag is not advertised');
229
+ }
230
+ if (target === 'window') await windowAction({ ...windowTarget(), action:'focus' });
231
+ return dragDrop(args);
232
+ }
233
+ if (action === 'clipboard') return clipboard(nested);
234
+ if (action === 'launch_app') return launchApp(args);
235
+ if (action === 'open_path') return openPath(args);
236
+ if (action === 'reveal_path') return revealPath(args);
237
+ if (action === 'notification') return notification(args);
238
+
239
+ const inferredTarget = automatic
240
+ ? (hasUi ? 'ui_element' : hasBrowser ? 'browser' : args.ocr_text ? 'ocr' : hasCoordinates ? 'coordinates' : 'window')
241
+ : target;
242
+
243
+ const tryUiThenBrowser = async uiActionName => {
244
+ let uiError = null;
245
+ if (hasUi) {
246
+ try { return await uiAction({ ...args, action:uiActionName }); }
247
+ catch (error) { uiError = error; }
248
+ }
249
+ if (hasBrowser) return browserComputerAction(args, uiActionName);
250
+ if (uiError) throw uiError;
251
+ return null;
252
+ };
253
+
254
+ if (['invoke','set_value','select','toggle','expand','collapse','scroll_into_view','set_range_value','add_to_selection','remove_from_selection'].includes(action)) {
255
+ const browserCompatible = ['invoke','set_value','select','toggle'].includes(action);
256
+ if (inferredTarget === 'browser') {
257
+ if (!browserCompatible) throw new Error(`Browser target does not support ${action}; use browser_action or native accessibility`);
258
+ return browserComputerAction(args, action);
259
+ }
260
+ if (inferredTarget === 'ui' || inferredTarget === 'ui_element') {
261
+ if (automatic && browserCompatible) {
262
+ const result = await tryUiThenBrowser(action);
263
+ if (result) return result;
264
+ }
265
+ return uiAction({ ...args, action });
266
+ }
267
+ throw new Error(`${action} requires a UI${browserCompatible ? ' or browser' : ''} target`);
268
+ }
269
+
270
+ if (action === 'focus') {
271
+ if (inferredTarget === 'browser') return browserComputerAction(args, 'focus');
272
+ if (inferredTarget === 'ui' || inferredTarget === 'ui_element') {
273
+ if (automatic) {
274
+ const result = await tryUiThenBrowser('focus');
275
+ if (result) return result;
276
+ }
277
+ return uiAction({ ...args, action:'focus' });
278
+ }
279
+ return windowAction({ ...windowTarget(), action:'focus' });
280
+ }
281
+
282
+ if (['minimize','maximize','restore','move_resize','resize','close'].includes(action)) {
283
+ return windowAction({ ...args, action });
284
+ }
285
+
286
+ if (['click','double_click','right_click','move','down','up'].includes(action)) {
287
+ if (action === 'click' && automatic) {
288
+ const failures = [];
289
+ if (hasUi) {
290
+ try { return await uiAction({ ...args, action:'click' }); }
291
+ catch (error) { failures.push(`accessibility: ${String(error?.message || error).slice(0, 300)}`); }
292
+ }
293
+ if (hasBrowser) {
294
+ if (await browserCapabilityAvailable(args.endpoint, 300)) {
295
+ try { return await browserComputerAction(args, 'click'); }
296
+ catch (error) { failures.push(`browser DOM: ${String(error?.message || error).slice(0, 300)}`); }
297
+ } else {
298
+ failures.push('browser DOM: no local CDP endpoint is available');
299
+ }
300
+ }
301
+ if (hasOcr) {
302
+ try { return await clickOcrText({ ...args, ocr_text:ocrNeedle }); }
303
+ catch (error) { failures.push(`OCR: ${String(error?.message || error).slice(0, 300)}`); }
304
+ }
305
+ if (hasCoordinates) return pointer({ ...args, action:'click' });
306
+ if (failures.length) throw new Error(`computer_action click targeting failed: ${failures.join('; ')}`);
307
+ throw new Error('computer_action click requires a UI/browser/OCR target or x/y coordinates');
308
+ }
309
+ if (inferredTarget === 'browser') {
310
+ if (action !== 'click') throw new Error(`Browser target does not support ${action}; use browser_action or coordinates`);
311
+ return browserComputerAction(args, 'click');
312
+ }
313
+ if (inferredTarget === 'ocr') {
314
+ if (action !== 'click') throw new Error(`OCR target only supports click; ${action} requires a semantic or coordinate target`);
315
+ return clickOcrText({ ...args, ocr_text:ocrNeedle });
316
+ }
317
+ if ((inferredTarget === 'ui' || inferredTarget === 'ui_element') && action === 'click') {
318
+ return uiAction({ ...args, action:'click' });
319
+ }
320
+ return pointer({ ...args, action });
321
+ }
322
+ throw new Error(`Unsupported computer_action action: ${action}`);
323
+ }
324
+
325
+ export const desktopHandlers = {
326
+ computer_snapshot: computerSnapshot,
327
+ computer_action: computerAction,
328
+ list_windows: listWindows,
329
+ window_action: windowAction,
330
+ launch_app: launchApp,
331
+ ui_snapshot: uiSnapshot,
332
+ ui_find: uiFind,
333
+ ui_action: uiAction,
334
+ type_text: typeText,
335
+ keyboard,
336
+ pointer,
337
+ drag_drop: dragDrop,
338
+ scroll,
339
+ wait_for_ui: waitForUi,
340
+ clipboard,
341
+ display_inventory: displayInventory,
342
+ screenshot_region: screenshotRegion,
343
+ open_path: openPath,
344
+ reveal_path: revealPath,
345
+ notification,
346
+ };
@@ -0,0 +1,245 @@
1
+ import os from 'node:os';
2
+ import process from 'node:process';
3
+ import { fileToolHandlers } from '../tools/files.mjs';
4
+ import { liveConfig, runtimeConfig } from '../config.mjs';
5
+ import { assertAllowedCommand } from '../policy.mjs';
6
+ import { multi, resolveSafePath, text } from '../util.mjs';
7
+ import { clamp, desktopSessionAvailable, jsonResult, optionalString, requireEnum } from './common.mjs';
8
+ import * as linux from './desktop-linux.mjs';
9
+ import * as macos from './desktop-macos.mjs';
10
+ import * as windows from './desktop-windows.mjs';
11
+ import { browserAction, browserCapabilityAvailable, browserSnapshot } from './browser.mjs';
12
+ import { findOcrText, ocrImage } from './ocr.mjs';
13
+
14
+
15
+ import { boundedComputerSnapshot, clipboard, cursorPosition, displayInventory, listWindows, objectRecord, resultValue, screenshotRegion, snapshotError, suggestedUiAction, toolResultMessage, uiSnapshot } from './desktop-core.mjs';
16
+ import { activeWindowCaptureBounds, browserTitleHint, computerScreenshotTask, hasExplicitComputerScreenshotTarget, ocrScreenshotResult } from './desktop-vision.mjs';
17
+
18
+ export async function computerSnapshot(args = {}) {
19
+ const desktopAvailable = desktopSessionAvailable();
20
+ const screenshotRequested = args.include_screenshot !== false || args.include_ocr === true;
21
+ const captureScreenshot = desktopAvailable && screenshotRequested;
22
+ const includeUi = args.include_ui !== false;
23
+ const [windowsResult, displaysResult, uiResult, clipboardResult, cursorResult, shotResult] = await Promise.allSettled([
24
+ desktopAvailable ? listWindows() : Promise.resolve(null),
25
+ desktopAvailable ? displayInventory() : Promise.resolve(null),
26
+ includeUi && desktopAvailable
27
+ ? uiSnapshot({
28
+ max_nodes:args.max_ui_nodes || 800,
29
+ max_depth:args.max_ui_depth || 12,
30
+ semantic_tree:args.semantic_tree !== false,
31
+ active_only:args.ui_scope === 'desktop' ? false : true,
32
+ ...(args.ui_browser_dom === true ? { browser_dom:true } : {}),
33
+ })
34
+ : Promise.resolve(null),
35
+ desktopAvailable ? clipboard({ action:'read' }) : Promise.resolve(null),
36
+ desktopAvailable ? cursorPosition() : Promise.resolve(null),
37
+ captureScreenshot ? computerScreenshotTask(args) : Promise.resolve(null),
38
+ ]);
39
+ const parse = (result, fallback = null) => {
40
+ if (result.status !== 'fulfilled' || !result.value || result.value.isError === true) return fallback;
41
+ return resultValue(result.value) ?? fallback;
42
+ };
43
+ const shapeErrors = [];
44
+ const parsedWindows = parse(windowsResult, []);
45
+ const windows = Array.isArray(parsedWindows) ? parsedWindows : [];
46
+ if (parsedWindows !== windows && parsedWindows != null) {
47
+ shapeErrors.push({ source:'windows', message:'window inventory returned an invalid result shape' });
48
+ }
49
+ const parsedDisplays = parse(displaysResult, []);
50
+ const displays = Array.isArray(parsedDisplays) ? parsedDisplays : [];
51
+ if (parsedDisplays !== displays && parsedDisplays != null) {
52
+ shapeErrors.push({ source:'displays', message:'display inventory returned an invalid result shape' });
53
+ }
54
+ const parsedCursor = parse(cursorResult, { x:null, y:null });
55
+ const cursor = objectRecord(parsedCursor) || { x:null, y:null };
56
+ if (parsedCursor !== cursor && parsedCursor != null) {
57
+ shapeErrors.push({ source:'cursor', message:'cursor position returned an invalid result shape' });
58
+ }
59
+ const parsedUi = includeUi ? parse(uiResult, null) : null;
60
+ const ui = objectRecord(parsedUi) || { platform:process.platform, count:0, nodes:[], ...(includeUi ? { unavailable:true } : { skipped:true }) };
61
+ if (includeUi && parsedUi !== ui && parsedUi != null) {
62
+ shapeErrors.push({ source:'ui', message:'accessibility snapshot returned an invalid result shape' });
63
+ }
64
+ const clipboardOk = desktopAvailable && clipboardResult.status === 'fulfilled' && clipboardResult.value?.isError !== true && Boolean(clipboardResult.value);
65
+ const clipText = clipboardOk ? clipboardResult.value.content?.[0]?.text || '' : '';
66
+ const uiNodes = Array.isArray(ui?.nodes) ? ui.nodes : Array.isArray(ui) ? ui : [];
67
+ const focusedNodes = uiNodes.filter(node => node.focused === true);
68
+ const focusScore = node => {
69
+ const app = String(node.app || '').toLowerCase();
70
+ const windowName = String(node.window || '').trim();
71
+ const role = String(node.role || '').toLowerCase();
72
+ let score = 0;
73
+ if (windowName) score += 100;
74
+ if (app && app !== 'gnome-shell') score += 40;
75
+ if (node.name) score += 15;
76
+ if (!['window','filler','panel'].includes(role)) score += 15;
77
+ if (Number(node.width) >= 100 && Number(node.height) >= 100) score += 10;
78
+ if (/desktop icons/i.test(windowName)) score -= 80;
79
+ if (app === 'gnome-shell' && !windowName) score -= 120;
80
+ return score;
81
+ };
82
+ const focusedNode = focusedNodes.sort((a, b) => focusScore(b) - focusScore(a))[0] || null;
83
+ const activeTopLevel = uiNodes.find(node =>
84
+ node.active === true
85
+ && /frame|window|dialog/i.test(String(node.role || ''))
86
+ && String(node.window || node.name || '').trim()
87
+ && !/desktop icons/i.test(String(node.window || node.name || ''))
88
+ ) || null;
89
+ const activeNode = activeTopLevel
90
+ || focusedNode
91
+ || uiNodes.find(node => /frame|window|dialog/i.test(String(node.role || '')) && String(node.window || node.name || '').trim())
92
+ || uiNodes[0]
93
+ || null;
94
+ const windowRows = Array.isArray(windows) ? windows : [];
95
+ const activeWindow = activeNode ? windowRows.find(row => {
96
+ const activePid = Number(activeNode.pid);
97
+ const rowPid = Number(row.pid);
98
+ const wantedTitle = String(activeNode.window || activeNode.name || '').trim().toLowerCase();
99
+ const rowTitle = String(row.title || '').trim().toLowerCase();
100
+ const pidMatch = activePid > 0 && rowPid > 0 && rowPid === activePid;
101
+ // XWayland/WM enumeration can report pid=0 while AT-SPI still exposes the real application PID.
102
+ // A strong top-level title match is the safe fallback for joining those two views.
103
+ const titleMatch = Boolean(wantedTitle && rowTitle && (rowTitle === wantedTitle || rowTitle.includes(wantedTitle) || wantedTitle.includes(rowTitle)));
104
+ return (pidMatch || titleMatch) && (!wantedTitle || titleMatch || pidMatch);
105
+ }) || null : null;
106
+ const activeApp = activeNode ? {
107
+ pid: Number(activeNode.pid) || activeWindow?.pid || null,
108
+ app: activeNode.app || activeWindow?.app || null,
109
+ window: activeNode.window || activeWindow?.title || null,
110
+ } : null;
111
+
112
+ const activeLabel = [activeApp?.app, activeApp?.window, activeWindow?.app, activeWindow?.title].filter(Boolean).join(' ');
113
+ const browserRequested = args.include_browser === true
114
+ || (args.include_browser !== false && /chrome|chromium|edge|brave/i.test(activeLabel));
115
+ let browser = null;
116
+ let browserError = null;
117
+ if (browserRequested && await browserCapabilityAvailable(undefined, 400)) {
118
+ const hint = browserTitleHint(activeWindow);
119
+ try {
120
+ const browserResult = await browserSnapshot({
121
+ ...(hint ? { title:hint } : {}),
122
+ max_nodes:clamp(args.browser_max_nodes, 500, 1, 2000),
123
+ timeout_ms:3000,
124
+ });
125
+ if (browserResult?.isError === true) throw new Error(toolResultMessage(browserResult));
126
+ browser = objectRecord(resultValue(browserResult));
127
+ if (!browser) throw new Error('browser snapshot returned an invalid result shape');
128
+ } catch (error) {
129
+ if (hint) {
130
+ try {
131
+ const fallbackResult = await browserSnapshot({
132
+ max_nodes:clamp(args.browser_max_nodes, 500, 1, 2000),
133
+ timeout_ms:3000,
134
+ });
135
+ if (fallbackResult?.isError === true) throw new Error(toolResultMessage(fallbackResult));
136
+ browser = objectRecord(resultValue(fallbackResult));
137
+ if (!browser) throw new Error('browser snapshot returned an invalid result shape');
138
+ } catch (fallbackError) {
139
+ browserError = String(fallbackError?.message || fallbackError);
140
+ }
141
+ } else {
142
+ browserError = String(error?.message || error);
143
+ }
144
+ }
145
+ }
146
+
147
+ const ocrRequested = args.include_ocr === true
148
+ || (desktopAvailable && args.include_ocr !== false && screenshotRequested && uiNodes.length === 0 && !browser);
149
+ const shouldOcr = desktopAvailable && ocrRequested;
150
+ let ocr = null;
151
+ let ocrCaptureBounds = null;
152
+ let ocrShotResult = shotResult;
153
+ // A whole-desktop OCR pass is noisy and slow when the active application is already known.
154
+ // Keep the normal screenshot contract unchanged, but use a second tightly scoped capture for OCR.
155
+ if (shouldOcr && !hasExplicitComputerScreenshotTarget(args)) {
156
+ const bounds = activeWindowCaptureBounds(activeWindow);
157
+ if (bounds) {
158
+ try {
159
+ const targeted = await screenshotRegion({ ...bounds, padding:0 });
160
+ ocrShotResult = { status:'fulfilled', value:targeted };
161
+ ocrCaptureBounds = bounds;
162
+ } catch {
163
+ // Fall back to the already captured desktop screenshot below.
164
+ }
165
+ }
166
+ }
167
+ if (shouldOcr) {
168
+ ocr = ocrShotResult.status === 'fulfilled' && ocrShotResult.value
169
+ ? await ocrScreenshotResult(ocrShotResult.value, args)
170
+ : { available:false, reason:'screenshot capture failed before OCR could run' };
171
+ if (ocr?.available && ocrCaptureBounds) {
172
+ ocr = { ...ocr, coordinate_space:'capture_pixels', capture_bounds:ocrCaptureBounds };
173
+ }
174
+ }
175
+
176
+ const payload = {
177
+ device: { hostname: os.hostname(), platform: process.platform, arch: process.arch },
178
+ platform: process.platform,
179
+ hostname: os.hostname(),
180
+ active_app: activeApp,
181
+ active_window: activeWindow,
182
+ windows,
183
+ displays,
184
+ cursor,
185
+ ui,
186
+ ...(browser ? { browser } : {}),
187
+ ...(ocr ? { ocr } : {}),
188
+ fallback_chain: {
189
+ accessibility: { requested:includeUi, available:includeUi && desktopAvailable && uiResult.status === 'fulfilled' && Boolean(uiResult.value), nodes:uiNodes.length },
190
+ native_browser_dom: {
191
+ requested:includeUi && args.ui_browser_dom === true,
192
+ available:Boolean(includeUi && ui?.browser_dom?.available),
193
+ provider:ui?.browser_dom?.provider || null,
194
+ },
195
+ browser_dom: { requested:browserRequested, available:Boolean(browser) },
196
+ ocr: { requested:ocrRequested, available:Boolean(ocr?.available), backend:ocr?.backend || null },
197
+ vision_screenshot: { available:shotResult.status === 'fulfilled' && Boolean(shotResult.value?.content?.some(part => part.type === 'image')) },
198
+ },
199
+ visual: {
200
+ coordinate_space:'virtual_desktop',
201
+ labeled_targets:uiNodes.filter(node => Number.isInteger(node.label)).slice(0, 500).map(node => ({
202
+ label:node.label,
203
+ id:node.id || null,
204
+ role:node.role || null,
205
+ name:String(node.name || '').slice(0, 240),
206
+ x:Number(node.x) || 0,
207
+ y:Number(node.y) || 0,
208
+ width:Number(node.width) || 0,
209
+ height:Number(node.height) || 0,
210
+ action:node.suggested_action || suggestedUiAction(node),
211
+ })),
212
+ ...(Number(args.grid_columns) > 0 || Number(args.grid_rows) > 0 ? {
213
+ reference_grid:{
214
+ columns:clamp(args.grid_columns, 0, 0, 20),
215
+ rows:clamp(args.grid_rows, 0, 0, 20),
216
+ },
217
+ } : {}),
218
+ screenshot_scope:Array.isArray(args.screenshot_region) ? 'region'
219
+ : args.screenshot_window_id ? 'window'
220
+ : (args.screenshot_monitor != null || args.screenshot_monitor_index != null) ? 'monitor'
221
+ : 'desktop',
222
+ },
223
+ clipboard: { available:clipboardOk, length:clipText.length },
224
+ errors: [
225
+ ...[
226
+ ['windows', windowsResult],
227
+ ['displays', displaysResult],
228
+ ...(includeUi ? [['ui', uiResult]] : []),
229
+ ['clipboard', clipboardResult],
230
+ ['cursor', cursorResult],
231
+ ...(captureScreenshot ? [['screenshot', shotResult]] : []),
232
+ ].map(([source, result]) => snapshotError(source, result)).filter(Boolean),
233
+ ...shapeErrors,
234
+ ...(browserError ? [{ source:'browser', message:browserError.slice(0, 2000) }] : []),
235
+ ],
236
+ };
237
+ const boundedPayload = boundedComputerSnapshot(payload);
238
+ const parts = [{ type: 'text', text: JSON.stringify(boundedPayload, null, 2) }];
239
+ if (args.include_screenshot !== false && shotResult.status === 'fulfilled' && shotResult.value?.content) {
240
+ for (const part of shotResult.value.content) if (part.type === 'image') parts.push(part);
241
+ }
242
+ const result = multi(parts);
243
+ result.structuredContent = boundedPayload;
244
+ return result;
245
+ }