@remcp/runtime 0.2.130 → 0.2.132
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.
- package/CHANGELOG.md +15 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/extended/browser.mjs +91 -6
- package/src/extended/catalog.mjs +107 -17
- package/src/extended/desktop.mjs +79 -8
- package/src/extended/diagnostics.mjs +56 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.132 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
- `computer_action` now exposes `type`, `scroll`, `drag`, and `target=window` in its MCP schema while keeping specialist tools available when the backend is already known.
|
|
6
|
+
- Auto `type` uses accessibility-only semantic editing before CDP and never falls through to unverified physical text input merely because a native selector failed.
|
|
7
|
+
- Browser DOM typing separates `browser_text` locator semantics from the `text` payload; legacy locator-in-`text` calls remain compatible when `value`/`text_value` carries the payload.
|
|
8
|
+
- `browser_action scroll` performs bounded page or nearest-scrollable-ancestor scrolling with the same delta/direction vocabulary as native `scroll`; `computer_action scroll` can route to it automatically.
|
|
9
|
+
- Action-specific schemas validate type targets/text, scroll deltas/directions, drag endpoints, and explicit backend/target compatibility before handler execution. Window targets require a window selector; UI/coordinate scroll and drag require target-appropriate fields; browser/OCR drag is rejected before dispatch.
|
|
10
|
+
- macOS `service action=list` now uses explicit launchd domains so `scope=user` reads `gui/<uid>` and `scope=system` reads `system`; the old legacy `launchctl list` path could not honor system scope.
|
|
11
|
+
- Public macOS release CI exercises the system launchd domain in addition to document/network backends. Runtime tool names remain unchanged at 83.
|
|
12
|
+
|
|
13
|
+
## 0.2.131 — 2026-10-02
|
|
14
|
+
|
|
15
|
+
- `network action=summary` is now bounded for Docker-heavy hosts: non-container interfaces are prioritized, at most eight interfaces and four addresses per interface are returned, and count/truncation metadata shows what was omitted.
|
|
16
|
+
- `network action=interfaces` remains the complete adapter/address inventory. The MCP schema and tool names remain compatible; stable count/truncation fields are now declared for chaining.
|
|
17
|
+
|
|
3
18
|
## 0.2.130 — 2026-10-02
|
|
4
19
|
|
|
5
20
|
- `computer_snapshot` now skips desktop-only probes on headless Linux and returns stable empty/unavailable desktop fields without false error noise. The shared `desktopSessionAvailable()` helper keeps this behavior aligned with live capability discovery.
|
package/README.md
CHANGED
|
@@ -60,7 +60,9 @@ If custom widgets are explicitly re-enabled, hosted discovery expands to **15 de
|
|
|
60
60
|
**5 app-only** UI helpers. Cached legacy clients can still call supported granular aliases without inflating
|
|
61
61
|
discovery for new clients.
|
|
62
62
|
|
|
63
|
-
`computer_snapshot` and `computer_action` keep OCR inside the compact high-level surface: when a local `tesseract` binary is available, snapshot can return bounded OCR text/boxes and click targeting can fall back through Accessibility → browser DOM/CDP → OCR → coordinates. OCR is optional and never adds another MCP tool or bundled OCR dependency.
|
|
63
|
+
`computer_snapshot` and `computer_action` keep OCR inside the compact high-level surface: when a local `tesseract` binary is available, snapshot can return bounded OCR text/boxes and click targeting can fall back through Accessibility → browser DOM/CDP → OCR → coordinates. `computer_action` also exposes backend-uncertain `type`, `scroll`, and `drag` plus explicit window targeting; it prefers semantic Accessibility, can route browser selectors through CDP, and keeps native coordinate/UI gestures as the fallback. When the backend is already known, the specialist UI/browser/type/scroll/drag/window tools keep their full controls. OCR is optional and never adds another MCP tool or bundled OCR dependency.
|
|
64
|
+
|
|
65
|
+
For browser typing, `browser_text` is the visible DOM locator while `text` is the Unicode payload. `browser_action(action="scroll")` accepts page/element deltas or direction/wheel steps and scrolls the page or nearest scrollable ancestor; `scroll_into_view` remains the separate semantic reveal operation.
|
|
64
66
|
|
|
65
67
|
For frontend QA, use semantic data to operate the page and rendered pixels to verify it: `browser_action(action="set_viewport")` sets an exact responsive-test viewport, `browser_wait` waits for the real application state, and `browser_snapshot(include_screenshot=true, selector=...)` recenters the component, returns its bounds, and attaches the real CDP-rendered viewport PNG. This intentionally treats screenshots as the visual oracle without making screenshot-driven coordinate guessing the primary control path.
|
|
66
68
|
|
package/package.json
CHANGED
package/src/extended/browser.mjs
CHANGED
|
@@ -1045,6 +1045,47 @@ export async function browserFind(args) {
|
|
|
1045
1045
|
});
|
|
1046
1046
|
}
|
|
1047
1047
|
|
|
1048
|
+
const PAGE_SCROLL_ACTION = `function(selector, needle, deltaX, deltaY) {
|
|
1049
|
+
let target = selector ? document.querySelector(selector) : null;
|
|
1050
|
+
if (!target && needle) {
|
|
1051
|
+
const lower = String(needle).toLowerCase();
|
|
1052
|
+
target = Array.from(document.querySelectorAll('*')).find(node =>
|
|
1053
|
+
String(node.innerText || node.textContent || '').trim().toLowerCase().includes(lower)
|
|
1054
|
+
) || null;
|
|
1055
|
+
}
|
|
1056
|
+
if ((selector || needle) && !target) throw new Error('Element not found');
|
|
1057
|
+
|
|
1058
|
+
let scroller = document.scrollingElement || document.documentElement;
|
|
1059
|
+
if (target) {
|
|
1060
|
+
let current = target;
|
|
1061
|
+
while (current && current !== document.documentElement) {
|
|
1062
|
+
const style = getComputedStyle(current);
|
|
1063
|
+
const scrollX = /(auto|scroll|overlay)/.test(String(style.overflowX || '')) && current.scrollWidth > current.clientWidth;
|
|
1064
|
+
const scrollY = /(auto|scroll|overlay)/.test(String(style.overflowY || '')) && current.scrollHeight > current.clientHeight;
|
|
1065
|
+
if (scrollX || scrollY) {
|
|
1066
|
+
scroller = current;
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
current = current.parentElement;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const beforeX = Number(scroller.scrollLeft || 0);
|
|
1074
|
+
const beforeY = Number(scroller.scrollTop || 0);
|
|
1075
|
+
scroller.scrollLeft = beforeX + Number(deltaX || 0);
|
|
1076
|
+
scroller.scrollTop = beforeY + Number(deltaY || 0);
|
|
1077
|
+
return {
|
|
1078
|
+
target: target ? target.tagName.toLowerCase() : 'page',
|
|
1079
|
+
scroller: scroller === document.scrollingElement || scroller === document.documentElement ? 'page' : scroller.tagName.toLowerCase(),
|
|
1080
|
+
before_x: beforeX,
|
|
1081
|
+
before_y: beforeY,
|
|
1082
|
+
after_x: Number(scroller.scrollLeft || 0),
|
|
1083
|
+
after_y: Number(scroller.scrollTop || 0),
|
|
1084
|
+
delta_x: Number(deltaX || 0),
|
|
1085
|
+
delta_y: Number(deltaY || 0),
|
|
1086
|
+
};
|
|
1087
|
+
}`;
|
|
1088
|
+
|
|
1048
1089
|
const PAGE_ELEMENT_ACTION = `function(selector, needle, action, value) {
|
|
1049
1090
|
let el = selector ? document.querySelector(selector) : null;
|
|
1050
1091
|
if (!el && needle) {
|
|
@@ -1081,14 +1122,46 @@ const PAGE_ELEMENT_ACTION = `function(selector, needle, action, value) {
|
|
|
1081
1122
|
};
|
|
1082
1123
|
}`;
|
|
1083
1124
|
|
|
1125
|
+
export function browserActionLocatorText(args = {}, action = '') {
|
|
1126
|
+
const explicit = optionalString(args.browser_text || args.match_text);
|
|
1127
|
+
if (explicit) return explicit;
|
|
1128
|
+
if (action === 'type') {
|
|
1129
|
+
// Legacy type calls used text as the locator and value/text_value as the payload.
|
|
1130
|
+
if (!optionalString(args.selector) && (args.value != null || args.text_value != null)) return optionalString(args.text);
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
return optionalString(args.text);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1084
1136
|
export function browserActionInputValue(args = {}, action = '') {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1137
|
+
if (action === 'type') {
|
|
1138
|
+
if (!optionalString(args.selector) && !optionalString(args.browser_text || args.match_text) && (args.value != null || args.text_value != null)) {
|
|
1139
|
+
return String(args.text_value ?? args.value ?? '');
|
|
1140
|
+
}
|
|
1141
|
+
return String(args.text ?? args.text_value ?? args.value ?? '');
|
|
1142
|
+
}
|
|
1143
|
+
return String(args.value ?? args.text_value ?? args.text ?? '');
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
export function browserScrollDeltas(args = {}) {
|
|
1147
|
+
const direction = optionalString(args.direction);
|
|
1148
|
+
const hasDelta = [args.delta_x,args.delta_y,args.delta].some(value => value != null && Number.isFinite(Number(value)));
|
|
1149
|
+
if (!direction && !hasDelta) return null;
|
|
1150
|
+
const times = clamp(args.wheel_times, 1, 1, 50);
|
|
1151
|
+
let deltaX = Number(args.delta_x || 0);
|
|
1152
|
+
let deltaY = Number(args.delta_y ?? args.delta ?? 0);
|
|
1153
|
+
if (direction) {
|
|
1154
|
+
const normalized = requireEnum(direction, 'direction', ['up','down','left','right']);
|
|
1155
|
+
if (normalized === 'up') deltaY = -120 * times;
|
|
1156
|
+
if (normalized === 'down') deltaY = 120 * times;
|
|
1157
|
+
if (normalized === 'left') deltaX = -120 * times;
|
|
1158
|
+
if (normalized === 'right') deltaX = 120 * times;
|
|
1159
|
+
}
|
|
1160
|
+
return { delta_x:deltaX, delta_y:deltaY };
|
|
1088
1161
|
}
|
|
1089
1162
|
|
|
1090
1163
|
export async function browserAction(args) {
|
|
1091
|
-
const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll_into_view', 'upload', 'press', 'set_viewport']);
|
|
1164
|
+
const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll', 'scroll_into_view', 'upload', 'press', 'set_viewport']);
|
|
1092
1165
|
return withTarget(args, async (session, target) => {
|
|
1093
1166
|
if (action === 'set_viewport') {
|
|
1094
1167
|
const width = clamp(args.width, 1280, 200, 8192);
|
|
@@ -1126,11 +1199,22 @@ export async function browserAction(args) {
|
|
|
1126
1199
|
return jsonResult({ target_id: target.id, action, key });
|
|
1127
1200
|
}
|
|
1128
1201
|
const selector = optionalString(args.selector);
|
|
1129
|
-
const needle =
|
|
1130
|
-
if (!selector && !needle) throw new Error('selector or text is required');
|
|
1202
|
+
const needle = browserActionLocatorText(args, action);
|
|
1131
1203
|
const value = browserActionInputValue(args, action);
|
|
1132
1204
|
const option = String(args.option ?? value);
|
|
1133
1205
|
|
|
1206
|
+
if (action === 'scroll') {
|
|
1207
|
+
const deltas = browserScrollDeltas(args);
|
|
1208
|
+
if (!deltas) throw new Error('browser_action scroll requires direction or a delta');
|
|
1209
|
+
return jsonResult({
|
|
1210
|
+
target_id: target.id,
|
|
1211
|
+
action,
|
|
1212
|
+
result: await callPageFunction(session, PAGE_SCROLL_ACTION, [selector, needle, deltas.delta_x, deltas.delta_y]),
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (!selector && !needle) throw new Error('selector or browser_text/text locator is required');
|
|
1217
|
+
|
|
1134
1218
|
if (action === 'click') {
|
|
1135
1219
|
const rect = await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'click', '']);
|
|
1136
1220
|
const x = Number(rect.x) + Number(rect.width) / 2;
|
|
@@ -1143,6 +1227,7 @@ export async function browserAction(args) {
|
|
|
1143
1227
|
|
|
1144
1228
|
if (action === 'type') {
|
|
1145
1229
|
const focused = await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'type', '']);
|
|
1230
|
+
if (args.clear === true) await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'set_value', '']);
|
|
1146
1231
|
await session.send('Input.insertText', { text:value });
|
|
1147
1232
|
const current = await callPageFunction(session, PAGE_ELEMENT_ACTION, [selector, needle, 'read_value', '']);
|
|
1148
1233
|
return jsonResult({ target_id:target.id, action, result:{ ...focused, value:current.value } });
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -30,7 +30,7 @@ const o = (properties = {}, required = []) => ({ type: 'object', properties, ...
|
|
|
30
30
|
|
|
31
31
|
const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
32
32
|
computer_snapshot: 'Use this for the first look at an unfamiliar desktop state when you need one compact bundle of windows, semantic UI, displays, clipboard metadata and optional pixels/browser/OCR. Prefer it over repeated screenshots; narrow later with ui_find, browser_find or screenshot_region.',
|
|
33
|
-
computer_action: 'Use this only when one semantic click/
|
|
33
|
+
computer_action: 'Use this only when one semantic click/type/invoke/select/focus/scroll/drag action must work across an uncertain target backend and may need Accessibility → browser DOM/CDP → OCR → coordinate fallback, or when multi_select/multi_edit avoids many identical native UI calls. When the backend is already known, prefer the specialist tool (ui_action, browser_action, type_text, drag_drop, scroll or window_action) for its full controls. Keep keyboard, clipboard, launch/open/reveal/notification and raw pointer gestures in their dedicated tools.',
|
|
34
34
|
list_windows: 'Use this to discover top-level desktop windows or obtain a stable window id/PID/bounds before window_action, screenshot_region or targeted input. Use ui_snapshot instead when you need controls inside a window; prefer list_windows over screenshot-based window guessing.',
|
|
35
35
|
window_action: 'Use this to focus, minimize, maximize, restore, move, resize or close one known top-level window. Select it by id/PID/app/title from list_windows. Use ui_action for controls inside the window; do not use pointer coordinates for window management when this semantic tool can express the action.',
|
|
36
36
|
launch_app: 'Use this to start a desktop application by executable/path or friendly application name without a shell. Prefer it over start_process for GUI application launch. If the user wants to open a specific file or directory in its associated application, use open_path instead; use start_process for terminal commands, build tools and shell pipelines.',
|
|
@@ -58,7 +58,7 @@ const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
58
58
|
browser_evaluate: 'Use this as the powerful browser escape hatch only when browser_snapshot/find/action/wait cannot express the required page operation or inspection. JavaScript executes in page context and page content is untrusted.',
|
|
59
59
|
service: 'Use this for operating-system service inventory/status/start/stop/restart. Prefer status before a mutation when service state matters; use start_process for ordinary commands that are not service-manager operations.',
|
|
60
60
|
event_log: 'Use this to read bounded recent OS logs (Windows Event Log, macOS unified log or systemd journal) when diagnosing a service/app/system problem. Prefer it over an unbounded shell log dump.',
|
|
61
|
-
network: 'Use this for
|
|
61
|
+
network: 'Use this for a compact network summary, full interface inventory, DNS/routes/listeners inspection or a bounded TCP connectivity test. Prefer action=summary for orientation and action=interfaces only when the complete adapter/address inventory is required; prefer this tool over shell ip/netstat/nslookup commands for routine diagnostics.',
|
|
62
62
|
installed_apps: 'Use this to answer what software/packages are installed and their versions. Prefer a filter when looking for one product instead of returning the full inventory.',
|
|
63
63
|
environment: 'Use this for PATH, shell, Node/runtime environment paths, architecture and optionally sanitized environment variables. Use get_system_info for host health such as CPU/memory/disk/uptime, and get_runtime_info for ReMCP policy, roots and limits. Secret-looking values remain masked.',
|
|
64
64
|
audio: 'Use this to read or change the default output volume/mute state. Use action=status for inspection; set_volume requires a 0–100 value.',
|
|
@@ -211,7 +211,7 @@ const OUTPUT_FIELDS = Object.freeze({
|
|
|
211
211
|
browser_evaluate: { target_id:outputString(), value:{} },
|
|
212
212
|
service: { data:outputArray(outputObject('Service record.')), action:outputString(), name:outputString(), text:outputString() },
|
|
213
213
|
event_log: { text:outputString('Bounded log output.'), data:outputArray(outputObject()) },
|
|
214
|
-
network: { hostname:outputString(), interfaces:outputObject(), dns:outputArray(outputString()), servers:outputArray(outputString()), host:outputString(), port:outputNumber(), ok:outputBoolean(), latency_ms:outputNumber(), data:outputArray(outputObject()) },
|
|
214
|
+
network: { hostname:outputString(), interfaces:outputObject(), dns:outputArray(outputString()), servers:outputArray(outputString()), host:outputString(), port:outputNumber(), ok:outputBoolean(), latency_ms:outputNumber(), interface_count:outputNumber(), interface_count_returned:outputNumber(), address_count:outputNumber(), address_count_returned:outputNumber(), interfaces_truncated:outputBoolean(), addresses_truncated:outputBoolean(), data:outputArray(outputObject()) },
|
|
215
215
|
installed_apps: { data:outputArray(outputObject('Installed app/package with name/version/path where available.')) },
|
|
216
216
|
environment: { platform:outputString(), arch:outputString(), hostname:outputString(), release:outputString(), shell:outputString(), path:outputString(), node:outputString(), env:outputObject() },
|
|
217
217
|
audio: { volume:outputNumber(), muted:outputBoolean(), action:outputString(), text:outputString() },
|
|
@@ -247,8 +247,18 @@ function requiredAny(...groups) {
|
|
|
247
247
|
function applySchemaRules(name, schema) {
|
|
248
248
|
const next = structuredClone(schema);
|
|
249
249
|
if (name === 'computer_action') {
|
|
250
|
-
const
|
|
250
|
+
const nativeTarget = requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']);
|
|
251
|
+
const browserElementTarget = requiredAny(['selector'], ['browser_text'], ['name']);
|
|
252
|
+
const windowTarget = requiredAny(['window_id'], ['pid'], ['app'], ['window_title'], ['title']);
|
|
253
|
+
const semanticTarget = [...nativeTarget, ...browserElementTarget];
|
|
251
254
|
next.allOf = [
|
|
255
|
+
{
|
|
256
|
+
if:{ properties:{ target:{ const:'window' } }, required:['target'] },
|
|
257
|
+
then:{
|
|
258
|
+
properties:{ action:{ enum:['type','focus','scroll','drag'] } },
|
|
259
|
+
anyOf:windowTarget,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
252
262
|
{
|
|
253
263
|
if:{ properties:{ action:{ const:'click' } }, required:['action'] },
|
|
254
264
|
then:{ anyOf:[...semanticTarget, { required:['ocr_text'] }, { required:['x','y'] }] },
|
|
@@ -259,7 +269,60 @@ function applySchemaRules(name, schema) {
|
|
|
259
269
|
},
|
|
260
270
|
{
|
|
261
271
|
if:{ properties:{ action:{ const:'focus' } }, required:['action'] },
|
|
262
|
-
then:{ anyOf:[...semanticTarget, ...requiredAny(['pid'], ['app'], ['window_title'], ['title'])] },
|
|
272
|
+
then:{ anyOf:[...semanticTarget, ...requiredAny(['window_id'], ['pid'], ['app'], ['window_title'], ['title'])] },
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
if:{ properties:{ action:{ const:'type' } }, required:['action'] },
|
|
276
|
+
then:{
|
|
277
|
+
required:['text'],
|
|
278
|
+
properties:{ target:{ enum:['auto','ui','ui_element','browser','window'] } },
|
|
279
|
+
allOf:[
|
|
280
|
+
{ anyOf:[...semanticTarget, ...windowTarget] },
|
|
281
|
+
{
|
|
282
|
+
if:{ properties:{ target:{ enum:['ui','ui_element'] } }, required:['target'] },
|
|
283
|
+
then:{ anyOf:nativeTarget },
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
if:{ properties:{ target:{ const:'browser' } }, required:['target'] },
|
|
287
|
+
then:{ anyOf:browserElementTarget },
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
if:{ properties:{ action:{ const:'scroll' } }, required:['action'] },
|
|
294
|
+
then:{
|
|
295
|
+
properties:{ target:{ enum:['auto','ui','ui_element','browser','coordinates','window'] } },
|
|
296
|
+
allOf:[
|
|
297
|
+
{ anyOf:requiredAny(['delta_x'], ['delta_y'], ['delta'], ['direction']) },
|
|
298
|
+
{
|
|
299
|
+
if:{ properties:{ target:{ enum:['ui','ui_element'] } }, required:['target'] },
|
|
300
|
+
then:{ anyOf:nativeTarget },
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
if:{ properties:{ target:{ const:'coordinates' } }, required:['target'] },
|
|
304
|
+
then:{ required:['x','y'] },
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
if:{ properties:{ action:{ const:'drag' } }, required:['action'] },
|
|
311
|
+
then:{
|
|
312
|
+
properties:{ target:{ enum:['auto','ui','ui_element','coordinates','window'] } },
|
|
313
|
+
allOf:[
|
|
314
|
+
{ anyOf:requiredAny(['from_id'], ['from_x','from_y']) },
|
|
315
|
+
{ anyOf:requiredAny(['to_id'], ['to_x','to_y']) },
|
|
316
|
+
{
|
|
317
|
+
if:{ properties:{ target:{ enum:['ui','ui_element'] } }, required:['target'] },
|
|
318
|
+
then:{ required:['from_id','to_id'] },
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
if:{ properties:{ target:{ const:'coordinates' } }, required:['target'] },
|
|
322
|
+
then:{ required:['from_x','from_y','to_x','to_y'] },
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
263
326
|
},
|
|
264
327
|
{
|
|
265
328
|
if:{ properties:{ action:{ enum:['set_value','set_range_value'] } }, required:['action'] },
|
|
@@ -357,8 +420,19 @@ function applySchemaRules(name, schema) {
|
|
|
357
420
|
} else if (name === 'browser_action') {
|
|
358
421
|
next.allOf = [
|
|
359
422
|
{
|
|
360
|
-
if:{ properties:{ action:{ enum:['click','focus','
|
|
361
|
-
then:{ anyOf:requiredAny(['selector'], ['text']) },
|
|
423
|
+
if:{ properties:{ action:{ enum:['click','focus','set_value','select','scroll_into_view'] } }, required:['action'] },
|
|
424
|
+
then:{ anyOf:requiredAny(['selector'], ['text'], ['browser_text']) },
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
if:{ properties:{ action:{ const:'type' } }, required:['action'] },
|
|
428
|
+
then:{
|
|
429
|
+
required:['text'],
|
|
430
|
+
anyOf:requiredAny(['selector'], ['browser_text'], ['value'], ['text_value']),
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
if:{ properties:{ action:{ const:'scroll' } }, required:['action'] },
|
|
435
|
+
then:{ anyOf:requiredAny(['delta_x'], ['delta_y'], ['delta'], ['direction']) },
|
|
362
436
|
},
|
|
363
437
|
{
|
|
364
438
|
if:{ properties:{ action:{ const:'upload' } }, required:['action'] },
|
|
@@ -477,22 +551,38 @@ export const extendedToolDefinitions = [
|
|
|
477
551
|
'Perform one semantic cross-backend action when the target may require Accessibility → browser DOM/CDP → OCR → coordinate fallback.',
|
|
478
552
|
o({
|
|
479
553
|
action:e([
|
|
480
|
-
'click','invoke','focus','
|
|
554
|
+
'click','type','invoke','focus','select','scroll','drag','set_value','toggle','expand','collapse',
|
|
481
555
|
'scroll_into_view','set_range_value','add_to_selection','remove_from_selection',
|
|
482
556
|
'multi_select','multi_edit',
|
|
483
557
|
]),
|
|
484
558
|
target:e(
|
|
485
|
-
['auto','ui','ui_element','browser','ocr','coordinates'],
|
|
486
|
-
'Optional target type. auto
|
|
559
|
+
['auto','ui','ui_element','browser','ocr','coordinates','window'],
|
|
560
|
+
'Optional target type. auto prefers native accessibility, then browser DOM/CDP, then OCR/coordinates where supported. Use window for explicit top-level window focus/text/scroll targeting.',
|
|
487
561
|
),
|
|
488
562
|
...uiSelector,
|
|
489
|
-
title:s('Optional browser page title substring when target=
|
|
490
|
-
|
|
563
|
+
title:s('Optional browser page title substring or top-level window title when target=window.'),
|
|
564
|
+
window_id:s('Top-level window id returned by list_windows when target=window.'),
|
|
565
|
+
text:s('Unicode text to enter for action=type, or text used by multi_edit.'),
|
|
566
|
+
clear:b('For action=type, clear the current semantic/browser value before inserting text when supported.'),
|
|
491
567
|
value:s('New value for set_value, set_range_value, or select.'),
|
|
492
|
-
x:n('Virtual-desktop X coordinate used
|
|
493
|
-
y:n('Virtual-desktop Y coordinate used
|
|
568
|
+
x:n('Virtual-desktop X coordinate used by click/scroll coordinate targeting.'),
|
|
569
|
+
y:n('Virtual-desktop Y coordinate used by click/scroll coordinate targeting.'),
|
|
494
570
|
selector:s('CSS selector when the fallback target is a browser element.'),
|
|
495
|
-
browser_text:s('Visible browser text used to locate a DOM
|
|
571
|
+
browser_text:s('Visible browser text used only to locate a DOM target; for action=type, text remains the value to enter.'),
|
|
572
|
+
delta_x:n('Horizontal scroll delta; positive means right.'),
|
|
573
|
+
delta_y:n('Vertical scroll delta; positive means down.'),
|
|
574
|
+
delta:n('Alias for delta_y.'),
|
|
575
|
+
direction:e(['up','down','left','right']),
|
|
576
|
+
wheel_times:n('Number of 120-unit wheel steps for direction; 1..50.'),
|
|
577
|
+
from_x:n('Drag source X coordinate.'),
|
|
578
|
+
from_y:n('Drag source Y coordinate.'),
|
|
579
|
+
to_x:n('Drag destination X coordinate.'),
|
|
580
|
+
to_y:n('Drag destination Y coordinate.'),
|
|
581
|
+
from_id:s('Source semantic UI element id for action=drag.'),
|
|
582
|
+
to_id:s('Destination semantic UI element id for action=drag.'),
|
|
583
|
+
button:e(['left','right','middle']),
|
|
584
|
+
hold_ms:n('Delay after pointer down before moving during action=drag.'),
|
|
585
|
+
duration_ms:n('Drag movement duration.'),
|
|
496
586
|
ocr_text:s('Visible text to find in rendered pixels for the OCR click fallback.'),
|
|
497
587
|
ocr_region:{
|
|
498
588
|
type:'array',
|
|
@@ -556,13 +646,13 @@ export const extendedToolDefinitions = [
|
|
|
556
646
|
define('browser_navigate', 'Browser navigate', 'Open a new debuggable tab or navigate a selected browser page by URL, history back/forward, or reload through Chrome DevTools Protocol.', o({ ...browserTarget, action:e(['url','new_tab','back','forward','reload'], 'Default url when url is provided, otherwise reload. new_tab creates the first page target when needed.'), url:s('Destination URL for action=url or action=new_tab.'), wait:b('Wait for the page load event; default true.'), ignore_cache:b('Reload without cache when action=reload.') }), openMutatingNonDestructive, browserHandlers.browser_navigate, ['browser_cdp']),
|
|
557
647
|
define('browser_snapshot', 'Browser snapshot', 'Return the selected browser page accessibility tree through Chrome DevTools Protocol and, when requested, attach a real rendered viewport PNG for visual verification. A selector is temporarily recentered for capture, its exact viewport bounds are returned, and the prior scroll position is restored before return.', o({ ...browserTarget, max_nodes:n('Maximum AX nodes; default 1500.'), include_screenshot:b('Attach a real CDP-rendered viewport PNG screenshot; default false.'), selector:s('When include_screenshot=true, center this CSS-selected element before capture and report its viewport bounds.') }), readOnlyLive, browserHandlers.browser_snapshot, ['browser_cdp']),
|
|
558
648
|
define('browser_find', 'Find browser element', 'Find visible DOM elements by CSS selector, text, or ARIA role and return reusable selectors plus text, value, and bounding boxes.', o({ ...browserTarget, selector:s('CSS selector.'), text:s('Visible text substring.'), role:s('ARIA role.'), limit:n() }), readOnlyLive, browserHandlers.browser_find, ['browser_cdp']),
|
|
559
|
-
define('browser_action', 'Browser element action', 'Click, focus, type/set a value, select, scroll
|
|
649
|
+
define('browser_action', 'Browser element action', 'Click, focus, type/set a value, select, scroll a page or scrollable element, scroll an element into view, upload files, press a key, or set an exact responsive-test viewport in a debuggable browser page using DOM/CDP semantics.', o({ ...browserTarget, action:e(['click','focus','type','set_value','select','scroll','scroll_into_view','upload','press','set_viewport']), selector:s('CSS selector for the target element; prefer the reusable selector returned by browser_find.'), browser_text:s('Visible text used only to locate a DOM element; use this instead of overloading text for action=type.'), text:s('For action=type, Unicode text to insert. For non-type element actions without selector/browser_text, visible text may identify the target.'), clear:b('For action=type, clear the current value before inserting text.'), value:s('Value for action=set_value; also accepted as a legacy type payload when text is used as the locator.'), text_value:s('Compatibility alias for the type/set_value text value.'), option:s(), delta_x:n('Horizontal scroll delta; positive means right.'), delta_y:n('Vertical scroll delta; positive means down.'), delta:n('Alias for delta_y.'), direction:e(['up','down','left','right']), wheel_times:n('Number of 120-unit wheel steps for direction; 1..50.'), path:s(), paths:{type:'array',items:{type:'string'}}, key:s(), width:n('CSS viewport width for set_viewport.'), height:n('CSS viewport height for set_viewport.'), device_scale_factor:n('Device scale factor for set_viewport; default 1.'), mobile:b('Enable mobile emulation for set_viewport.') }, ['action']), openMutating, browserHandlers.browser_action, ['browser_cdp']),
|
|
560
650
|
define('browser_wait', 'Wait for browser', 'Wait for DOM state, page text, URL, completed loading, a navigation away from the current URL, or a short network-idle period in a debuggable browser page. Use browser_evaluate separately for JavaScript predicates or page-specific inspection.', o({ ...browserTarget, condition:e(['selector','text','url_contains','load','navigation','network_idle']), selector:s(), text:s(), value:s(), poll_ms:n(), idle_ms:n('Required zero-in-flight network quiet window for network_idle; default 500 ms.') }), readOnlyLive, browserHandlers.browser_wait, ['browser_cdp']),
|
|
561
651
|
define('browser_evaluate', 'Evaluate browser JavaScript', 'Evaluate JavaScript in a selected debuggable browser page and return its serializable value. This is a powerful escape hatch for page-specific automation.', o({ ...browserTarget, expression:s('JavaScript expression.'), await_promise:b('Await a returned Promise; default true.') }, ['expression']), openMutating, browserHandlers.browser_evaluate, ['browser_evaluate']),
|
|
562
652
|
|
|
563
653
|
define('service', 'Service control', 'List, inspect, start, stop, or restart operating-system services using Service Control Manager, launchctl, or systemd. Mutating actions pass through ReMCP command policy.', o({ action:e(['list','status','start','stop','restart']), name:s('Service/unit/launchd label.'), scope:e(['user','system']) }, ['action']), mutating, diagnosticHandlers.service, ['services']),
|
|
564
654
|
define('event_log', 'Event logs', 'Read recent Windows Event Log, macOS unified log, or systemd journal entries with bounded time and result limits.', o({ log:s('Windows log name.'), since:s('Time window such as 30s, 10m, 2h, or 1d.'), filter:s(), query:s(), limit:n() }), readOnlyLive, diagnosticHandlers.event_log, ['logs']),
|
|
565
|
-
define('network', 'Network diagnostics', 'Inspect
|
|
655
|
+
define('network', 'Network diagnostics', 'Inspect a bounded network summary, full interface inventory, DNS servers, routes and listening sockets, or perform a bounded TCP connectivity test to a host and port. Use action=interfaces when the complete adapter/address inventory is needed.', o({ action:e(['summary','interfaces','dns','routes','listeners','test']), host:s(), port:n(), timeout_ms:n() }), readOnlyOpen, diagnosticHandlers.network),
|
|
566
656
|
define('installed_apps', 'Installed applications', 'Inventory installed applications or packages with versions and publishers where available using native package/application inventories.', o({ filter:s('Name substring.'), limit:n('Maximum results; default 1000.') }), readOnly, diagnosticHandlers.installed_apps, ['apps']),
|
|
567
657
|
define('environment', 'Runtime environment', 'Report platform, architecture, Node version, working/home/temp paths, shell, PATH, and optionally environment variables. Environment variables are opt-in and secret-looking or credential-bearing values are masked.', o({ include_env:b('Include sanitized environment variables; default false.') }), readOnly, diagnosticHandlers.environment),
|
|
568
658
|
define('audio', 'Audio control', 'Read or change default output volume or mute state using native operating-system audio controls when available.', o({ action:e(['status','set_volume','mute','unmute']), volume:n('0..100 for set_volume.') }, ['action']), mutatingNonDestructive, diagnosticHandlers.audio, ['audio']),
|
package/src/extended/desktop.mjs
CHANGED
|
@@ -1263,6 +1263,7 @@ function browserComputerAction(args, action) {
|
|
|
1263
1263
|
set_value:'set_value',
|
|
1264
1264
|
select:'select',
|
|
1265
1265
|
type:'type',
|
|
1266
|
+
scroll:'scroll',
|
|
1266
1267
|
}[action];
|
|
1267
1268
|
if (!mapped) throw new Error(`Browser target does not support computer_action action: ${action}`);
|
|
1268
1269
|
const visibleText = optionalString(args.browser_text)
|
|
@@ -1276,10 +1277,17 @@ function browserComputerAction(args, action) {
|
|
|
1276
1277
|
timeout_ms: args.timeout_ms,
|
|
1277
1278
|
action: mapped,
|
|
1278
1279
|
selector: args.selector,
|
|
1279
|
-
...(visibleText ? {
|
|
1280
|
+
...(visibleText ? { browser_text: visibleText } : {}),
|
|
1280
1281
|
...(args.value != null ? { value: args.value } : {}),
|
|
1281
1282
|
...(args.option != null ? { option: args.option } : {}),
|
|
1282
|
-
...(action === 'type' && args.text != null ? {
|
|
1283
|
+
...(action === 'type' && args.text != null ? { text: args.text, clear:args.clear === true } : {}),
|
|
1284
|
+
...(action === 'scroll' ? {
|
|
1285
|
+
delta_x:args.delta_x,
|
|
1286
|
+
delta_y:args.delta_y,
|
|
1287
|
+
delta:args.delta,
|
|
1288
|
+
direction:args.direction,
|
|
1289
|
+
wheel_times:args.wheel_times,
|
|
1290
|
+
} : {}),
|
|
1283
1291
|
});
|
|
1284
1292
|
}
|
|
1285
1293
|
|
|
@@ -1362,21 +1370,84 @@ export async function computerAction(args = {}) {
|
|
|
1362
1370
|
if (action === 'ui') return uiAction(nested);
|
|
1363
1371
|
if (action === 'pointer') return pointer(nested);
|
|
1364
1372
|
if (action === 'window') return windowAction(nested);
|
|
1365
|
-
|
|
1373
|
+
|
|
1374
|
+
const windowTarget = () => ({
|
|
1375
|
+
...args,
|
|
1376
|
+
...(args.window_id ? { id:args.window_id } : {}),
|
|
1377
|
+
...(args.window_title && !args.title ? { title:args.window_title } : {}),
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
if (action === 'type') {
|
|
1381
|
+
if (target === 'browser') return browserComputerAction(args, 'type');
|
|
1382
|
+
if (target === 'ocr' || target === 'coordinates') {
|
|
1383
|
+
throw new Error('computer_action type requires a native UI, browser, or window target');
|
|
1384
|
+
}
|
|
1385
|
+
if (target === 'window') return typeText(windowTarget());
|
|
1386
|
+
if (!automatic) return typeText({ ...args, method:args.method || 'accessibility' });
|
|
1387
|
+
|
|
1388
|
+
const failures = [];
|
|
1389
|
+
if (hasUi) {
|
|
1390
|
+
try { return await typeText({ ...args, method:'accessibility' }); }
|
|
1391
|
+
catch (error) { failures.push(`accessibility: ${String(error?.message || error).slice(0, 300)}`); }
|
|
1392
|
+
}
|
|
1393
|
+
if (hasBrowser) {
|
|
1394
|
+
if (await browserCapabilityAvailable(args.endpoint, 300)) {
|
|
1395
|
+
try { return await browserComputerAction(args, 'type'); }
|
|
1396
|
+
catch (error) { failures.push(`browser DOM: ${String(error?.message || error).slice(0, 300)}`); }
|
|
1397
|
+
} else {
|
|
1398
|
+
failures.push('browser DOM: no local CDP endpoint is available');
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (args.window_id || args.pid != null || args.app || args.window_title || args.title) {
|
|
1402
|
+
try { return await typeText(windowTarget()); }
|
|
1403
|
+
catch (error) { failures.push(`window: ${String(error?.message || error).slice(0, 300)}`); }
|
|
1404
|
+
}
|
|
1405
|
+
if (failures.length) throw new Error(`computer_action type targeting failed: ${failures.join('; ')}`);
|
|
1406
|
+
throw new Error('computer_action type requires a native UI, browser, or window target');
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1366
1409
|
if (action === 'keyboard') {
|
|
1367
1410
|
if (target === 'browser' && args.key && !args.shortcut && !Array.isArray(args.keys)) {
|
|
1368
1411
|
return browserAction({ ...args, action:'press' });
|
|
1369
1412
|
}
|
|
1370
1413
|
return keyboard(args);
|
|
1371
1414
|
}
|
|
1415
|
+
|
|
1372
1416
|
if (action === 'scroll') {
|
|
1373
|
-
if (target === 'browser'
|
|
1374
|
-
|
|
1417
|
+
if (target === 'browser') return browserComputerAction(args, 'scroll');
|
|
1418
|
+
if (target === 'ocr') throw new Error('computer_action scroll does not support OCR targets; use browser/UI/window/coordinates');
|
|
1419
|
+
|
|
1420
|
+
if (automatic) {
|
|
1421
|
+
let uiError = null;
|
|
1422
|
+
if (hasUi) {
|
|
1423
|
+
try { return await scroll(args); }
|
|
1424
|
+
catch (error) { uiError = error; }
|
|
1425
|
+
}
|
|
1426
|
+
if (hasBrowser) {
|
|
1427
|
+
if (await browserCapabilityAvailable(args.endpoint, 300)) return browserComputerAction(args, 'scroll');
|
|
1428
|
+
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`);
|
|
1429
|
+
throw new Error('computer_action scroll browser target is unavailable because no local CDP endpoint is available');
|
|
1430
|
+
}
|
|
1431
|
+
if (args.window_id || args.pid != null || args.app || args.window_title || args.title) {
|
|
1432
|
+
await windowAction({ ...windowTarget(), action:'focus' });
|
|
1433
|
+
}
|
|
1434
|
+
return scroll(args);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if (target === 'window') {
|
|
1438
|
+
await windowAction({ ...windowTarget(), action:'focus' });
|
|
1439
|
+
return scroll(args);
|
|
1375
1440
|
}
|
|
1376
|
-
if (args.id || args.name || args.role || args.automation_id) await uiAction({ ...args, action:'focus' }).catch(() => null);
|
|
1377
1441
|
return scroll(args);
|
|
1378
1442
|
}
|
|
1379
|
-
|
|
1443
|
+
|
|
1444
|
+
if (action === 'drag') {
|
|
1445
|
+
if (target === 'browser' || target === 'ocr') {
|
|
1446
|
+
throw new Error('computer_action drag currently requires native UI element ids or desktop coordinates; browser/OCR drag is not advertised');
|
|
1447
|
+
}
|
|
1448
|
+
if (target === 'window') await windowAction({ ...windowTarget(), action:'focus' });
|
|
1449
|
+
return dragDrop(args);
|
|
1450
|
+
}
|
|
1380
1451
|
if (action === 'clipboard') return clipboard(nested);
|
|
1381
1452
|
if (action === 'launch_app') return launchApp(args);
|
|
1382
1453
|
if (action === 'open_path') return openPath(args);
|
|
@@ -1423,7 +1494,7 @@ export async function computerAction(args = {}) {
|
|
|
1423
1494
|
}
|
|
1424
1495
|
return uiAction({ ...args, action:'focus' });
|
|
1425
1496
|
}
|
|
1426
|
-
return windowAction({ ...
|
|
1497
|
+
return windowAction({ ...windowTarget(), action:'focus' });
|
|
1427
1498
|
}
|
|
1428
1499
|
|
|
1429
1500
|
if (['minimize','maximize','restore','move_resize','resize','close'].includes(action)) {
|
|
@@ -32,6 +32,11 @@ function durationSpec(value) {
|
|
|
32
32
|
return raw;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function macosLaunchctlDomain(scope = 'user', uid = process.getuid?.() ?? 0) {
|
|
36
|
+
const normalized = requireEnum(scope, 'scope', ['user','system']);
|
|
37
|
+
return normalized === 'system' ? 'system' : `gui/${uid}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
export async function serviceTool(args) {
|
|
36
41
|
const action = requireEnum(args.action || 'list', 'action', ['list','status','start','stop','restart']);
|
|
37
42
|
const name = optionalString(args.name);
|
|
@@ -55,8 +60,8 @@ export async function serviceTool(args) {
|
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
if (process.platform === 'darwin') {
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
const domain = macosLaunchctlDomain(scope);
|
|
64
|
+
if (action === 'list') return text((await runFile('/bin/launchctl', ['print', domain], { label: `launchctl print ${domain}` })).stdout);
|
|
60
65
|
const target = `${domain}/${name}`;
|
|
61
66
|
if (action === 'status') {
|
|
62
67
|
const result = await runFile('/bin/launchctl', ['print', target], { label: 'launchctl print', allowFailure: true });
|
|
@@ -133,11 +138,59 @@ export function linuxListenerBackend({ commandExistsFn = commandExists } = {}) {
|
|
|
133
138
|
return null;
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
const CONTAINER_INTERFACE_PATTERN = /^(?:veth|docker\d*$|br-[0-9a-f]{6,}$)/i;
|
|
142
|
+
|
|
143
|
+
export function compactNetworkSummary({
|
|
144
|
+
hostname = os.hostname(),
|
|
145
|
+
interfaces = os.networkInterfaces(),
|
|
146
|
+
dnsServers = dns.getServers(),
|
|
147
|
+
maxInterfaces = 8,
|
|
148
|
+
maxAddressesPerInterface = 4,
|
|
149
|
+
} = {}) {
|
|
150
|
+
const entries = Object.entries(interfaces || {}).map(([name, rows], index) => {
|
|
151
|
+
const normalizedRows = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
|
152
|
+
const hasExternal = normalizedRows.some(row => row?.internal !== true);
|
|
153
|
+
const rank = hasExternal
|
|
154
|
+
? (CONTAINER_INTERFACE_PATTERN.test(name) ? 1 : 0)
|
|
155
|
+
: 2;
|
|
156
|
+
return { name, rows:normalizedRows, index, rank };
|
|
157
|
+
});
|
|
158
|
+
entries.sort((left, right) => left.rank - right.rank || left.index - right.index);
|
|
159
|
+
|
|
160
|
+
const interfaceLimit = clamp(maxInterfaces, 8, 1, 64);
|
|
161
|
+
const addressLimit = clamp(maxAddressesPerInterface, 4, 1, 16);
|
|
162
|
+
const selected = entries.slice(0, interfaceLimit);
|
|
163
|
+
const compact = {};
|
|
164
|
+
let addressCount = 0;
|
|
165
|
+
let addressCountReturned = 0;
|
|
166
|
+
let addressesTruncated = false;
|
|
167
|
+
|
|
168
|
+
for (const entry of entries) addressCount += entry.rows.length;
|
|
169
|
+
for (const entry of selected) {
|
|
170
|
+
const rows = entry.rows.slice(0, addressLimit);
|
|
171
|
+
compact[entry.name] = rows;
|
|
172
|
+
addressCountReturned += rows.length;
|
|
173
|
+
if (entry.rows.length > rows.length) addressesTruncated = true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
hostname,
|
|
178
|
+
dns:Array.isArray(dnsServers) ? dnsServers : [],
|
|
179
|
+
interfaces:compact,
|
|
180
|
+
interface_count:entries.length,
|
|
181
|
+
interface_count_returned:selected.length,
|
|
182
|
+
address_count:addressCount,
|
|
183
|
+
address_count_returned:addressCountReturned,
|
|
184
|
+
interfaces_truncated:entries.length > selected.length,
|
|
185
|
+
addresses_truncated:addressesTruncated,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
136
189
|
export async function networkTool(args) {
|
|
137
190
|
const action = requireEnum(args.action || 'summary', 'action', ['summary','interfaces','dns','routes','listeners','test']);
|
|
138
191
|
if (action === 'interfaces') return jsonResult(os.networkInterfaces());
|
|
139
192
|
if (action === 'dns') return jsonResult({ servers:dns.getServers(), hostname:os.hostname() });
|
|
140
|
-
if (action === 'summary') return jsonResult(
|
|
193
|
+
if (action === 'summary') return jsonResult(compactNetworkSummary());
|
|
141
194
|
if (action === 'test') {
|
|
142
195
|
const host = optionalString(args.host); if (!host) throw new Error('host is required for action=test');
|
|
143
196
|
return jsonResult(await connectivityTest(host, clamp(args.port, 443, 1, 65535), clamp(args.timeout_ms, 3000, 100, 30_000)));
|