@remcp/runtime 0.2.48 → 0.2.50
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 +18 -0
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/catalog.mjs +86 -13
- package/src/extended/browser.mjs +20 -6
- package/src/extended/catalog.mjs +94 -6
- package/src/extended/desktop.mjs +30 -4
- package/src/tools/terminal.mjs +58 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.50 — 2026-09-20
|
|
6
|
+
|
|
7
|
+
- Lockstep release with the ReMCP client after the hosted MCP/plugin contract was hardened for current OpenAI/MCP Apps requirements.
|
|
8
|
+
- Computer-use tools retain the 83-tool runtime surface with AI-oriented descriptions, conditional JSON Schema validation, structured outputs, dynamic capability advertising, and semantic Accessibility/DOM-first routing.
|
|
9
|
+
- `browser_navigate` adds `action=new_tab` so CDP automation can create the first page target without a shell/browser-address-bar workaround.
|
|
10
|
+
- Live Linux verification covers Tesseract OCR targeting, XDG RemoteDesktop keyboard/pointer input, Unicode `type_text`, and the complete 39-tool computer-use smoke.
|
|
11
|
+
|
|
12
|
+
## 0.2.49 — 2026-09-20
|
|
13
|
+
|
|
14
|
+
- Terminal process tools now return schema-validated structured session state, output ranges, match results and termination facts alongside their human-readable text.
|
|
15
|
+
- The runtime catalog and hosted tool contract align on bounded structured outputs and accurate read-only/destructive/idempotent/open-world annotations for the expanded computer-use surface.
|
|
16
|
+
- Preview references remain optional presentation metadata rather than replacing the source result used for model reasoning.
|
|
17
|
+
- Lockstep patch release with the ReMCP client at `0.2.49`.
|
|
18
|
+
|
|
5
19
|
## 0.2.48 — 2026-09-20
|
|
6
20
|
|
|
7
21
|
- Migrate the local stdio runtime to the official `@modelcontextprotocol/server` v2 serving entry while preserving legacy 2025-era client compatibility.
|
|
@@ -13,6 +27,10 @@
|
|
|
13
27
|
- Added high-level `computer_snapshot` and `computer_action` primitives so agents can prefer semantic Accessibility/DOM targets and fall back to coordinates/screenshots only when necessary.
|
|
14
28
|
- Chrome DevTools Protocol endpoints are restricted to loopback addresses. New filesystem/document operations continue through the existing resolved-path allowlist, and launch/service/power operations reuse the runtime command policy.
|
|
15
29
|
- Fixed metadata output truncation once the JSON tool contract exceeded the operating-system pipe buffer by waiting for stdout writes to flush before exit.
|
|
30
|
+
- Terminal session tools now expose schema-validated structured `pid`, status, exit information, output ranges/matches, and session lists alongside their human-readable text, so hosted agents can chain process calls without parsing prose.
|
|
31
|
+
- Tool descriptions now state the intended routing boundary for exact-path vs glob reads, singular vs batch filesystem operations, full-screen vs targeted/semantic screenshots, and local runtime settings vs hosted device labels.
|
|
32
|
+
- Desktop action schemas and common guards now reject targetless `ui_action`/`window_action`, coordinate-less pointer moves, zero-motion scrolls, incomplete cross-backend `computer_action` targets, and connectivity tests without host/port before platform adapters can act. Empty `ui_find`/`browser_find` calls are redirected to their snapshot tools, and `wait_for_ui` requires an explicit state/condition instead of silently acting as a no-op.
|
|
33
|
+
- `take_screenshot` is correctly advertised as non-read-only/non-idempotent because kept or oversized captures can persist timestamped files; selector-scoped `browser_snapshot` screenshots restore the original page scroll position before returning so the inspection path remains read-only.
|
|
16
34
|
- No browser/document rendering stack was added; the runtime remains dependency-light and uses native OS facilities plus existing qpdf/poppler/zip helpers when available.
|
|
17
35
|
|
|
18
36
|
## 0.2.47 — 2026-09-19
|
package/README.md
CHANGED
|
@@ -49,9 +49,11 @@ support and emits `notifications/tools/list_changed` when that capability set ch
|
|
|
49
49
|
| Diagnostics | `service`, `event_log`, `network`, `installed_apps`, `environment`, `audio`, `power_action`, `record_screen` |
|
|
50
50
|
| Documents | `read_document`, `edit_spreadsheet`, `edit_document`, `pdf_action` |
|
|
51
51
|
|
|
52
|
-
The hosted ReMCP endpoint adds
|
|
52
|
+
The hosted ReMCP endpoint adds eleven account/presentation tools — `list_devices`, `ping_device`,
|
|
53
53
|
`who_am_i`, `create_pairing_command`, `get_recent_tool_calls`, `get_usage_statistics`,
|
|
54
|
-
`get_configuration`,
|
|
54
|
+
`get_configuration`, `render_file_preview`, `render_image_preview`, `render_terminal_preview`, and
|
|
55
|
+
`rename_device` — for a 94-tool MCP Apps catalog. Plain MCP clients receive 91 tools because the
|
|
56
|
+
three presentation-only render tools are omitted unless the UI extension is negotiated.
|
|
55
57
|
|
|
56
58
|
`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.
|
|
57
59
|
|
package/package.json
CHANGED
package/src/catalog.mjs
CHANGED
|
@@ -16,6 +16,72 @@ export const TEXT_OUTPUT_SCHEMA = {
|
|
|
16
16
|
additionalProperties: false,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
const NULLABLE_NUMBER_SCHEMA = { anyOf:[{ type:'number' }, { type:'null' }] };
|
|
20
|
+
const NULLABLE_STRING_SCHEMA = { anyOf:[{ type:'string' }, { type:'null' }] };
|
|
21
|
+
const TERMINAL_SESSION_PROPERTIES = {
|
|
22
|
+
pid:{ type:'number', description:'Process/session id used by follow-up terminal tools.' },
|
|
23
|
+
status:{ type:'string', description:'running or exited, with an exit code/signal when known.' },
|
|
24
|
+
runtimeMs:{ type:'number' },
|
|
25
|
+
lines:{ type:'number', description:'Total output lines produced by the session.' },
|
|
26
|
+
exited:{ type:'boolean' },
|
|
27
|
+
exitCode:NULLABLE_NUMBER_SCHEMA,
|
|
28
|
+
signal:NULLABLE_STRING_SCHEMA,
|
|
29
|
+
};
|
|
30
|
+
function terminalOutputSchema(extraProperties = {}, extraRequired = []) {
|
|
31
|
+
return {
|
|
32
|
+
type:'object',
|
|
33
|
+
properties:{ ...TERMINAL_SESSION_PROPERTIES, ...extraProperties },
|
|
34
|
+
required:['pid','status','runtimeMs','lines','exited','exitCode','signal', ...extraRequired],
|
|
35
|
+
additionalProperties:false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const START_PROCESS_OUTPUT_SCHEMA = terminalOutputSchema({
|
|
39
|
+
command:{ type:'string' },
|
|
40
|
+
output:{ type:'string' },
|
|
41
|
+
partial:{ type:'string' },
|
|
42
|
+
warning:{ type:'string' },
|
|
43
|
+
}, ['command','output']);
|
|
44
|
+
const READ_PROCESS_OUTPUT_SCHEMA = terminalOutputSchema({
|
|
45
|
+
range:{ type:'string' },
|
|
46
|
+
output:{ type:'string' },
|
|
47
|
+
explicitOffset:{ type:'boolean' },
|
|
48
|
+
}, ['range','output','explicitOffset']);
|
|
49
|
+
const WAIT_PROCESS_OUTPUT_SCHEMA = terminalOutputSchema({
|
|
50
|
+
pattern:{ type:'string' },
|
|
51
|
+
matched:{ type:'boolean' },
|
|
52
|
+
bufferedMatch:{ type:'boolean' },
|
|
53
|
+
timeoutMs:{ type:'number' },
|
|
54
|
+
output:{ type:'string' },
|
|
55
|
+
}, ['pattern','matched','bufferedMatch','timeoutMs','output']);
|
|
56
|
+
const INTERACT_PROCESS_OUTPUT_SCHEMA = terminalOutputSchema({
|
|
57
|
+
output:{ type:'string' },
|
|
58
|
+
}, ['output']);
|
|
59
|
+
const TERMINATE_PROCESS_OUTPUT_SCHEMA = terminalOutputSchema({
|
|
60
|
+
terminated:{ type:'boolean' },
|
|
61
|
+
alreadyExited:{ type:'boolean' },
|
|
62
|
+
escalated:{ type:'boolean' },
|
|
63
|
+
}, ['terminated','alreadyExited','escalated']);
|
|
64
|
+
const LIST_SESSIONS_OUTPUT_SCHEMA = {
|
|
65
|
+
type:'object',
|
|
66
|
+
properties:{
|
|
67
|
+
sessions:{
|
|
68
|
+
type:'array',
|
|
69
|
+
items:{
|
|
70
|
+
type:'object',
|
|
71
|
+
properties:{
|
|
72
|
+
...TERMINAL_SESSION_PROPERTIES,
|
|
73
|
+
blocked:{ type:'string' },
|
|
74
|
+
command:{ type:'string' },
|
|
75
|
+
},
|
|
76
|
+
required:['pid','status','runtimeMs','lines','exited','exitCode','signal','blocked','command'],
|
|
77
|
+
additionalProperties:false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required:['sessions'],
|
|
82
|
+
additionalProperties:false,
|
|
83
|
+
};
|
|
84
|
+
|
|
19
85
|
const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
20
86
|
const readOnlyNonIdempotent = { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
21
87
|
const additive = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
@@ -43,7 +109,7 @@ export const toolDefinitions = [
|
|
|
43
109
|
{
|
|
44
110
|
name: 'read_files',
|
|
45
111
|
title: 'Read files by glob',
|
|
46
|
-
description: 'Read
|
|
112
|
+
description: 'Read files discovered by one glob under a directory, each section prefixed with its path and line count. Use this when the exact file list is not already known. If you already know the exact paths, use read_multiple_files; for one known path use read_file.',
|
|
47
113
|
inputSchema: {
|
|
48
114
|
type: 'object',
|
|
49
115
|
properties: {
|
|
@@ -61,7 +127,7 @@ export const toolDefinitions = [
|
|
|
61
127
|
{
|
|
62
128
|
name: 'read_multiple_files',
|
|
63
129
|
title: 'Read multiple files',
|
|
64
|
-
description: 'Read several text files in one call.
|
|
130
|
+
description: 'Read several explicitly named text files in one call. Use this when the exact paths are already known; each file is returned separately and one failure does not stop the others. Do not use this for glob discovery: use read_files instead. For one path use read_file.',
|
|
65
131
|
inputSchema: {
|
|
66
132
|
type: 'object',
|
|
67
133
|
properties: {
|
|
@@ -173,7 +239,7 @@ export const toolDefinitions = [
|
|
|
173
239
|
{
|
|
174
240
|
name: 'write_file',
|
|
175
241
|
title: 'Write file',
|
|
176
|
-
description: 'Create
|
|
242
|
+
description: 'Create or replace exactly one text file. Parent directories are created automatically; mode "append" adds to the end. Use write_files for two or more independent files, and write_binary for binary bytes.',
|
|
177
243
|
inputSchema: {
|
|
178
244
|
type: 'object',
|
|
179
245
|
properties: {
|
|
@@ -190,7 +256,7 @@ export const toolDefinitions = [
|
|
|
190
256
|
{
|
|
191
257
|
name: 'write_files',
|
|
192
258
|
title: 'Write multiple files',
|
|
193
|
-
description: 'Create or replace
|
|
259
|
+
description: 'Create or replace two or more text files in one batch, each with its own path, content, and optional mode. Use this for scaffolding or coherent multi-file writes; for one path use write_file.',
|
|
194
260
|
inputSchema: {
|
|
195
261
|
type: 'object',
|
|
196
262
|
properties: {
|
|
@@ -361,7 +427,7 @@ export const toolDefinitions = [
|
|
|
361
427
|
{
|
|
362
428
|
name: 'move_file',
|
|
363
429
|
title: 'Move or rename',
|
|
364
|
-
description: 'Move or rename
|
|
430
|
+
description: 'Move or rename exactly one file or directory from one source to one destination. Replaces an existing destination file by default; pass overwrite false to refuse instead. Use move_paths for two or more moves.',
|
|
365
431
|
inputSchema: {
|
|
366
432
|
type: 'object',
|
|
367
433
|
properties: {
|
|
@@ -378,7 +444,7 @@ export const toolDefinitions = [
|
|
|
378
444
|
{
|
|
379
445
|
name: 'copy_file',
|
|
380
446
|
title: 'Copy file',
|
|
381
|
-
description: 'Copy one file to a new path
|
|
447
|
+
description: 'Copy exactly one regular file to a new path. Directories are not copied recursively. Use copy_paths for directories or two or more copy operations; pass overwrite false to refuse an existing destination.',
|
|
382
448
|
inputSchema: {
|
|
383
449
|
type: 'object',
|
|
384
450
|
properties: {
|
|
@@ -395,7 +461,7 @@ export const toolDefinitions = [
|
|
|
395
461
|
{
|
|
396
462
|
name: 'copy_paths',
|
|
397
463
|
title: 'Copy paths',
|
|
398
|
-
description: 'Copy
|
|
464
|
+
description: 'Copy two or more paths, or copy a directory recursively, in one call. Each item has its own source and destination. For exactly one regular file use copy_file.',
|
|
399
465
|
inputSchema: {
|
|
400
466
|
type: 'object',
|
|
401
467
|
properties: {
|
|
@@ -423,7 +489,7 @@ export const toolDefinitions = [
|
|
|
423
489
|
{
|
|
424
490
|
name: 'move_paths',
|
|
425
491
|
title: 'Move paths',
|
|
426
|
-
description: 'Move or rename
|
|
492
|
+
description: 'Move or rename two or more files/directories in one batch, each with its own source and destination. Falls back to copy-and-delete across filesystems. For one source/destination pair use move_file.',
|
|
427
493
|
inputSchema: {
|
|
428
494
|
type: 'object',
|
|
429
495
|
properties: {
|
|
@@ -451,7 +517,7 @@ export const toolDefinitions = [
|
|
|
451
517
|
{
|
|
452
518
|
name: 'delete_path',
|
|
453
519
|
title: 'Delete path',
|
|
454
|
-
description: 'Delete
|
|
520
|
+
description: 'Delete exactly one file or directory. Directories are removed with their contents unless recursive is false; the filesystem root is refused. Use delete_paths for two or more targets, or move_to_trash when deletion should be reversible.',
|
|
455
521
|
inputSchema: {
|
|
456
522
|
type: 'object',
|
|
457
523
|
properties: {
|
|
@@ -467,7 +533,7 @@ export const toolDefinitions = [
|
|
|
467
533
|
{
|
|
468
534
|
name: 'delete_paths',
|
|
469
535
|
title: 'Delete paths',
|
|
470
|
-
description: 'Delete
|
|
536
|
+
description: 'Delete two or more files/directories in one batch and report each result. For one target use delete_path. Use move_to_trash instead when deletion should be reversible.',
|
|
471
537
|
inputSchema: {
|
|
472
538
|
type: 'object',
|
|
473
539
|
properties: {
|
|
@@ -516,7 +582,7 @@ export const toolDefinitions = [
|
|
|
516
582
|
{
|
|
517
583
|
name: 'take_screenshot',
|
|
518
584
|
title: 'Take screenshot',
|
|
519
|
-
description: 'Capture the
|
|
585
|
+
description: 'Capture the entire desktop only when full-screen pixels are actually needed. Prefer ui_snapshot or browser_snapshot for semantic inspection, and screenshot_region for one window, monitor, or rectangle. This tool uses the platform screenshot portal/utility and returns an image.',
|
|
520
586
|
inputSchema: {
|
|
521
587
|
type: 'object',
|
|
522
588
|
properties: {
|
|
@@ -525,7 +591,8 @@ export const toolDefinitions = [
|
|
|
525
591
|
},
|
|
526
592
|
additionalProperties: false,
|
|
527
593
|
},
|
|
528
|
-
|
|
594
|
+
// keep=true and oversized fallbacks persist a timestamped PNG, so a repeated call can add files.
|
|
595
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
529
596
|
handler: fileToolHandlers.take_screenshot,
|
|
530
597
|
},
|
|
531
598
|
{
|
|
@@ -606,6 +673,7 @@ export const toolDefinitions = [
|
|
|
606
673
|
additionalProperties: false,
|
|
607
674
|
},
|
|
608
675
|
annotations: command,
|
|
676
|
+
outputSchema: START_PROCESS_OUTPUT_SCHEMA,
|
|
609
677
|
handler: terminalToolHandlers.start_process,
|
|
610
678
|
},
|
|
611
679
|
{
|
|
@@ -626,6 +694,7 @@ export const toolDefinitions = [
|
|
|
626
694
|
// Reading without an explicit offset advances the session cursor, so retries can return
|
|
627
695
|
// different output even after the process exits. It never modifies user files.
|
|
628
696
|
annotations: readOnlyNonIdempotent,
|
|
697
|
+
outputSchema: READ_PROCESS_OUTPUT_SCHEMA,
|
|
629
698
|
handler: terminalToolHandlers.read_process_output,
|
|
630
699
|
},
|
|
631
700
|
{
|
|
@@ -643,6 +712,7 @@ export const toolDefinitions = [
|
|
|
643
712
|
additionalProperties: false,
|
|
644
713
|
},
|
|
645
714
|
annotations: command,
|
|
715
|
+
outputSchema: INTERACT_PROCESS_OUTPUT_SCHEMA,
|
|
646
716
|
handler: terminalToolHandlers.interact_with_process,
|
|
647
717
|
},
|
|
648
718
|
{
|
|
@@ -660,6 +730,7 @@ export const toolDefinitions = [
|
|
|
660
730
|
additionalProperties: false,
|
|
661
731
|
},
|
|
662
732
|
annotations: readOnlyNonIdempotent,
|
|
733
|
+
outputSchema: WAIT_PROCESS_OUTPUT_SCHEMA,
|
|
663
734
|
handler: terminalToolHandlers.wait_for_process_output,
|
|
664
735
|
},
|
|
665
736
|
{
|
|
@@ -675,6 +746,7 @@ export const toolDefinitions = [
|
|
|
675
746
|
additionalProperties: false,
|
|
676
747
|
},
|
|
677
748
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
749
|
+
outputSchema: TERMINATE_PROCESS_OUTPUT_SCHEMA,
|
|
678
750
|
handler: terminalToolHandlers.force_terminate,
|
|
679
751
|
},
|
|
680
752
|
{
|
|
@@ -683,6 +755,7 @@ export const toolDefinitions = [
|
|
|
683
755
|
description: 'List terminal sessions started during this ReMCP runtime session with their status and how long they have been running.',
|
|
684
756
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
685
757
|
annotations: readOnly,
|
|
758
|
+
outputSchema: LIST_SESSIONS_OUTPUT_SCHEMA,
|
|
686
759
|
handler: terminalToolHandlers.list_sessions,
|
|
687
760
|
},
|
|
688
761
|
{
|
|
@@ -741,7 +814,7 @@ export const toolDefinitions = [
|
|
|
741
814
|
{
|
|
742
815
|
name: 'set_config_value',
|
|
743
816
|
title: 'Change runtime setting',
|
|
744
|
-
description: 'Change one of this runtime’s own preferences on
|
|
817
|
+
description: 'Change one of this local runtime’s own preferences on the selected computer: telemetryEnabled, maxReadLines, maxBufferedLines or maxOutputBytes. This does not rename the paired device; use hosted rename_device for that label. Access roots, blocked commands, shell/guardrails and the write limit remain local-only.',
|
|
745
818
|
inputSchema: {
|
|
746
819
|
type: 'object',
|
|
747
820
|
properties: {
|
package/src/extended/browser.mjs
CHANGED
|
@@ -310,10 +310,12 @@ export async function browserSnapshot(args) {
|
|
|
310
310
|
|
|
311
311
|
await session.send('Page.enable');
|
|
312
312
|
const selector = optionalString(args.selector);
|
|
313
|
+
let restoreScroll = null;
|
|
313
314
|
if (selector) {
|
|
314
315
|
const bounds = await evaluate(session, `new Promise((resolve,reject) => {
|
|
315
316
|
const el=document.querySelector(${JSON.stringify(selector)});
|
|
316
317
|
if(!el){reject(new Error('Screenshot element not found'));return;}
|
|
318
|
+
const originalScrollX=scrollX, originalScrollY=scrollY;
|
|
317
319
|
const initial=el.getBoundingClientRect();
|
|
318
320
|
const desiredTop=Math.max(0, scrollY + initial.top - Math.max(0,(innerHeight-initial.height)/2));
|
|
319
321
|
window.scrollTo({top:desiredTop,left:scrollX,behavior:'instant'});
|
|
@@ -328,6 +330,8 @@ export async function browserSnapshot(args) {
|
|
|
328
330
|
viewport_height:innerHeight,
|
|
329
331
|
scroll_x:scrollX,
|
|
330
332
|
scroll_y:scrollY,
|
|
333
|
+
original_scroll_x:originalScrollX,
|
|
334
|
+
original_scroll_y:originalScrollY,
|
|
331
335
|
fully_visible:r.x>=0 && r.y>=0 && r.right<=innerWidth && r.bottom<=innerHeight
|
|
332
336
|
});
|
|
333
337
|
}));
|
|
@@ -335,7 +339,9 @@ export async function browserSnapshot(args) {
|
|
|
335
339
|
if (!bounds || !Number.isFinite(Number(bounds.width)) || !Number.isFinite(Number(bounds.height)) || bounds.width <= 0 || bounds.height <= 0) {
|
|
336
340
|
throw new Error('Screenshot element has invalid bounds');
|
|
337
341
|
}
|
|
338
|
-
|
|
342
|
+
restoreScroll = { x:Number(bounds.original_scroll_x || 0), y:Number(bounds.original_scroll_y || 0) };
|
|
343
|
+
const { original_scroll_x: _originalX, original_scroll_y: _originalY, ...reportedBounds } = bounds;
|
|
344
|
+
payload.screenshot_target = { selector, ...reportedBounds };
|
|
339
345
|
}
|
|
340
346
|
payload.screenshot = {
|
|
341
347
|
scope: 'viewport',
|
|
@@ -343,11 +349,18 @@ export async function browserSnapshot(args) {
|
|
|
343
349
|
height: await evaluate(session, 'innerHeight').catch(() => null),
|
|
344
350
|
};
|
|
345
351
|
const result = jsonResult(payload);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
352
|
+
let captured;
|
|
353
|
+
try {
|
|
354
|
+
captured = await session.send('Page.captureScreenshot', {
|
|
355
|
+
format: 'png',
|
|
356
|
+
fromSurface: true,
|
|
357
|
+
captureBeyondViewport: false,
|
|
358
|
+
});
|
|
359
|
+
} finally {
|
|
360
|
+
if (restoreScroll) {
|
|
361
|
+
await evaluate(session, `window.scrollTo({left:${restoreScroll.x},top:${restoreScroll.y},behavior:'instant'})`).catch(() => {});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
351
364
|
const data = String(captured?.data || '');
|
|
352
365
|
if (!data) throw new Error('CDP returned no screenshot data');
|
|
353
366
|
if (Buffer.byteLength(data, 'base64') > 8 * 1024 * 1024) throw new Error('Browser screenshot exceeds the 8 MiB inline image limit');
|
|
@@ -393,6 +406,7 @@ function findExpression(args) {
|
|
|
393
406
|
}
|
|
394
407
|
|
|
395
408
|
export async function browserFind(args) {
|
|
409
|
+
if (!(args.selector || args.text || args.role)) throw new Error('browser_find requires selector, text, or role; use browser_snapshot to enumerate page structure');
|
|
396
410
|
return withTarget(args, async (session, target) => {
|
|
397
411
|
const matches = await evaluate(session, findExpression(args));
|
|
398
412
|
return jsonResult({ target_id: target.id, count: Array.isArray(matches) ? matches.length : 0, matches: Array.isArray(matches) ? matches : [] });
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -34,14 +34,14 @@ const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
34
34
|
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; do not use pointer coordinates for window management when this semantic tool can express the action.',
|
|
35
35
|
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; use start_process for terminal commands, build tools and shell pipelines.',
|
|
36
36
|
ui_snapshot: 'Use this to inspect native desktop Accessibility/UI Automation semantics for the active app before clicking or typing. Prefer scope=active; use scope=desktop only for cross-application discovery. For a Chromium web page with CDP available, prefer browser_snapshot for page DOM semantics.',
|
|
37
|
-
ui_find: 'Use this after ui_snapshot when you need a small set of native UI elements by role/name/label/AutomationId. Prefer returned id or compact label for subsequent ui_action; refresh only when the UI has changed.',
|
|
37
|
+
ui_find: 'Use this after ui_snapshot when you need a small set of native UI elements by role/name/label/AutomationId. It requires an element criterion; use ui_snapshot, not an empty ui_find, to enumerate UI. Prefer returned id or compact label for subsequent ui_action; refresh only when the UI has changed.',
|
|
38
38
|
ui_action: 'Use this to invoke, focus, edit, select, toggle, expand/collapse, range-edit or scroll a native accessible UI element. Prefer this over pointer clicks because semantic actions survive layout changes; fall back to pointer only when accessibility cannot perform the requested action.',
|
|
39
39
|
type_text: 'Use this for reliable Unicode text entry into a native semantic target or focused control. Prefer method=auto so Accessibility or clipboard restoration preserves the requested characters regardless of keyboard layout. Use method=keys only for deliberate physical key-by-key input; printable output then follows the current keyboard layout on the computer. Use keyboard for shortcuts, navigation keys and control keys.',
|
|
40
40
|
keyboard: 'Use this for shortcuts and individual key presses such as Ctrl/Cmd+C, Tab, Enter or Escape. Do not use it to enter normal prose when type_text is available. On Wayland auto uses an already-authorized portal; backend=portal explicitly requests consent.',
|
|
41
41
|
pointer: 'Use this only when an action truly requires screen coordinates or a semantic UI/browser action is unavailable. Prefer ui_action for native controls and browser_action for web controls; pointer is the coordinate fallback for move/click/button-down/button-up.',
|
|
42
42
|
drag_drop: 'Use this for a real drag gesture between two UI element ids or coordinates. Prefer element ids from ui_find/ui_snapshot because their current bounds are resolved at execution time; use coordinates only when no semantic endpoints exist.',
|
|
43
43
|
scroll: 'Use this for native wheel scrolling of a desktop region or semantic UI target. Prefer browser_action scroll_into_view for a known web element and ui_action scroll_into_view for a known accessible desktop element.',
|
|
44
|
-
wait_for_ui: 'Use this after a desktop action to wait for
|
|
44
|
+
wait_for_ui: 'Use this after a desktop action to wait for an explicit native UI state or condition instead of sleeping or polling screenshots. Presence/absence waits need a semantic target; use state=changed for a broad tree change and set a bounded timeout.',
|
|
45
45
|
clipboard: 'Use this for explicit text clipboard read/write/clear operations. Do not use clipboard as a typing workaround directly; type_text already uses safe clipboard paste with restoration when needed.',
|
|
46
46
|
display_inventory: 'Use this before monitor-specific screenshots, cross-monitor geometry or coordinate work to obtain monitor bounds, scale and primary-display information.',
|
|
47
47
|
screenshot_region: 'Use this when pixels are required for visual verification but only one window, monitor or rectangle matters. Prefer this over a full-desktop screenshot; use ui_snapshot/browser_snapshot first when semantic structure is sufficient.',
|
|
@@ -50,8 +50,8 @@ const AI_TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
50
50
|
notification: 'Use this only when the user wants a visible native desktop notification on the paired computer. Do not use it as a substitute for replying in chat.',
|
|
51
51
|
browser_tabs: 'Use this first for CDP browser automation when the target page/tab is not already unambiguous. It lists debuggable Chromium pages from a loopback-only endpoint; use the returned target_id/title/url to scope later browser tools.',
|
|
52
52
|
browser_navigate: 'Use this to open a URL in a new debuggable tab or navigate a known Chromium page by URL, back, forward or reload. Prefer it over typing into the address bar; new_tab also bootstraps browser automation when no page target exists yet.',
|
|
53
|
-
browser_snapshot: 'Use this to inspect a web page structurally through CDP accessibility. Prefer it over desktop ui_snapshot for page content; add include_screenshot=true only when final pixel/layout verification is needed.',
|
|
54
|
-
browser_find: 'Use this to locate visible web elements by CSS selector, text or ARIA role and obtain reusable selectors/bounds. Prefer this before browser_action rather than guessing selectors or coordinates.',
|
|
53
|
+
browser_snapshot: 'Use this to inspect a web page structurally through CDP accessibility. Prefer it over desktop ui_snapshot for page content; add include_screenshot=true only when final pixel/layout verification is needed. A selector may be temporarily centered for capture, but the original page scroll position is restored before the tool returns.',
|
|
54
|
+
browser_find: 'Use this to locate visible web elements by CSS selector, text or ARIA role and obtain reusable selectors/bounds. It requires an element criterion; use browser_snapshot, not an empty browser_find, to enumerate page structure. Prefer this before browser_action rather than guessing selectors or coordinates.',
|
|
55
55
|
browser_action: 'Use this for deterministic DOM/CDP interaction with a known web element: click/focus/type/value/select/upload/key/scroll or viewport emulation. Prefer it over desktop pointer/ui_action for page content; use browser_evaluate only when the supported actions cannot express the task.',
|
|
56
56
|
browser_wait: 'Use this after browser navigation/action to wait for selector/text/URL/load/navigation/network-idle instead of fixed sleeps or screenshot polling. Use browser_evaluate separately for JavaScript predicates.',
|
|
57
57
|
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.',
|
|
@@ -244,7 +244,93 @@ function requiredAny(...groups) {
|
|
|
244
244
|
|
|
245
245
|
function applySchemaRules(name, schema) {
|
|
246
246
|
const next = structuredClone(schema);
|
|
247
|
-
if (name === '
|
|
247
|
+
if (name === 'computer_action') {
|
|
248
|
+
const semanticTarget = requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id'], ['selector'], ['browser_text']);
|
|
249
|
+
next.allOf = [
|
|
250
|
+
{
|
|
251
|
+
if:{ properties:{ action:{ const:'click' } }, required:['action'] },
|
|
252
|
+
then:{ anyOf:[...semanticTarget, { required:['ocr_text'] }, { required:['x','y'] }] },
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
if:{ properties:{ action:{ enum:['invoke','set_value','select','toggle','expand','collapse','scroll_into_view','set_range_value','add_to_selection','remove_from_selection'] } }, required:['action'] },
|
|
256
|
+
then:{ anyOf:semanticTarget },
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
if:{ properties:{ action:{ const:'focus' } }, required:['action'] },
|
|
260
|
+
then:{ anyOf:[...semanticTarget, ...requiredAny(['pid'], ['app'], ['window_title'], ['title'])] },
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
if:{ properties:{ action:{ enum:['set_value','set_range_value'] } }, required:['action'] },
|
|
264
|
+
then:{ required:['value'] },
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
if:{ properties:{ action:{ const:'multi_select' } }, required:['action'] },
|
|
268
|
+
then:{ required:['targets'] },
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
if:{ properties:{ action:{ const:'multi_edit' } }, required:['action'] },
|
|
272
|
+
then:{ required:['edits'] },
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
} else if (name === 'ui_find') {
|
|
276
|
+
next.anyOf = requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']);
|
|
277
|
+
} else if (name === 'ui_action') {
|
|
278
|
+
next.allOf = [
|
|
279
|
+
{ anyOf:requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']) },
|
|
280
|
+
{
|
|
281
|
+
if:{ properties:{ action:{ enum:['set_value','set_range_value'] } }, required:['action'] },
|
|
282
|
+
then:{ required:['value'] },
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
} else if (name === 'wait_for_ui') {
|
|
286
|
+
next.anyOf = requiredAny(['state'], ['condition']);
|
|
287
|
+
next.allOf = [
|
|
288
|
+
{
|
|
289
|
+
if:{ properties:{ condition:{ const:'text_exists' } }, required:['condition'] },
|
|
290
|
+
then:{ required:['text'] },
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
if:{ properties:{ condition:{ const:'active_window' } }, required:['condition'] },
|
|
294
|
+
then:{ anyOf:requiredAny(['text'], ['name'], ['window_title']) },
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
if:{ properties:{ condition:{ enum:['element_exists','element_enabled'] } }, required:['condition'] },
|
|
298
|
+
then:{ anyOf:requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']) },
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
if:{ properties:{ state:{ enum:['present','absent'] } }, required:['state'] },
|
|
302
|
+
then:{ anyOf:requiredAny(['id'], ['label'], ['name'], ['role'], ['automation_id']) },
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
} else if (name === 'window_action') {
|
|
306
|
+
next.allOf = [
|
|
307
|
+
{ anyOf:requiredAny(['id'], ['pid'], ['app'], ['title']) },
|
|
308
|
+
{
|
|
309
|
+
if:{ properties:{ action:{ const:'move' } }, required:['action'] },
|
|
310
|
+
then:{ required:['x','y'] },
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
if:{ properties:{ action:{ const:'resize' } }, required:['action'] },
|
|
314
|
+
then:{ required:['width','height'] },
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
if:{ properties:{ action:{ const:'move_resize' } }, required:['action'] },
|
|
318
|
+
then:{ required:['x','y','width','height'] },
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
} else if (name === 'pointer') {
|
|
322
|
+
next.allOf = [{
|
|
323
|
+
if:{ properties:{ action:{ const:'move' } }, required:['action'] },
|
|
324
|
+
then:{ required:['x','y'] },
|
|
325
|
+
}];
|
|
326
|
+
} else if (name === 'scroll') {
|
|
327
|
+
next.anyOf = requiredAny(['delta_x'], ['delta_y'], ['delta'], ['direction']);
|
|
328
|
+
} else if (name === 'network') {
|
|
329
|
+
next.allOf = [{
|
|
330
|
+
if:{ properties:{ action:{ const:'test' } }, required:['action'] },
|
|
331
|
+
then:{ required:['host','port'] },
|
|
332
|
+
}];
|
|
333
|
+
} else if (name === 'launch_app') {
|
|
248
334
|
next.anyOf = requiredAny(['app'], ['path']);
|
|
249
335
|
} else if (name === 'keyboard') {
|
|
250
336
|
next.anyOf = requiredAny(['shortcut'], ['key'], ['keys']);
|
|
@@ -259,6 +345,8 @@ function applySchemaRules(name, schema) {
|
|
|
259
345
|
{ required:['x','y','width','height'] },
|
|
260
346
|
...requiredAny(['window_id'], ['pid'], ['app'], ['title'], ['monitor'], ['monitor_index']),
|
|
261
347
|
];
|
|
348
|
+
} else if (name === 'browser_find') {
|
|
349
|
+
next.anyOf = requiredAny(['selector'], ['text'], ['role']);
|
|
262
350
|
} else if (name === 'browser_navigate') {
|
|
263
351
|
next.allOf = [{
|
|
264
352
|
if:{ properties:{ action:{ enum:['url','new_tab'] } }, required:['action'] },
|
|
@@ -464,7 +552,7 @@ export const extendedToolDefinitions = [
|
|
|
464
552
|
|
|
465
553
|
define('browser_tabs', 'Browser tabs', 'List debuggable Chrome, Edge, or Chromium page targets from a loopback-only Chrome DevTools Protocol endpoint.', o({ ...browserTarget }), readOnlyLive, browserHandlers.browser_tabs, ['browser_cdp']),
|
|
466
554
|
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']),
|
|
467
|
-
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
|
|
555
|
+
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']),
|
|
468
556
|
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']),
|
|
469
557
|
define('browser_action', 'Browser element action', 'Click, focus, type/set a value, select, scroll to, 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_into_view','upload','press','set_viewport']), selector:s(), text:s(), value:s(), text_value:s(), option:s(), 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']),
|
|
470
558
|
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']),
|
package/src/extended/desktop.mjs
CHANGED
|
@@ -273,7 +273,15 @@ export async function listWindows(args = {}) {
|
|
|
273
273
|
return { ...window, monitor: monitor?.name ?? null, monitor_index: monitor?.index ?? null };
|
|
274
274
|
}));
|
|
275
275
|
}
|
|
276
|
-
export async function windowAction(args = {}) {
|
|
276
|
+
export async function windowAction(args = {}) {
|
|
277
|
+
const hasTarget = Boolean(args.id || Number.isInteger(Number(args.pid)) || args.app || args.title);
|
|
278
|
+
if (!hasTarget) throw new Error('window_action requires id, pid, app, or title');
|
|
279
|
+
const action = optionalString(args.action);
|
|
280
|
+
if (action === 'move' && ![args.x,args.y].every(value => Number.isFinite(Number(value)))) throw new Error('window_action move requires x and y');
|
|
281
|
+
if (action === 'resize' && ![args.width,args.height].every(value => Number.isFinite(Number(value)))) throw new Error('window_action resize requires width and height');
|
|
282
|
+
if (action === 'move_resize' && ![args.x,args.y,args.width,args.height].every(value => Number.isFinite(Number(value)))) throw new Error('window_action move_resize requires x, y, width and height');
|
|
283
|
+
return adapter.windowAction(args);
|
|
284
|
+
}
|
|
277
285
|
export async function uiSnapshot(args = {}) {
|
|
278
286
|
const explicitlyScoped = args.pid != null || args.app || args.window_title || args.windowTitle;
|
|
279
287
|
const activeOnly = args.active_only != null
|
|
@@ -285,10 +293,18 @@ export async function uiSnapshot(args = {}) {
|
|
|
285
293
|
return jsonResult(enrichUiPayload(value, effective));
|
|
286
294
|
}
|
|
287
295
|
export async function uiAction(args = {}) {
|
|
288
|
-
|
|
296
|
+
const resolved = withResolvedUiLabel(args);
|
|
297
|
+
const hasTarget = Boolean(resolved.id || resolved.label != null || resolved.name || resolved.role || resolved.automation_id || resolved.automationId);
|
|
298
|
+
if (!hasTarget) throw new Error('ui_action requires id, label, name, role, or automation_id');
|
|
299
|
+
const action = optionalString(resolved.action);
|
|
300
|
+
if (['set_value','set_range_value'].includes(action) && resolved.value == null) throw new Error(`ui_action ${action} requires value`);
|
|
301
|
+
return adapter.uiAction(resolved);
|
|
289
302
|
}
|
|
290
303
|
export async function keyboard(args = {}) { return adapter.keyboard(args); }
|
|
291
|
-
export async function pointer(args = {}) {
|
|
304
|
+
export async function pointer(args = {}) {
|
|
305
|
+
if (args.action === 'move' && ![args.x,args.y].every(value => Number.isFinite(Number(value)))) throw new Error('pointer move requires x and y');
|
|
306
|
+
return adapter.pointer(args);
|
|
307
|
+
}
|
|
292
308
|
export async function clipboard(args = {}) { return adapter.clipboard(args); }
|
|
293
309
|
export async function displayInventory(args = {}) { return adapter.displayInventory(args); }
|
|
294
310
|
export async function cursorPosition(args = {}) { return typeof adapter.cursorPosition === 'function' ? adapter.cursorPosition(args) : jsonResult({ x:null, y:null }); }
|
|
@@ -334,6 +350,8 @@ export async function screenshotRegion(args = {}) {
|
|
|
334
350
|
export async function notification(args = {}) { return adapter.notification(args); }
|
|
335
351
|
export async function scroll(args = {}) {
|
|
336
352
|
const direction = optionalString(args.direction);
|
|
353
|
+
const hasDelta = [args.delta_x,args.delta_y,args.delta].some(value => value != null && Number.isFinite(Number(value)));
|
|
354
|
+
if (!direction && !hasDelta) throw new Error('scroll requires direction or a delta');
|
|
337
355
|
const times = clamp(args.wheel_times, 1, 1, 50);
|
|
338
356
|
let deltaX = Number(args.delta_x || 0);
|
|
339
357
|
let deltaY = Number(args.delta_y ?? args.delta ?? 0);
|
|
@@ -403,6 +421,8 @@ async function uiMatches(args = {}) {
|
|
|
403
421
|
}
|
|
404
422
|
|
|
405
423
|
export async function uiFind(args = {}) {
|
|
424
|
+
const hasSelector = Boolean(args.id || args.label != null || args.name || args.role || args.automation_id || args.automationId);
|
|
425
|
+
if (!hasSelector) throw new Error('ui_find requires id, label, name, role, or automation_id; use ui_snapshot to enumerate UI');
|
|
406
426
|
const nodes = await uiMatches(args);
|
|
407
427
|
return jsonResult({ count: nodes.length, nodes });
|
|
408
428
|
}
|
|
@@ -473,11 +493,17 @@ export async function waitForUi(args = {}) {
|
|
|
473
493
|
const timeoutMs = clamp(args.timeout_ms, 10_000, 100, 120_000);
|
|
474
494
|
const pollMs = clamp(args.poll_ms, 250, 50, 5000);
|
|
475
495
|
const rawCondition = optionalString(args.condition);
|
|
496
|
+
const rawState = optionalString(args.state);
|
|
497
|
+
if (!rawCondition && !rawState) throw new Error('wait_for_ui requires state or condition');
|
|
476
498
|
const aliases = { text:'text_exists', window:'active_window', element:'element_exists', enabled:'element_enabled', focused:'focused_element' };
|
|
477
499
|
const condition = rawCondition
|
|
478
500
|
? requireEnum(aliases[rawCondition] || rawCondition, 'condition', ['text_exists','active_window','element_exists','element_enabled','focused_element'])
|
|
479
501
|
: null;
|
|
480
|
-
const state = condition ? null : requireEnum(
|
|
502
|
+
const state = condition ? null : requireEnum(rawState, 'state', ['present', 'absent', 'changed']);
|
|
503
|
+
const hasSelector = Boolean(args.id || args.label != null || args.name || args.role || args.automation_id || args.automationId);
|
|
504
|
+
if (['element_exists','element_enabled'].includes(condition) && !hasSelector) throw new Error(`wait_for_ui condition=${condition} requires a semantic target`);
|
|
505
|
+
if (condition === 'active_window' && !(args.text || args.name || args.window_title || args.windowTitle)) throw new Error('wait_for_ui condition=active_window requires text, name, or window_title');
|
|
506
|
+
if (['present','absent'].includes(state) && !hasSelector) throw new Error(`wait_for_ui state=${state} requires a semantic target`);
|
|
481
507
|
const deadline = Date.now() + timeoutMs;
|
|
482
508
|
const startedAt = Date.now();
|
|
483
509
|
let attempts = 0;
|
package/src/tools/terminal.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
waitForProcessExit,
|
|
18
18
|
waitForProcessActivity,
|
|
19
19
|
} from '../sessions.mjs';
|
|
20
|
-
import { clampInteger, fail, requireInteger, requireString, text } from '../util.mjs';
|
|
20
|
+
import { clampInteger, fail, requireInteger, requireString, structured, text } from '../util.mjs';
|
|
21
21
|
|
|
22
22
|
function shellCommand() {
|
|
23
23
|
if (runtimeConfig.defaultShell) return runtimeConfig.defaultShell;
|
|
@@ -32,7 +32,15 @@ function shellArgs(command) {
|
|
|
32
32
|
|
|
33
33
|
function describeSession(session) {
|
|
34
34
|
const status = session.exited ? `exited${session.signal ? ` (${session.signal})` : session.exitCode === null ? '' : ` (code ${session.exitCode})`}` : 'running';
|
|
35
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
pid: session.pid,
|
|
37
|
+
status,
|
|
38
|
+
runtimeMs: (session.finishedAt || Date.now()) - session.startedAt,
|
|
39
|
+
lines: totalLines(session),
|
|
40
|
+
exited: Boolean(session.exited),
|
|
41
|
+
exitCode: session.exitCode ?? null,
|
|
42
|
+
signal: session.signal ?? null,
|
|
43
|
+
};
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
function abortError() {
|
|
@@ -90,8 +98,16 @@ export async function startProcessTool(args, extra = {}) {
|
|
|
90
98
|
const partial = session.partial;
|
|
91
99
|
session.cursor = session.droppedLines + session.lines.length;
|
|
92
100
|
session.lastPartialRead = session.partial || null;
|
|
93
|
-
const warning = verdict.note ?
|
|
94
|
-
|
|
101
|
+
const warning = verdict.note ? String(verdict.note) : '';
|
|
102
|
+
const rendered = [warning, headline, output, partial].filter(Boolean).join('\n');
|
|
103
|
+
const status = describeSession(session);
|
|
104
|
+
return structured({
|
|
105
|
+
...status,
|
|
106
|
+
command,
|
|
107
|
+
output,
|
|
108
|
+
...(partial ? { partial } : {}),
|
|
109
|
+
...(warning ? { warning } : {}),
|
|
110
|
+
}, rendered);
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
export async function readProcessOutputTool(args, extra = {}) {
|
|
@@ -124,7 +140,13 @@ export async function readProcessOutputTool(args, extra = {}) {
|
|
|
124
140
|
}
|
|
125
141
|
const status = describeSession(session);
|
|
126
142
|
const header = `pid ${pid} ${status.status} · lines ${range}`;
|
|
127
|
-
|
|
143
|
+
const output = slice.join('\n');
|
|
144
|
+
return structured({
|
|
145
|
+
...status,
|
|
146
|
+
range,
|
|
147
|
+
output,
|
|
148
|
+
explicitOffset:hasOffset,
|
|
149
|
+
}, `${header}\n${output}`);
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
function compileWaiter(pattern) {
|
|
@@ -164,7 +186,15 @@ export async function waitForProcessOutputTool(args, extra = {}) {
|
|
|
164
186
|
const headline = matched
|
|
165
187
|
? `pid ${pid} ${status.status} · pattern matched${bufferedMatch ? ' (already buffered)' : ''}`
|
|
166
188
|
: `pid ${pid} ${status.status} · pattern not matched within ${timeoutMs}ms`;
|
|
167
|
-
|
|
189
|
+
const output = slice.join('\n');
|
|
190
|
+
return structured({
|
|
191
|
+
...status,
|
|
192
|
+
pattern,
|
|
193
|
+
matched,
|
|
194
|
+
bufferedMatch,
|
|
195
|
+
timeoutMs,
|
|
196
|
+
output,
|
|
197
|
+
}, [headline, output].filter(Boolean).join('\n'));
|
|
168
198
|
}
|
|
169
199
|
|
|
170
200
|
export async function interactWithProcessTool(args, extra = {}) {
|
|
@@ -195,34 +225,49 @@ export async function interactWithProcessTool(args, extra = {}) {
|
|
|
195
225
|
}
|
|
196
226
|
const slice = readNewOutput(session);
|
|
197
227
|
const status = describeSession(session);
|
|
198
|
-
|
|
228
|
+
const output = slice.join('\n');
|
|
229
|
+
return structured({
|
|
230
|
+
...status,
|
|
231
|
+
output,
|
|
232
|
+
}, [`pid ${pid} ${status.status}`, output].filter(Boolean).join('\n'));
|
|
199
233
|
}
|
|
200
234
|
|
|
201
235
|
export async function forceTerminateTool(args) {
|
|
202
236
|
const pid = requireInteger(args.pid, 'pid');
|
|
203
237
|
const session = getProcessSession(pid);
|
|
204
238
|
if (!session) fail(`No ReMCP session with pid ${pid}`);
|
|
205
|
-
if (session.exited)
|
|
239
|
+
if (session.exited) {
|
|
240
|
+
const status = describeSession(session);
|
|
241
|
+
return structured({ ...status, terminated:false, alreadyExited:true, escalated:false }, `Process ${pid} already exited.`);
|
|
242
|
+
}
|
|
206
243
|
killSessionTree(session, 'SIGTERM');
|
|
207
244
|
const deadline = Date.now() + 2000;
|
|
208
245
|
while (!session.exited && Date.now() < deadline) await waitForProcessActivity(session, 100);
|
|
246
|
+
let escalated = false;
|
|
209
247
|
if (!session.exited) {
|
|
248
|
+
escalated = true;
|
|
210
249
|
killSessionTree(session, 'SIGKILL');
|
|
211
250
|
await waitForProcessActivity(session, 1000);
|
|
212
251
|
}
|
|
213
252
|
const status = describeSession(session);
|
|
214
|
-
return
|
|
253
|
+
return structured(
|
|
254
|
+
{ ...status, terminated:Boolean(session.exited), alreadyExited:false, escalated },
|
|
255
|
+
`Terminated session ${pid}${status.status.startsWith('exited') ? '' : ' (still running)'}. Status: ${status.status}.`,
|
|
256
|
+
);
|
|
215
257
|
}
|
|
216
258
|
|
|
217
259
|
export async function listSessionsTool() {
|
|
218
260
|
const sessions = listProcessSessions();
|
|
219
|
-
|
|
220
|
-
const rows = sessions.map(session => {
|
|
261
|
+
const facts = sessions.map(session => {
|
|
221
262
|
const status = describeSession(session);
|
|
222
263
|
const blocked = session.exited ? '' : session.partial ? 'blocked-possibly' : 'idle-or-running';
|
|
223
|
-
return
|
|
264
|
+
return { ...status, blocked, command:session.command.slice(0, 120) };
|
|
224
265
|
});
|
|
225
|
-
|
|
266
|
+
if (!facts.length) return structured({ sessions:[] }, 'No active terminal sessions.');
|
|
267
|
+
const rows = facts.map(session =>
|
|
268
|
+
`pid ${session.pid} · ${session.status} · ${Math.round(session.runtimeMs / 1000)}s · ${session.blocked} · ${session.command}`
|
|
269
|
+
);
|
|
270
|
+
return structured({ sessions:facts }, rows.join('\n'));
|
|
226
271
|
}
|
|
227
272
|
|
|
228
273
|
export const terminalToolHandlers = {
|