@remcp/runtime 0.2.71 → 0.2.73
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/package.json
CHANGED
package/src/extended/browser.mjs
CHANGED
|
@@ -470,15 +470,40 @@ function findExpression(args) {
|
|
|
470
470
|
}
|
|
471
471
|
return parts.join(' > ');
|
|
472
472
|
}
|
|
473
|
+
function semanticRole(el) {
|
|
474
|
+
const explicit = (el.getAttribute('role') || '').trim().toLowerCase();
|
|
475
|
+
if (explicit) return explicit;
|
|
476
|
+
const tag = el.tagName.toLowerCase();
|
|
477
|
+
if (tag === 'button') return 'button';
|
|
478
|
+
if (tag === 'a' && el.hasAttribute('href')) return 'link';
|
|
479
|
+
if (tag === 'textarea') return 'textbox';
|
|
480
|
+
if (tag === 'select') return el.multiple || el.size > 1 ? 'listbox' : 'combobox';
|
|
481
|
+
if (tag === 'option') return 'option';
|
|
482
|
+
if (tag === 'img') return 'img';
|
|
483
|
+
if (tag === 'progress') return 'progressbar';
|
|
484
|
+
if (tag === 'meter') return 'meter';
|
|
485
|
+
if (tag === 'summary') return 'button';
|
|
486
|
+
if (tag === 'input') {
|
|
487
|
+
const type = String(el.type || 'text').toLowerCase();
|
|
488
|
+
if (['button','submit','reset','image'].includes(type)) return 'button';
|
|
489
|
+
if (type === 'checkbox') return 'checkbox';
|
|
490
|
+
if (type === 'radio') return 'radio';
|
|
491
|
+
if (type === 'range') return 'slider';
|
|
492
|
+
if (type === 'number') return 'spinbutton';
|
|
493
|
+
if (type === 'search') return 'searchbox';
|
|
494
|
+
if (!['hidden','file','color'].includes(type)) return 'textbox';
|
|
495
|
+
}
|
|
496
|
+
return '';
|
|
497
|
+
}
|
|
473
498
|
return source.filter(el => {
|
|
474
499
|
const style = getComputedStyle(el);
|
|
475
500
|
const visible = style.visibility !== 'hidden' && style.display !== 'none' && el.getClientRects().length > 0;
|
|
476
501
|
const hay = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
477
|
-
const r = el
|
|
478
|
-
return visible && (!needle || hay.toLowerCase().includes(needle.toLowerCase())) && (!role || r
|
|
502
|
+
const r = semanticRole(el);
|
|
503
|
+
return visible && (!needle || hay.toLowerCase().includes(needle.toLowerCase())) && (!role || r === role.toLowerCase());
|
|
479
504
|
}).slice(0, ${limit}).map(el => {
|
|
480
505
|
const b = el.getBoundingClientRect();
|
|
481
|
-
return { selector: cssPath(el), tag: el.tagName.toLowerCase(), role: el
|
|
506
|
+
return { selector: cssPath(el), tag: el.tagName.toLowerCase(), role: semanticRole(el), name: el.getAttribute('aria-label') || el.getAttribute('name') || '', text: (el.innerText || el.textContent || '').trim().slice(0, 500), value: 'value' in el ? String(el.value).slice(0, 500) : '', x: b.x, y: b.y, width: b.width, height: b.height, disabled: Boolean(el.disabled) };
|
|
482
507
|
});
|
|
483
508
|
})()`;
|
|
484
509
|
}
|
|
@@ -583,8 +583,28 @@ while queue:
|
|
|
583
583
|
for i in range(e.childCount):queue.append((e.getChildAtIndex(i),app_name,pid,window_name,node_path+"."+str(i)))
|
|
584
584
|
except:pass
|
|
585
585
|
if target is None:raise Exception("UI element not found")
|
|
586
|
-
|
|
587
|
-
|
|
586
|
+
pointer_fallback=None
|
|
587
|
+
if action in ("click","invoke"):
|
|
588
|
+
try:
|
|
589
|
+
applied=target.queryAction().doAction(0)
|
|
590
|
+
if applied is False: raise Exception("accessibility action returned false")
|
|
591
|
+
except Exception as action_error:
|
|
592
|
+
try:
|
|
593
|
+
extents=target.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
|
|
594
|
+
def _rect_value(rect,name,index):
|
|
595
|
+
try: return int(getattr(rect,name))
|
|
596
|
+
except:
|
|
597
|
+
try: return int(rect[index])
|
|
598
|
+
except: return 0
|
|
599
|
+
px=_rect_value(extents,"x",0);py=_rect_value(extents,"y",1)
|
|
600
|
+
pw=_rect_value(extents,"width",2);ph=_rect_value(extents,"height",3)
|
|
601
|
+
if pw<=0 or ph<=0: raise Exception("target has no usable screen bounds")
|
|
602
|
+
pointer_fallback={"x":px+pw//2,"y":py+ph//2,"width":pw,"height":ph,"reason":str(action_error)}
|
|
603
|
+
except Exception as bounds_error:
|
|
604
|
+
raise Exception("accessibility action failed: %s; coordinate fallback unavailable: %s"%(action_error,bounds_error))
|
|
605
|
+
elif action in ("select","toggle"):
|
|
606
|
+
applied=target.queryAction().doAction(0)
|
|
607
|
+
if applied is False: raise Exception(action+" accessibility action returned false")
|
|
588
608
|
elif action=="focus":
|
|
589
609
|
if target.queryComponent().grabFocus() is False: raise Exception("focus accessibility action returned false")
|
|
590
610
|
elif action=="set_value":
|
|
@@ -618,10 +638,28 @@ elif action in ("add_to_selection","remove_from_selection"):
|
|
|
618
638
|
if not selection.selectChild(idx): raise Exception("could not add item to selection")
|
|
619
639
|
else:
|
|
620
640
|
if not selection.deselectChild(idx): raise Exception("could not remove item from selection")
|
|
621
|
-
|
|
641
|
+
out={"action":action,"name":target.name or "","role":target.getRoleName(),**target_meta}
|
|
642
|
+
if pointer_fallback is not None: out["pointer_fallback"]=pointer_fallback
|
|
643
|
+
print(json.dumps(out))`;
|
|
622
644
|
const result = await runFile('python3', ['-c', script], { label: 'AT-SPI action', timeout: 30_000, allowFailure: true });
|
|
623
645
|
let payload = null;
|
|
624
646
|
try { payload = JSON.parse(result.stdout || ''); } catch {}
|
|
647
|
+
if (payload?.pointer_fallback) {
|
|
648
|
+
const fallback = payload.pointer_fallback;
|
|
649
|
+
const x = Number(fallback.x), y = Number(fallback.y);
|
|
650
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) unavailable('Linux accessibility action', 'coordinate fallback returned invalid screen bounds');
|
|
651
|
+
await pointer({ action:'click', x, y, ...(args.backend ? { backend:args.backend } : {}) });
|
|
652
|
+
return jsonResult({
|
|
653
|
+
action,
|
|
654
|
+
id:id || null,
|
|
655
|
+
name:payload.name || '',
|
|
656
|
+
role:payload.role || '',
|
|
657
|
+
backend:'pointer_fallback',
|
|
658
|
+
x,
|
|
659
|
+
y,
|
|
660
|
+
fallback_reason:String(fallback.reason || 'accessibility action unavailable').slice(0, 500),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
625
663
|
if (payload && !payload.error) return text(JSON.stringify(payload));
|
|
626
664
|
if (payload?.error) unavailable('Linux accessibility action', payload.error);
|
|
627
665
|
if (result.code !== 0) {
|
package/src/extended/desktop.mjs
CHANGED
|
@@ -330,9 +330,21 @@ export async function uiSnapshot(args = {}) {
|
|
|
330
330
|
const activeOnly = args.active_only != null
|
|
331
331
|
? args.active_only === true
|
|
332
332
|
: (!explicitlyScoped && args.scope !== 'desktop');
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
let effective = { ...args, active_only:activeOnly };
|
|
334
|
+
let raw = await adapter.uiSnapshot(effective);
|
|
335
|
+
let value = resultValue(raw);
|
|
336
|
+
const requestedDepth = Number(effective.max_depth);
|
|
337
|
+
const emptySnapshot = Array.isArray(value)
|
|
338
|
+
? value.length === 0
|
|
339
|
+
: Array.isArray(value?.nodes) && value.nodes.length === 0;
|
|
340
|
+
if (process.platform === 'linux' && Number.isFinite(requestedDepth) && requestedDepth > 15 && emptySnapshot) {
|
|
341
|
+
// Some GNOME/AT-SPI providers collapse otherwise valid trees to an empty result
|
|
342
|
+
// when traversed too deeply. Preserve the caller's deep request first, but recover
|
|
343
|
+
// from that provider failure with the deepest stable fallback observed on Wayland.
|
|
344
|
+
effective = { ...effective, max_depth:15 };
|
|
345
|
+
raw = await adapter.uiSnapshot(effective);
|
|
346
|
+
value = resultValue(raw);
|
|
347
|
+
}
|
|
336
348
|
return jsonResult(enrichUiPayload(value, effective));
|
|
337
349
|
}
|
|
338
350
|
export async function uiAction(args = {}) {
|
|
@@ -452,7 +464,7 @@ async function uiMatches(args = {}) {
|
|
|
452
464
|
const scopedPid = resolved.pid != null ? resolved.pid : stableLinuxPid ? Number(stableLinuxPid) : null;
|
|
453
465
|
const result = await uiSnapshot({
|
|
454
466
|
max_nodes: resolved.max_nodes || 2000,
|
|
455
|
-
max_depth: resolved.max_depth || 16,
|
|
467
|
+
max_depth: resolved.max_depth || (process.platform === 'linux' ? 12 : 16),
|
|
456
468
|
...(scopedPid != null ? { pid: scopedPid } : {}),
|
|
457
469
|
...(resolved.app ? { app: resolved.app } : {}),
|
|
458
470
|
...((resolved.window_title || resolved.windowTitle) ? { window_title: resolved.window_title || resolved.windowTitle } : {}),
|
|
@@ -508,7 +520,7 @@ async function uiConditionMatches(args, condition) {
|
|
|
508
520
|
if (!cached) {
|
|
509
521
|
const snapshot = resultValue(await uiSnapshot({
|
|
510
522
|
max_nodes:args.max_nodes || 2000,
|
|
511
|
-
max_depth:args.max_depth || 16,
|
|
523
|
+
max_depth:args.max_depth || (process.platform === 'linux' ? 12 : 16),
|
|
512
524
|
...(args.pid != null ? { pid:args.pid } : {}),
|
|
513
525
|
...(args.app ? { app:args.app } : {}),
|
|
514
526
|
...((args.window_title || args.windowTitle) ? { window_title:args.window_title || args.windowTitle } : {}),
|
|
@@ -336,6 +336,29 @@ export async function editDocument(args) {
|
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
export function expandPdfPages(spec, maxPages = 10_000) {
|
|
340
|
+
const normalized = String(spec || '').replace(/\s+/g, '');
|
|
341
|
+
if (!normalized || !/^[0-9,\-]+$/.test(normalized)) throw new Error('pages must look like 1-3,5');
|
|
342
|
+
const pages = [];
|
|
343
|
+
for (const token of normalized.split(',')) {
|
|
344
|
+
if (!token) throw new Error('pages must look like 1-3,5');
|
|
345
|
+
const range = token.match(/^(\d+)-(\d+)$/);
|
|
346
|
+
if (range) {
|
|
347
|
+
const start = Number(range[1]);
|
|
348
|
+
const end = Number(range[2]);
|
|
349
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start) throw new Error(`Invalid PDF page range: ${token}`);
|
|
350
|
+
if (pages.length + (end - start + 1) > maxPages) throw new Error(`PDF page selection exceeds ${maxPages} pages`);
|
|
351
|
+
for (let page = start; page <= end; page += 1) pages.push(page);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const page = Number(token);
|
|
355
|
+
if (!Number.isInteger(page) || page < 1) throw new Error(`Invalid PDF page: ${token}`);
|
|
356
|
+
pages.push(page);
|
|
357
|
+
if (pages.length > maxPages) throw new Error(`PDF page selection exceeds ${maxPages} pages`);
|
|
358
|
+
}
|
|
359
|
+
return pages;
|
|
360
|
+
}
|
|
361
|
+
|
|
339
362
|
function pdfAnnotations(buffer) {
|
|
340
363
|
const raw = buffer.toString('latin1');
|
|
341
364
|
const rows = [];
|
|
@@ -382,12 +405,38 @@ export async function pdfAction(args) {
|
|
|
382
405
|
else unavailable('PDF split','install poppler-utils (pdfseparate)');
|
|
383
406
|
return jsonResult({action,output_dir:outputDir,files:(await readdir(outputDir)).filter(name=>name.toLowerCase().endsWith('.pdf')).sort()});
|
|
384
407
|
}
|
|
385
|
-
const pages=optionalString(args.pages);
|
|
408
|
+
const pages=optionalString(args.pages);
|
|
409
|
+
const selectedPages=expandPdfPages(pages);
|
|
410
|
+
const normalizedPages=selectedPages.join(',');
|
|
386
411
|
const output=await resolveSafePath(args.output,'output');
|
|
387
|
-
if(commandExists('qpdf'))
|
|
388
|
-
|
|
389
|
-
else
|
|
390
|
-
|
|
412
|
+
if(commandExists('qpdf')) {
|
|
413
|
+
await runFile('qpdf',[source,'--pages','.',pages.replace(/\s+/g,''),'--',output],{label:'PDF extract pages',timeout:60_000});
|
|
414
|
+
} else if(commandExists('pdftk')) {
|
|
415
|
+
await runFile('pdftk',[source,'cat',...pages.replace(/\s+/g,'').split(','),'output',output],{label:'PDF extract pages',timeout:60_000});
|
|
416
|
+
} else if(commandExists('pdfseparate')) {
|
|
417
|
+
const dir=await tempDir('remcp-pdf-pages-');
|
|
418
|
+
try {
|
|
419
|
+
const extracted=[];
|
|
420
|
+
for(let index=0;index<selectedPages.length;index+=1){
|
|
421
|
+
const page=selectedPages[index];
|
|
422
|
+
const pattern=path.join(dir,`selection-${index+1}-%d.pdf`);
|
|
423
|
+
await runFile('pdfseparate',['-f',String(page),'-l',String(page),source,pattern],{label:`PDF extract page ${page}`,timeout:60_000});
|
|
424
|
+
extracted.push(path.join(dir,`selection-${index+1}-${page}.pdf`));
|
|
425
|
+
}
|
|
426
|
+
if(extracted.length===1) {
|
|
427
|
+
await copyFile(extracted[0],output);
|
|
428
|
+
} else if(commandExists('pdfunite')) {
|
|
429
|
+
await runFile('pdfunite',[...extracted,output],{label:'PDF assemble extracted pages',timeout:60_000});
|
|
430
|
+
} else {
|
|
431
|
+
unavailable('PDF page extraction','pdfunite is required with pdfseparate when extracting multiple pages');
|
|
432
|
+
}
|
|
433
|
+
} finally {
|
|
434
|
+
await removeTemp(dir);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
unavailable('PDF page extraction','install qpdf, pdftk, or poppler-utils (pdfseparate; pdfunite for multiple pages)');
|
|
438
|
+
}
|
|
439
|
+
return jsonResult({action,source,pages:normalizedPages,output,bytes:(await stat(output)).size});
|
|
391
440
|
}
|
|
392
441
|
|
|
393
442
|
export const documentHandlers = {
|
package/src/sessions.mjs
CHANGED
|
@@ -68,6 +68,10 @@ export function createProcessSession({ pid, child, command, shell }) {
|
|
|
68
68
|
droppedLines: 0,
|
|
69
69
|
bufferedChars: 0,
|
|
70
70
|
cursor: 0,
|
|
71
|
+
// wait_for_process_output has its own consumption watermark. start_process may
|
|
72
|
+
// display initial output and advance the ordinary read cursor, but the first
|
|
73
|
+
// wait must still be able to match that retained output once.
|
|
74
|
+
waitCursor: 0,
|
|
71
75
|
exitCode: null,
|
|
72
76
|
signal: null,
|
|
73
77
|
exited: false,
|
package/src/tools/terminal.mjs
CHANGED
|
@@ -150,6 +150,7 @@ export async function readProcessOutputTool(args, extra = {}) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
slice = readNewOutput(session);
|
|
153
|
+
session.waitCursor = session.cursor;
|
|
153
154
|
const first = Math.max(1, session.cursor - slice.length + 1);
|
|
154
155
|
range = slice.length ? `${first}-${session.cursor} of ${totalLines(session)}` : `no new output (${totalLines(session)} lines total)`;
|
|
155
156
|
}
|
|
@@ -181,9 +182,11 @@ export async function waitForProcessOutputTool(args, extra = {}) {
|
|
|
181
182
|
const pattern = requireString(args.pattern, 'pattern');
|
|
182
183
|
const matcher = compileWaiter(pattern);
|
|
183
184
|
const timeoutMs = clampInteger(args.timeout_ms, 10000, 0, 120000);
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
//
|
|
185
|
+
const waitCursor = Number.isInteger(session.waitCursor) ? session.waitCursor : session.cursor;
|
|
186
|
+
const cursorStart = Math.max(0, waitCursor - session.droppedLines);
|
|
187
|
+
// Output already retained before the call still counts once, even when start_process
|
|
188
|
+
// displayed it and advanced the ordinary read cursor. A separate wait watermark keeps
|
|
189
|
+
// retries from replaying the same historical match forever.
|
|
187
190
|
let slice = session.lines.slice(cursorStart);
|
|
188
191
|
const bufferedMatch = waiterMatches(slice, matcher);
|
|
189
192
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -196,6 +199,7 @@ export async function waitForProcessOutputTool(args, extra = {}) {
|
|
|
196
199
|
}
|
|
197
200
|
const matched = waiterMatches(slice, matcher);
|
|
198
201
|
session.cursor = session.droppedLines + session.lines.length;
|
|
202
|
+
session.waitCursor = session.cursor;
|
|
199
203
|
session.lastPartialRead = session.partial || null;
|
|
200
204
|
const status = describeSession(session);
|
|
201
205
|
const headline = matched
|
|
@@ -220,6 +224,7 @@ export async function interactWithProcessTool(args, extra = {}) {
|
|
|
220
224
|
const input = typeof args.input === 'string' ? args.input : fail('input must be a string');
|
|
221
225
|
const timeoutMs = clampInteger(args.timeout_ms, 1000, 0, 120000);
|
|
222
226
|
session.cursor = session.droppedLines + session.lines.length;
|
|
227
|
+
session.waitCursor = session.cursor;
|
|
223
228
|
session.lastPartialRead = null;
|
|
224
229
|
throwIfAborted(extra.signal);
|
|
225
230
|
const stdin = session.child?.stdin;
|
|
@@ -239,6 +244,7 @@ export async function interactWithProcessTool(args, extra = {}) {
|
|
|
239
244
|
await waitForProcessActivity(session, Math.min(200, Math.max(20, deadline - Date.now())));
|
|
240
245
|
}
|
|
241
246
|
const slice = readNewOutput(session);
|
|
247
|
+
session.waitCursor = session.cursor;
|
|
242
248
|
const status = describeSession(session);
|
|
243
249
|
const output = slice.join('\n');
|
|
244
250
|
return terminalStructured({
|