@remcp/runtime 0.2.46 → 0.2.48
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 +19 -0
- package/README.md +42 -25
- package/package.json +6 -2
- package/src/catalog.mjs +1 -1
- package/src/extended/browser.mjs +592 -0
- package/src/extended/catalog.mjs +564 -0
- package/src/extended/common.mjs +209 -0
- package/src/extended/desktop-linux.mjs +1173 -0
- package/src/extended/desktop-macos.mjs +408 -0
- package/src/extended/desktop-windows.mjs +546 -0
- package/src/extended/desktop.mjs +1313 -0
- package/src/extended/diagnostics.mjs +274 -0
- package/src/extended/documents.mjs +381 -0
- package/src/extended/ocr.mjs +139 -0
- package/src/helpers/wayland-eis-helper.py +450 -0
- package/src/index.mjs +46 -44
- package/src/invoke.mjs +3 -3
- package/src/mcp-v2.mjs +104 -0
- package/src/permissions.mjs +1 -1
- package/src/util.mjs +24 -1
- package/src/wayland-remote-desktop.mjs +654 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { image, multi, text } from '../util.mjs';
|
|
4
|
+
import {
|
|
5
|
+
clamp,
|
|
6
|
+
escapeAppleScript,
|
|
7
|
+
jsonResult,
|
|
8
|
+
optionalString,
|
|
9
|
+
removeTemp,
|
|
10
|
+
requireEnum,
|
|
11
|
+
runFile,
|
|
12
|
+
runOsa,
|
|
13
|
+
runWithInput,
|
|
14
|
+
spawnDetached,
|
|
15
|
+
tempDir,
|
|
16
|
+
} from './common.mjs';
|
|
17
|
+
|
|
18
|
+
function parseTsv(value, fields) {
|
|
19
|
+
return String(value || '').split(/\r?\n/).filter(Boolean).map(line => {
|
|
20
|
+
const parts = line.split('\t');
|
|
21
|
+
return Object.fromEntries(fields.map((field, index) => [field, parts[index] ?? '']));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function listWindows() {
|
|
26
|
+
const script = `
|
|
27
|
+
set output to ""
|
|
28
|
+
tell application "System Events"
|
|
29
|
+
repeat with p in (application processes whose background only is false)
|
|
30
|
+
try
|
|
31
|
+
set appName to name of p
|
|
32
|
+
set appPid to unix id of p
|
|
33
|
+
repeat with w in windows of p
|
|
34
|
+
try
|
|
35
|
+
set pos to position of w
|
|
36
|
+
set sz to size of w
|
|
37
|
+
set output to output & appPid & tab & appName & tab & (name of w) & tab & (item 1 of pos) & tab & (item 2 of pos) & tab & (item 1 of sz) & tab & (item 2 of sz) & linefeed
|
|
38
|
+
end try
|
|
39
|
+
end repeat
|
|
40
|
+
end try
|
|
41
|
+
end repeat
|
|
42
|
+
end tell
|
|
43
|
+
return output`;
|
|
44
|
+
const { stdout } = await runOsa(script, { label: 'list windows' });
|
|
45
|
+
const rows = parseTsv(stdout, ['pid','app','title','x','y','width','height']).map((row,index)=>({
|
|
46
|
+
id:`mac:${row.pid}:${index}`,pid:Number(row.pid),app:row.app,title:row.title,
|
|
47
|
+
x:Number(row.x),y:Number(row.y),width:Number(row.width),height:Number(row.height),
|
|
48
|
+
}));
|
|
49
|
+
return jsonResult(rows);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function processQuery(args) {
|
|
53
|
+
if (Number.isInteger(Number(args.pid))) return `first application process whose unix id is ${Number(args.pid)}`;
|
|
54
|
+
if (optionalString(args.app)) return `first application process whose name contains "${escapeAppleScript(args.app)}"`;
|
|
55
|
+
return 'first application process whose frontmost is true';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function windowQuery(args) {
|
|
59
|
+
const title = optionalString(args.window_title || args.windowTitle || args.title);
|
|
60
|
+
return title ? `first window whose name contains "${escapeAppleScript(title)}"` : 'front window';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function windowAction(args = {}) {
|
|
64
|
+
const action=requireEnum(args.action,'action',['focus','minimize','maximize','restore','move','resize','move_resize','close']);
|
|
65
|
+
const x=Number(args.x),y=Number(args.y),width=Number(args.width),height=Number(args.height);
|
|
66
|
+
if((action==='move'||action==='move_resize')&&(!Number.isFinite(x)||!Number.isFinite(y)))throw new Error('x and y are required for window move');
|
|
67
|
+
if((action==='resize'||action==='move_resize')&&(!Number.isFinite(width)||!Number.isFinite(height)||width<=0||height<=0))throw new Error('width and height must be positive for window resize');
|
|
68
|
+
const script=`(function(){
|
|
69
|
+
${macJxaTargetPrelude(args)}
|
|
70
|
+
function perform(e,wanted){let acts=[];try{acts=e.actions()}catch(_){};for(let i=0;i<acts.length;i++){let n='';try{n=String(acts[i].name())}catch(_){};if(n===wanted){acts[i].perform();return true}}return false}
|
|
71
|
+
function setAttr(e,n,v){try{e.attributes.byName(n).value=v;return true}catch(_){return false}}
|
|
72
|
+
const action=${JSON.stringify(action)};
|
|
73
|
+
if(action==='focus'){try{proc.frontmost=true}catch(_){};perform(win,'AXRaise')}
|
|
74
|
+
else if(action==='minimize'){if(!setAttr(win,'AXMinimized',true)){try{win.minimized=true}catch(_){throw new Error('Minimize unavailable')}}}
|
|
75
|
+
else if(action==='maximize'){if(!perform(win,'AXZoomWindow'))throw new Error('AXZoomWindow unavailable')}
|
|
76
|
+
else if(action==='restore'){setAttr(win,'AXMinimized',false);try{proc.frontmost=true}catch(_){};perform(win,'AXRaise')}
|
|
77
|
+
else if(action==='close'){if(!perform(win,'AXClose'))throw new Error('AXClose unavailable')}
|
|
78
|
+
else if(action==='move'){win.position=[${Number.isFinite(x)?Math.trunc(x):0},${Number.isFinite(y)?Math.trunc(y):0}]}
|
|
79
|
+
else if(action==='resize'){win.size=[${Number.isFinite(width)?Math.trunc(width):800},${Number.isFinite(height)?Math.trunc(height):600}]}
|
|
80
|
+
else if(action==='move_resize'){win.position=[${Number.isFinite(x)?Math.trunc(x):0},${Number.isFinite(y)?Math.trunc(y):0}];win.size=[${Number.isFinite(width)?Math.trunc(width):800},${Number.isFinite(height)?Math.trunc(height):600}]}
|
|
81
|
+
let p={};try{p=win.properties()}catch(_){};
|
|
82
|
+
return JSON.stringify({action,pid:Number(proc.unixId()),app:safeName(proc),window:safeName(win),x:Array.isArray(p.position)?Number(p.position[0]):null,y:Array.isArray(p.position)?Number(p.position[1]):null,width:Array.isArray(p.size)?Number(p.size[0]):null,height:Array.isArray(p.size)?Number(p.size[1]):null,backend:'macos-accessibility'});
|
|
83
|
+
})()`;
|
|
84
|
+
const {stdout}=await runOsa(script,{javascript:true,label:'window action',timeout:30_000});
|
|
85
|
+
let parsed;try{parsed=JSON.parse(stdout.trim())}catch{parsed={action,backend:'macos-accessibility'}}
|
|
86
|
+
return jsonResult(parsed);
|
|
87
|
+
}
|
|
88
|
+
function macJxaTargetPrelude(args = {}) {
|
|
89
|
+
const pid = Number.isInteger(Number(args.pid)) ? Number(args.pid) : 0;
|
|
90
|
+
const app = JSON.stringify(optionalString(args.app) || '');
|
|
91
|
+
const title = JSON.stringify(optionalString(args.window_title || args.windowTitle || args.title) || '');
|
|
92
|
+
return String.raw`
|
|
93
|
+
const se=Application('System Events');
|
|
94
|
+
const wantPid=${pid};
|
|
95
|
+
const wantApp=${app};
|
|
96
|
+
const wantTitle=${title};
|
|
97
|
+
function safeWindows(p){try{return p.windows()}catch(e){return []}}
|
|
98
|
+
function safeName(spec){try{return String(spec.name()||'')}catch(e){return ''}}
|
|
99
|
+
function chooseProcess(){
|
|
100
|
+
if(wantPid){
|
|
101
|
+
const rows=se.applicationProcesses.whose({unixId:wantPid})();
|
|
102
|
+
if(rows.length)return rows[0];
|
|
103
|
+
throw new Error('No matching UI process found');
|
|
104
|
+
}
|
|
105
|
+
if(wantApp){
|
|
106
|
+
const rows=se.applicationProcesses();
|
|
107
|
+
for(let i=0;i<rows.length;i++){
|
|
108
|
+
if(safeName(rows[i]).toLowerCase().includes(wantApp.toLowerCase()) && safeWindows(rows[i]).length)return rows[i];
|
|
109
|
+
}
|
|
110
|
+
throw new Error('No matching UI application found');
|
|
111
|
+
}
|
|
112
|
+
const front=se.applicationProcesses.whose({frontmost:true})();
|
|
113
|
+
let p=front.length?front[0]:null;
|
|
114
|
+
if(p && safeWindows(p).length)return p;
|
|
115
|
+
const preferredName=p?safeName(p):'';
|
|
116
|
+
const rows=se.applicationProcesses();
|
|
117
|
+
if(preferredName){
|
|
118
|
+
for(let i=0;i<rows.length;i++){
|
|
119
|
+
if(safeName(rows[i])===preferredName && safeWindows(rows[i]).length)return rows[i];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for(let i=0;i<rows.length;i++){
|
|
123
|
+
let background=true;
|
|
124
|
+
try{background=Boolean(rows[i].backgroundOnly())}catch(e){}
|
|
125
|
+
if(!background && safeWindows(rows[i]).length)return rows[i];
|
|
126
|
+
}
|
|
127
|
+
throw new Error('No accessible application window is available');
|
|
128
|
+
}
|
|
129
|
+
function chooseWindow(p){
|
|
130
|
+
const rows=safeWindows(p);
|
|
131
|
+
if(!rows.length)throw new Error('The selected application has no accessible windows');
|
|
132
|
+
if(wantTitle){
|
|
133
|
+
for(let i=0;i<rows.length;i++){
|
|
134
|
+
if(safeName(rows[i]).toLowerCase().includes(wantTitle.toLowerCase()))return rows[i];
|
|
135
|
+
}
|
|
136
|
+
throw new Error('No matching accessibility window found');
|
|
137
|
+
}
|
|
138
|
+
return rows[0];
|
|
139
|
+
}
|
|
140
|
+
const proc=chooseProcess();
|
|
141
|
+
const win=chooseWindow(proc);
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function uiSnapshot(args = {}) {
|
|
146
|
+
const maxNodes=clamp(args.max_nodes,500,1,5000);
|
|
147
|
+
const maxDepth=clamp(args.max_depth,8,1,32);
|
|
148
|
+
const script=`(function(){
|
|
149
|
+
${macJxaTargetPrelude(args)}
|
|
150
|
+
function scalar(v){
|
|
151
|
+
if(v===null||v===undefined)return '';
|
|
152
|
+
if(typeof v==='string'||typeof v==='number'||typeof v==='boolean')return v;
|
|
153
|
+
try{return String(v)}catch(e){return ''}
|
|
154
|
+
}
|
|
155
|
+
function vector(v){try{return Array.isArray(v)?v:(v==null?[]:[v])}catch(e){return []}}
|
|
156
|
+
function props(e){try{return e.properties()}catch(_){return {}}}
|
|
157
|
+
function children(e){try{return e.uiElements()}catch(_){return []}}
|
|
158
|
+
const appPid=Number(proc.unixId());
|
|
159
|
+
const appName=safeName(proc);
|
|
160
|
+
const windowName=safeName(win);
|
|
161
|
+
const out=[];
|
|
162
|
+
const winProps=props(win);
|
|
163
|
+
const winPos=vector(winProps.position),winSize=vector(winProps.size);
|
|
164
|
+
out.push({
|
|
165
|
+
id:'mac:'+appPid+':r',parent_id:null,index:0,depth:0,pid:appPid,app:appName,window:windowName,active:true,
|
|
166
|
+
role:'Window',localized_role:'window',name:windowName,value:'',focused:Boolean(winProps.focused),enabled:winProps.enabled!==false,
|
|
167
|
+
selected:Boolean(winProps.selected),frameworkId:'AX',x:Number(winPos[0]||0),y:Number(winPos[1]||0),width:Number(winSize[0]||0),height:Number(winSize[1]||0)
|
|
168
|
+
});
|
|
169
|
+
const stack=[];
|
|
170
|
+
const roots=children(win);
|
|
171
|
+
for(let i=roots.length-1;i>=0;i--)stack.push({e:roots[i],depth:1,path:'r.'+i,parent:'mac:'+appPid+':r'});
|
|
172
|
+
let index=1;
|
|
173
|
+
while(stack.length && out.length<${maxNodes}){
|
|
174
|
+
const item=stack.pop(),e=item.e,p=props(e),pos=vector(p.position),size=vector(p.size);
|
|
175
|
+
const role=String(scalar(p.role)).replace(/^AX/,'');
|
|
176
|
+
const subrole=String(scalar(p.subrole));
|
|
177
|
+
const name=scalar(p.title)||scalar(p.name)||scalar(p.accessibilityDescription)||scalar(p.description);
|
|
178
|
+
const rawValue=scalar(p.value);
|
|
179
|
+
const id='mac:'+appPid+':'+item.path;
|
|
180
|
+
out.push({
|
|
181
|
+
id,parent_id:item.parent,index,depth:item.depth,pid:appPid,app:appName,window:windowName,active:true,
|
|
182
|
+
role,localized_role:scalar(p.roleDescription),subrole,name,value:/secure/i.test(subrole)?'':rawValue,
|
|
183
|
+
focused:Boolean(p.focused),enabled:p.enabled!==false,selected:Boolean(p.selected),password:/secure/i.test(subrole),
|
|
184
|
+
help_text:scalar(p.help),range_min:scalar(p.minimumValue),range_max:scalar(p.maximumValue),frameworkId:'AX',
|
|
185
|
+
x:Number(pos[0]||0),y:Number(pos[1]||0),width:Number(size[0]||0),height:Number(size[1]||0)
|
|
186
|
+
});
|
|
187
|
+
index+=1;
|
|
188
|
+
if(item.depth<${maxDepth}){
|
|
189
|
+
const cs=children(e);
|
|
190
|
+
for(let i=cs.length-1;i>=0;i--)stack.push({e:cs[i],depth:item.depth+1,path:item.path+'.'+i,parent:id});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return JSON.stringify({platform:'darwin',count:out.length,nodes:out,truncated:stack.length>0,max_nodes:${maxNodes},max_depth:${maxDepth}});
|
|
194
|
+
})()`;
|
|
195
|
+
const {stdout}=await runOsa(script,{javascript:true,label:'UI snapshot',timeout:30_000});
|
|
196
|
+
let parsed;
|
|
197
|
+
try{parsed=stdout.trim()?JSON.parse(stdout.trim()):{platform:'darwin',count:0,nodes:[]};}
|
|
198
|
+
catch{throw new Error('Could not normalize macOS accessibility snapshot');}
|
|
199
|
+
return jsonResult(parsed);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function uiAction(args = {}) {
|
|
203
|
+
const action=requireEnum(args.action,'action',['click','invoke','focus','set_value','select','toggle','expand','collapse','scroll_into_view','set_range_value','add_to_selection','remove_from_selection']);
|
|
204
|
+
const id=optionalString(args.id);
|
|
205
|
+
const idParts=id?.startsWith('mac:')?id.split(':'):null;
|
|
206
|
+
const targetPid=idParts?.length>=3&&Number.isInteger(Number(idParts[1]))?Number(idParts[1]):null;
|
|
207
|
+
const token=idParts?.length>=3?idParts.slice(2).join(':'):null;
|
|
208
|
+
const pathToken=token?.startsWith('r')?token:null;
|
|
209
|
+
const legacyIndex=token&&!pathToken&&Number.isInteger(Number(token))?Number(token):null;
|
|
210
|
+
const targetArgs=targetPid?{...args,pid:targetPid}:args;
|
|
211
|
+
const name=JSON.stringify(optionalString(args.name)||'');
|
|
212
|
+
const role=JSON.stringify(optionalString(args.role)||'');
|
|
213
|
+
const value=JSON.stringify(String(args.value??''));
|
|
214
|
+
const numericValue=Number(args.value);
|
|
215
|
+
if(action==='set_range_value'&&!Number.isFinite(numericValue))throw new Error('set_range_value requires a finite numeric value');
|
|
216
|
+
const script=`(function(){
|
|
217
|
+
ObjC.import('ApplicationServices');
|
|
218
|
+
${macJxaTargetPrelude(targetArgs)}
|
|
219
|
+
const wantedPath=${JSON.stringify(pathToken||'')};
|
|
220
|
+
const legacyIndex=${legacyIndex==null?'null':legacyIndex};
|
|
221
|
+
const wantedName=${name};
|
|
222
|
+
const wantedRole=${role};
|
|
223
|
+
const action=${JSON.stringify(action)};
|
|
224
|
+
const inputValue=${value};
|
|
225
|
+
const numericValue=${Number.isFinite(numericValue)?numericValue:'null'};
|
|
226
|
+
function scalar(v){if(v===null||v===undefined)return '';if(typeof v==='string'||typeof v==='number'||typeof v==='boolean')return v;try{return String(v)}catch(e){return ''}}
|
|
227
|
+
function vector(v){try{return Array.isArray(v)?v:(v==null?[]:[v])}catch(e){return []}}
|
|
228
|
+
function props(e){try{return e.properties()}catch(_){return {}}}
|
|
229
|
+
function children(e){try{return e.uiElements()}catch(_){return []}}
|
|
230
|
+
function meta(e){
|
|
231
|
+
const p=props(e),pos=vector(p.position),size=vector(p.size);
|
|
232
|
+
return {name:scalar(p.title)||scalar(p.name)||scalar(p.accessibilityDescription)||scalar(p.description),role:String(scalar(p.role)).replace(/^AX/,''),x:Number(pos[0]||0),y:Number(pos[1]||0),width:Number(size[0]||0),height:Number(size[1]||0)};
|
|
233
|
+
}
|
|
234
|
+
function resolvePath(){
|
|
235
|
+
if(!wantedPath)return null;
|
|
236
|
+
if(wantedPath==='r')return win;
|
|
237
|
+
const bits=wantedPath.split('.');
|
|
238
|
+
if(bits[0]!=='r')return null;
|
|
239
|
+
let current=win;
|
|
240
|
+
for(let i=1;i<bits.length;i++){
|
|
241
|
+
const index=Number(bits[i]),rows=children(current);
|
|
242
|
+
if(!Number.isInteger(index)||index<0||index>=rows.length)return null;
|
|
243
|
+
current=rows[index];
|
|
244
|
+
}
|
|
245
|
+
return current;
|
|
246
|
+
}
|
|
247
|
+
function findTarget(){
|
|
248
|
+
const byPath=resolvePath();if(byPath)return byPath;
|
|
249
|
+
const stack=[];const roots=children(win);for(let i=roots.length-1;i>=0;i--)stack.push(roots[i]);
|
|
250
|
+
let seen=0;
|
|
251
|
+
while(stack.length&&seen<5000){
|
|
252
|
+
const e=stack.pop();seen+=1;
|
|
253
|
+
const m=meta(e);
|
|
254
|
+
if((legacyIndex!==null&&seen===legacyIndex)||(legacyIndex===null&&(!wantedName||m.name.toLowerCase().includes(wantedName.toLowerCase()))&&(!wantedRole||m.role.toLowerCase().includes(wantedRole.toLowerCase()))))return e;
|
|
255
|
+
const cs=children(e);for(let i=cs.length-1;i>=0;i--)stack.push(cs[i]);
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
function perform(e,wanted){
|
|
260
|
+
let acts=[];try{acts=e.actions()}catch(_){}
|
|
261
|
+
for(let i=0;i<acts.length;i++){let n='';try{n=String(acts[i].name())}catch(_){}if(n===wanted){acts[i].perform();return true}}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
function setAttribute(e,n,v){try{e.attributes.byName(n).value=v;return true}catch(_){return false}}
|
|
265
|
+
function coordinateClick(e){
|
|
266
|
+
const m=meta(e);if(!(m.width>0&&m.height>0))throw new Error('UI element has no clickable bounds');
|
|
267
|
+
const p=$.CGPointMake(m.x+m.width/2,m.y+m.height/2);
|
|
268
|
+
function post(t){const ev=$.CGEventCreateMouseEvent(null,t,p,$.kCGMouseButtonLeft);$.CGEventPost($.kCGHIDEventTap,ev)}
|
|
269
|
+
post($.kCGEventLeftMouseDown);post($.kCGEventLeftMouseUp);return 'coordinate';
|
|
270
|
+
}
|
|
271
|
+
const target=findTarget();if(!target)throw new Error('UI element not found');
|
|
272
|
+
let backend='accessibility';
|
|
273
|
+
if(action==='focus'){target.focused=true}
|
|
274
|
+
else if(action==='set_value'){target.value=inputValue}
|
|
275
|
+
else if(action==='set_range_value'){target.value=numericValue}
|
|
276
|
+
else if(action==='select'){if(!perform(target,'AXPick')){try{target.selected=true}catch(_){if(!perform(target,'AXPress'))throw new Error('Selection action unavailable')}}}
|
|
277
|
+
else if(action==='add_to_selection'){try{target.selected=true}catch(_){throw new Error('Selection add unavailable')}}
|
|
278
|
+
else if(action==='remove_from_selection'){try{target.selected=false}catch(_){throw new Error('Selection remove unavailable')}}
|
|
279
|
+
else if(action==='expand'){if(!setAttribute(target,'AXExpanded',true)&&!perform(target,'AXExpand'))throw new Error('Expand action unavailable')}
|
|
280
|
+
else if(action==='collapse'){if(!setAttribute(target,'AXExpanded',false)&&!perform(target,'AXCollapse'))throw new Error('Collapse action unavailable')}
|
|
281
|
+
else if(action==='scroll_into_view'){if(!perform(target,'AXScrollToVisible')){try{target.focused=true}catch(_){throw new Error('Scroll-to-visible action unavailable')}}}
|
|
282
|
+
else if(action==='click'||action==='invoke'||action==='toggle'){
|
|
283
|
+
if(!perform(target,'AXPress')){
|
|
284
|
+
if(action==='invoke')throw new Error('AXPress action unavailable');
|
|
285
|
+
backend=coordinateClick(target);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const m=meta(target);
|
|
289
|
+
return JSON.stringify({id:${JSON.stringify(id||'')},action,backend,name:m.name,role:m.role,x:m.x,y:m.y,width:m.width,height:m.height,pid:Number(proc.unixId()),app:safeName(proc),window:safeName(win)});
|
|
290
|
+
})()`;
|
|
291
|
+
const {stdout}=await runOsa(script,{javascript:true,label:'UI action',timeout:30_000});
|
|
292
|
+
let parsed;
|
|
293
|
+
try{parsed=JSON.parse(stdout.trim());}catch{parsed={action,backend:'accessibility'};}
|
|
294
|
+
return jsonResult(parsed);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function clipboard(args = {}) {
|
|
298
|
+
const action=requireEnum(args.action,'action',['read','write','clear']);
|
|
299
|
+
if(action==='read') return text((await runFile('/usr/bin/pbpaste',[],{label:'clipboard read'})).stdout);
|
|
300
|
+
const value=action==='clear'?'':String(args.text??args.value??'');
|
|
301
|
+
await runWithInput('/usr/bin/pbcopy',[],value,{label:'clipboard write'});
|
|
302
|
+
return jsonResult({action,length:value.length});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function keyboard(args = {}) {
|
|
306
|
+
const shortcut=optionalString(args.shortcut)||(Array.isArray(args.keys)?args.keys.join('+'):optionalString(args.key));
|
|
307
|
+
if(!shortcut)throw new Error('shortcut or key is required');
|
|
308
|
+
const tokens=shortcut.split('+').map(value=>value.trim()).filter(Boolean);
|
|
309
|
+
const key=tokens.at(-1);
|
|
310
|
+
const modifiers={ctrl:'control down',control:'control down',alt:'option down',option:'option down',shift:'shift down',cmd:'command down',command:'command down',meta:'command down',super:'command down'};
|
|
311
|
+
const mods=tokens.slice(0,-1).map(value=>modifiers[value.toLowerCase()]).filter(Boolean);
|
|
312
|
+
const codes={enter:36,return:36,esc:53,escape:53,tab:48,backspace:51,delete:117,up:126,down:125,left:123,right:124,home:115,end:119,pageup:116,pagedown:121,space:49};
|
|
313
|
+
const op=Number.isInteger(codes[key.toLowerCase()])?`key code ${codes[key.toLowerCase()]}`:`keystroke "${escapeAppleScript(key)}"`;
|
|
314
|
+
await runOsa(`tell application "System Events" to ${op}${mods.length?` using {${mods.join(',')}}`:''}`,{label:'keyboard'});
|
|
315
|
+
return text(`Sent ${shortcut}.`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function typeTextKeys(value) {
|
|
319
|
+
await runOsa(`tell application "System Events" to keystroke "${escapeAppleScript(value)}"`,{label:'type text',timeout:30_000});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function mouseInfo(button='left') {
|
|
323
|
+
const b=String(button).toLowerCase();
|
|
324
|
+
if(b==='right')return {button:'$.kCGMouseButtonRight',down:'$.kCGEventRightMouseDown',up:'$.kCGEventRightMouseUp',drag:'$.kCGEventRightMouseDragged'};
|
|
325
|
+
if(b==='middle')return {button:'$.kCGMouseButtonCenter',down:'$.kCGEventOtherMouseDown',up:'$.kCGEventOtherMouseUp',drag:'$.kCGEventOtherMouseDragged'};
|
|
326
|
+
return {button:'$.kCGMouseButtonLeft',down:'$.kCGEventLeftMouseDown',up:'$.kCGEventLeftMouseUp',drag:'$.kCGEventLeftMouseDragged'};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function pointer(args = {}) {
|
|
330
|
+
const action=requireEnum(args.action,'action',['move','click','double_click','right_click','down','up']);
|
|
331
|
+
const x=Number(args.x),y=Number(args.y);
|
|
332
|
+
const info=mouseInfo(action==='right_click'?'right':args.button);
|
|
333
|
+
const p=`$.CGPointMake(${Number.isFinite(x)?x:0},${Number.isFinite(y)?y:0})`;
|
|
334
|
+
const op=action==='move'?'post($.kCGEventMouseMoved,0);'
|
|
335
|
+
:action==='down'?`post(${info.down},1);`
|
|
336
|
+
:action==='up'?`post(${info.up},1);`
|
|
337
|
+
:action==='double_click'?`post(${info.down},1);post(${info.up},1);post(${info.down},2);post(${info.up},2);`
|
|
338
|
+
:`post(${info.down},1);post(${info.up},1);`;
|
|
339
|
+
const script=`ObjC.import('ApplicationServices');var p=${p};function post(t,c){var e=$.CGEventCreateMouseEvent(null,t,p,${info.button});if(c)$.CGEventSetIntegerValueField(e,$.kCGMouseEventClickState,c);$.CGEventPost($.kCGHIDEventTap,e);}${op}`;
|
|
340
|
+
await runOsa(script,{javascript:true,label:'pointer'});
|
|
341
|
+
return text(`Pointer ${action} completed.`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function dragDrop(args = {}) {
|
|
345
|
+
const info=mouseInfo(args.button);
|
|
346
|
+
const script=`ObjC.import('ApplicationServices');function post(t,x,y){var p=$.CGPointMake(x,y);var e=$.CGEventCreateMouseEvent(null,t,p,${info.button});$.CGEventPost($.kCGHIDEventTap,e);}post(${info.down},${args.from_x},${args.from_y});delay(${clamp(args.hold_ms,120,0,5000)/1000});post(${info.drag},${args.to_x},${args.to_y});delay(${clamp(args.duration_ms,120,0,5000)/1000});post(${info.up},${args.to_x},${args.to_y});`;
|
|
347
|
+
await runOsa(script,{javascript:true,label:'drag and drop'});
|
|
348
|
+
return jsonResult({from:[args.from_x,args.from_y],to:[args.to_x,args.to_y]});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function scroll(args = {}) {
|
|
352
|
+
const dx=Number(args.delta_x||0),dy=Number(args.delta_y??args.delta??0);
|
|
353
|
+
await runOsa(`ObjC.import('ApplicationServices');var e=$.CGEventCreateScrollWheelEvent(null,$.kCGScrollEventUnitPixel,2,${Math.trunc(-dy)},${Math.trunc(-dx)});$.CGEventPost($.kCGHIDEventTap,e);`,{javascript:true,label:'scroll'});
|
|
354
|
+
return jsonResult({delta_x:dx,delta_y:dy});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function cursorPosition() {
|
|
358
|
+
const script="ObjC.import('ApplicationServices');var e=$.CGEventCreate(null);var p=$.CGEventGetLocation(e);JSON.stringify({x:Number(p.x),y:Number(p.y)})";
|
|
359
|
+
const { stdout } = await runOsa(script, { javascript:true, label:'cursor position' });
|
|
360
|
+
let parsed;
|
|
361
|
+
try { parsed = JSON.parse(stdout.trim()); } catch { parsed = null; }
|
|
362
|
+
if (!parsed || !Number.isFinite(Number(parsed.x)) || !Number.isFinite(Number(parsed.y))) throw new Error('Could not read cursor position');
|
|
363
|
+
return jsonResult({ x:Number(parsed.x), y:Number(parsed.y) });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function displayInventory() {
|
|
367
|
+
const script=`ObjC.import('AppKit');var screens=$.NSScreen.screens;var main=$.NSScreen.mainScreen;var mf=main.frame;var top=Number(mf.origin.y)+Number(mf.size.height);var out=[];for(var i=0;i<Number(screens.count);i++){var s=screens.objectAtIndex(i);var f=s.frame;var name='Display '+(i+1);try{name=ObjC.unwrap(s.localizedName)}catch(e){};out.push({name:name,primary:Boolean(s.isEqual(main)),x:Number(f.origin.x),y:top-(Number(f.origin.y)+Number(f.size.height)),width:Number(f.size.width),height:Number(f.size.height),scale:Number(s.backingScaleFactor)})}JSON.stringify(out)`;
|
|
368
|
+
const {stdout}=await runOsa(script,{javascript:true,label:'display inventory',timeout:30_000});
|
|
369
|
+
let parsed;
|
|
370
|
+
try { parsed=JSON.parse(stdout.trim()); } catch { parsed=null; }
|
|
371
|
+
if (!Array.isArray(parsed)) throw new Error('Could not normalize macOS display inventory');
|
|
372
|
+
return jsonResult(parsed);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function screenshotRegion(args = {}) {
|
|
376
|
+
const x=Math.trunc(Number(args.x)),y=Math.trunc(Number(args.y)),width=Math.trunc(Number(args.width)),height=Math.trunc(Number(args.height));
|
|
377
|
+
if(![x,y,width,height].every(Number.isFinite)||width<=0||height<=0)throw new Error('x, y, width and height are required; width/height must be positive');
|
|
378
|
+
if(width>8192||height>8192||width*height>16_777_216)throw new Error('screenshot region is too large; width/height must be <= 8192 and area <= 16 megapixels');
|
|
379
|
+
const dir=await tempDir('remcp-region-'),target=path.join(dir,'region.png');
|
|
380
|
+
try{
|
|
381
|
+
await runFile('/usr/sbin/screencapture',['-x','-R',`${x},${y},${width},${height}`,target],{label:'screenshot region'});
|
|
382
|
+
const info=await stat(target);if(info.size>4*1024*1024)throw new Error(`captured region is ${info.size} bytes; request a smaller region so it fits the inline image limit`);
|
|
383
|
+
const data=await readFile(target);
|
|
384
|
+
return multi([{type:'text',text:`Captured ${width}x${height} at ${x},${y}.`},image(data.toString('base64'),'image/png')]);
|
|
385
|
+
}finally{await removeTemp(dir);}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function notification(args = {}) {
|
|
389
|
+
const title=escapeAppleScript(optionalString(args.title)||'ReMCP');
|
|
390
|
+
const body=escapeAppleScript(String(args.message??args.body??''));
|
|
391
|
+
await runOsa(`display notification "${body}" with title "${title}"`,{label:'notification'});
|
|
392
|
+
return text('Notification sent.');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function launchApp(app,argv=[],options={}) {
|
|
396
|
+
if(options.cwd){
|
|
397
|
+
const direct=path.isAbsolute(app)||String(app).includes('/');
|
|
398
|
+
if(!direct) throw new Error('cwd requires an executable/path on macOS; friendly application-name launch via open -a cannot guarantee the child working directory');
|
|
399
|
+
const pid=spawnDetached(app,argv,{cwd:options.cwd});
|
|
400
|
+
return `Launched ${app} (pid ${pid}) in ${options.cwd}.`;
|
|
401
|
+
}
|
|
402
|
+
const openArgs=['-a',app];
|
|
403
|
+
if(argv.length)openArgs.push('--args',...argv);
|
|
404
|
+
const pid=spawnDetached('/usr/bin/open',openArgs);
|
|
405
|
+
return `Launched ${app} (launcher pid ${pid}).`;
|
|
406
|
+
}
|
|
407
|
+
export async function openPath(target){spawnDetached('/usr/bin/open',[target]);}
|
|
408
|
+
export async function revealPath(target){spawnDetached('/usr/bin/open',['-R',target]);}
|