arena-bridge 0.6.3 → 0.6.5
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 +4 -2
- package/package.json +1 -1
- package/src/direct.mjs +1 -1
- package/src/hub.mjs +1 -1
- package/src/mac.mjs +121 -37
- package/src/mcp.mjs +1 -1
- package/src/prompt.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.6.5
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Peekaboo 4.9 compatibility: `peekaboo list` was removed in v4, so `mac_apps` now uses
|
|
11
|
+
`app list` and `mac_windows` uses `window list` (enumerating the running apps when no app is
|
|
12
|
+
given). `mac_menu` / `mac_menu_click` accept a process id, run against the local host
|
|
13
|
+
(`--no-remote`) and read the `menu_structure` payload; `mac_spaces` / `mac_space_switch` also
|
|
14
|
+
run locally, because a remote host cannot read or switch Spaces.
|
|
15
|
+
|
|
16
|
+
## 0.6.4
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- The accessibility element map is read from Peekaboo's real payload (`data.ui_elements`), so
|
|
20
|
+
`mac_see` and `mac_element` now return the controls of a window instead of an empty list.
|
|
21
|
+
Elements carry their label, description, identifier and value-settable flag.
|
|
22
|
+
- Commands follow the Peekaboo v4 grammar: `set-value "<value>" --on <id>`, `menu click --item|--path`
|
|
23
|
+
with an explicit app, `space switch --to <n> --foreground`, and the tree-only read
|
|
24
|
+
`see --tree --no-screenshot --json` (no screenshot artifact for pure lookups).
|
|
25
|
+
|
|
7
26
|
## 0.6.3
|
|
8
27
|
|
|
9
28
|
### Added
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license">
|
|
13
13
|
<img src="https://img.shields.io/badge/node-%3E%3D18-green.svg?style=flat-square" alt="node">
|
|
14
14
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen.svg?style=flat-square" alt="dependencies">
|
|
15
|
-
<img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<img src="https://img.shields.io/badge/tests-112%20passed-brightgreen.svg?style=flat-square" alt="tests">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -150,13 +150,14 @@ cookies и localStorage нет.
|
|
|
150
150
|
{"tool": "mac_set", "args": {"on": "B2", "value": "hello"}} // вписать значение напрямую
|
|
151
151
|
{"tool": "mac_type", "args": {"text": "hello", "submit": true}}
|
|
152
152
|
{"tool": "mac_key", "args": {"key": "cmd+s"}}
|
|
153
|
-
{"tool": "mac_menu_click", "args": {"
|
|
153
|
+
{"tool": "mac_menu_click", "args": {"app": "Safari", "item": "Save"}} // пункт меню по названию
|
|
154
154
|
{"tool": "mac_wait", "args": {"name": "Saved", "timeout": 20}} // дождаться результата
|
|
155
155
|
{"tool": "mac_apps", "args": {}} // что запущено
|
|
156
156
|
{"tool": "mac_permissions", "args": {}} // чего не хватает в macOS
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
Установить Peekaboo: `brew install peekaboo` (или бинарник в `/usr/local/bin/peekaboo`).
|
|
160
|
+
Проверено с Peekaboo 4.9 (4.3 тоже работает, но синтаксис команд обновлён под 4.9).
|
|
160
161
|
macOS спросит разрешения **Screen Recording** и **Accessibility** для терминала, из которого
|
|
161
162
|
запущен мост — без них скриншоты и клики не работают; `mac_permissions` покажет, что не так.
|
|
162
163
|
|
|
@@ -172,6 +173,7 @@ macOS отдаёт приложениям два разных слоя: карт
|
|
|
172
173
|
| как вписать текст | свайп, фокус, клавиатура | `mac_set {"on":"<id>","value":"..."}` — без фокуса и без клавиш |
|
|
173
174
|
| как открыть «Файл → Экспорт» | перемещать курсор по меню | `mac_menu_click {"item":"Экспорт"}` — по названию |
|
|
174
175
|
| как убедиться, что получилось | смотреть скриншот | `mac_wait {"name":"Готово"}` — ждать появления |
|
|
176
|
+
| как заполнить поле | свайп + клавиатура + фокус | `mac_set {"on":"<id>","value":"..."}` — значение пишется в элемент |
|
|
175
177
|
| чужой ввод и фокус | мешает вам | действия уходят в фоновое окно |
|
|
176
178
|
|
|
177
179
|
Практический вывод: с выданным Accessibility агент работает заметно точнее и тише. Разрешение
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arena-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Give a cloud AI agent (Arena.ai Agent Mode, Claude, Cursor, ...) controlled access to your own machine over HTTP/MCP: files, shell, Chrome and the macOS desktop, with an approval gate, trust windows and an audit log.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/direct.mjs
CHANGED
package/src/hub.mjs
CHANGED
package/src/mac.mjs
CHANGED
|
@@ -100,21 +100,31 @@ function parseJson(text) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function collectElements(node, out = [], depth = 0) {
|
|
103
|
-
if (!node || depth >
|
|
103
|
+
if (!node || depth > 8) return out;
|
|
104
104
|
if (Array.isArray(node)) {
|
|
105
105
|
for (const n of node) collectElements(n, out, depth + 1);
|
|
106
106
|
return out;
|
|
107
107
|
}
|
|
108
108
|
if (typeof node !== 'object') return out;
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
for (const key of ['ui_elements', 'elements']) {
|
|
110
|
+
const list = node[key];
|
|
111
|
+
if (!Array.isArray(list) || !list.length) continue;
|
|
112
|
+
for (const e of list) {
|
|
113
|
+
if (!e || typeof e !== 'object') continue;
|
|
114
|
+
const value = e.value;
|
|
111
115
|
out.push({
|
|
112
|
-
id: e.id ?? e.
|
|
113
|
-
role: e.role ?? e.elementRole ?? e.type ?? null,
|
|
116
|
+
id: e.id ?? e.element_id ?? e.elementId ?? null,
|
|
117
|
+
role: e.role ?? e.ax_role ?? e.elementRole ?? e.type ?? null,
|
|
114
118
|
title: e.title ?? e.label ?? e.name ?? e.placeholder ?? null,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
label: e.label ?? null,
|
|
120
|
+
description: e.description ?? null,
|
|
121
|
+
identifier: e.identifier ?? null,
|
|
122
|
+
value: typeof value === 'string' ? value.slice(0, 200) : (value ?? null),
|
|
123
|
+
bounds: e.bounds ?? e.frame ?? e.rect ?? null,
|
|
124
|
+
enabled: e.is_enabled ?? e.enabled ?? null,
|
|
125
|
+
actionable: e.is_actionable ?? null,
|
|
126
|
+
settable: e.is_value_settable ?? null,
|
|
127
|
+
shortcut: e.keyboard_shortcut ?? null,
|
|
118
128
|
});
|
|
119
129
|
}
|
|
120
130
|
return out;
|
|
@@ -178,10 +188,18 @@ export function macTools(cfg = {}) {
|
|
|
178
188
|
const seeVariants = (args, outPath) => {
|
|
179
189
|
const mode = args.mode === 'window' ? 'window' : 'screen';
|
|
180
190
|
const base = ['see', '--mode', mode, ...targetFlags(args)];
|
|
191
|
+
if (!outPath) {
|
|
192
|
+
return [
|
|
193
|
+
['see', '--tree', '--no-screenshot', '--json', ...targetFlags(args)],
|
|
194
|
+
['see', '--tree', '--no-screenshot', '--no-remote', '--json', ...targetFlags(args)],
|
|
195
|
+
['see', '--tree', '--json', ...targetFlags(args)],
|
|
196
|
+
['see', '--json', ...targetFlags(args)],
|
|
197
|
+
];
|
|
198
|
+
}
|
|
181
199
|
return [
|
|
182
|
-
[...base, '--json', '--path', outPath, '--snapshot'],
|
|
183
|
-
[...base, '--json', '--path', outPath, '--tree'],
|
|
184
200
|
[...base, '--json', '--path', outPath],
|
|
201
|
+
[...base, '--path', outPath, '--json'],
|
|
202
|
+
[...base, '--no-remote', '--json', '--path', outPath],
|
|
185
203
|
['image', '--mode', mode, ...targetFlags(args), '--json', '--path', outPath],
|
|
186
204
|
];
|
|
187
205
|
};
|
|
@@ -202,7 +220,10 @@ export function macTools(cfg = {}) {
|
|
|
202
220
|
|
|
203
221
|
const matchElement = (e, args) => {
|
|
204
222
|
const q = (v) => String(v ?? '').toLowerCase();
|
|
205
|
-
|
|
223
|
+
const text = [e.title, e.label, e.description, e.identifier, e.value, e.id]
|
|
224
|
+
.map(q)
|
|
225
|
+
.join('\n');
|
|
226
|
+
if (args.name && !text.includes(q(args.name))) return false;
|
|
206
227
|
if (args.role && !q(e.role).includes(q(args.role))) return false;
|
|
207
228
|
if (args.value && !q(e.value).includes(q(args.value))) return false;
|
|
208
229
|
return true;
|
|
@@ -210,11 +231,8 @@ export function macTools(cfg = {}) {
|
|
|
210
231
|
|
|
211
232
|
const searchPath = () => path.join(os.tmpdir(), 'arena-bridge-see', 'see.png');
|
|
212
233
|
|
|
213
|
-
const searchSee =
|
|
214
|
-
|
|
215
|
-
ensureDir(path.dirname(out));
|
|
216
|
-
return collectSee({ app: args.app, window_title: args.window_title, mode: args.mode || 'screen' }, out);
|
|
217
|
-
};
|
|
234
|
+
const searchSee = (args) =>
|
|
235
|
+
collectSee({ app: args.app, window_title: args.window_title, mode: args.mode || 'screen' }, null);
|
|
218
236
|
|
|
219
237
|
return [
|
|
220
238
|
{
|
|
@@ -451,9 +469,9 @@ export function macTools(cfg = {}) {
|
|
|
451
469
|
const res = await runVariants(
|
|
452
470
|
p.bin,
|
|
453
471
|
[
|
|
454
|
-
['list', 'apps', '--json'],
|
|
455
472
|
['app', 'list', '--json'],
|
|
456
|
-
['list', 'apps'],
|
|
473
|
+
['list', 'apps', '--json'],
|
|
474
|
+
['app', 'list'],
|
|
457
475
|
],
|
|
458
476
|
{ timeout: 30 },
|
|
459
477
|
);
|
|
@@ -476,12 +494,16 @@ export function macTools(cfg = {}) {
|
|
|
476
494
|
const p = checkPeek(peek());
|
|
477
495
|
if (!p.ok) return p;
|
|
478
496
|
const flags = args.app ? ['--app', String(args.app)] : [];
|
|
497
|
+
if (!flags.length) {
|
|
498
|
+
const windows = await windowsOfAllApps(p.bin);
|
|
499
|
+
return { ok: true, windows, source: 'per-app', hint: 'Pass app or pid to narrow the list down.' };
|
|
500
|
+
}
|
|
479
501
|
const res = await runVariants(
|
|
480
502
|
p.bin,
|
|
481
503
|
[
|
|
482
|
-
['list', 'windows', ...flags, '--json'],
|
|
483
504
|
['window', 'list', ...flags, '--json'],
|
|
484
|
-
['list', 'windows', ...flags],
|
|
505
|
+
['list', 'windows', ...flags, '--json'],
|
|
506
|
+
['window', 'list', ...flags],
|
|
485
507
|
],
|
|
486
508
|
{ timeout: 30 },
|
|
487
509
|
);
|
|
@@ -607,9 +629,10 @@ export function macTools(cfg = {}) {
|
|
|
607
629
|
const res = await runVariants(
|
|
608
630
|
p.bin,
|
|
609
631
|
[
|
|
632
|
+
['set-value', String(args.value), '--on', String(args.on), ...flags],
|
|
633
|
+
['set-value', String(args.value), '--on', String(args.on)],
|
|
610
634
|
['set-value', '--on', String(args.on), String(args.value), ...flags],
|
|
611
635
|
['set-value', '--on', String(args.on), '--value', String(args.value), ...flags],
|
|
612
|
-
['set-value', String(args.on), String(args.value), ...flags],
|
|
613
636
|
],
|
|
614
637
|
{ timeout: 30 },
|
|
615
638
|
);
|
|
@@ -625,10 +648,11 @@ export function macTools(cfg = {}) {
|
|
|
625
648
|
handler: async (args) => {
|
|
626
649
|
const p = checkPeek(peek());
|
|
627
650
|
if (!p.ok) return p;
|
|
628
|
-
const flags =
|
|
651
|
+
const flags = menuFlags(args);
|
|
629
652
|
const res = await runVariants(
|
|
630
653
|
p.bin,
|
|
631
654
|
[
|
|
655
|
+
['menu', 'list', ...flags, '--no-remote', '--json'],
|
|
632
656
|
['menu', 'list', ...flags, '--json'],
|
|
633
657
|
['menu', 'list', ...flags],
|
|
634
658
|
],
|
|
@@ -637,30 +661,52 @@ export function macTools(cfg = {}) {
|
|
|
637
661
|
const json = parseJson(res.stdout);
|
|
638
662
|
return {
|
|
639
663
|
ok: res.exit_code === 0,
|
|
640
|
-
menus: json ? (json.menus || json.data?.menus || json) : undefined,
|
|
664
|
+
menus: json ? (json.menus || json.data?.menus || json.data?.menu_structure || json) : undefined,
|
|
641
665
|
output: json ? undefined : res.stdout.slice(0, 4000),
|
|
642
666
|
error: res.exit_code === 0 ? undefined : (res.stderr || '').trim().slice(0, 600),
|
|
667
|
+
hint: res.exit_code === 0 ? undefined : menuHint(res),
|
|
643
668
|
};
|
|
644
669
|
},
|
|
645
670
|
},
|
|
646
671
|
{
|
|
647
672
|
name: 'mac_menu_click',
|
|
648
|
-
description:
|
|
673
|
+
description:
|
|
674
|
+
'Trigger a menu item by name (e.g. "Save", "Copy Link") or a nested path ("File > Export") — no pointer, no coordinates.',
|
|
649
675
|
approval: true,
|
|
650
|
-
schema: S(
|
|
651
|
-
|
|
676
|
+
schema: S(
|
|
677
|
+
{
|
|
678
|
+
item: { type: 'string', description: 'name of the menu item' },
|
|
679
|
+
path: { type: 'string', description: 'nested path, e.g. "File > Export"' },
|
|
680
|
+
app: { type: 'string', description: 'required: the background menu click needs a target app' },
|
|
681
|
+
pid: { type: 'number', description: 'process id, when the app name is ambiguous' },
|
|
682
|
+
},
|
|
683
|
+
[],
|
|
684
|
+
),
|
|
685
|
+
required: [],
|
|
652
686
|
handler: async (args) => {
|
|
653
687
|
const p = checkPeek(peek());
|
|
654
688
|
if (!p.ok) return p;
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
689
|
+
if (!args.item && !args.path) {
|
|
690
|
+
return { ok: false, error: 'missing_item', hint: 'Pass item ("Save") or path ("File > Export").' };
|
|
691
|
+
}
|
|
692
|
+
if (!args.app && !args.pid) {
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
error: 'missing_app',
|
|
696
|
+
hint: 'A background menu click needs an explicit app: mac_menu_click {"app":"Safari","item":"Save"}.',
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
const flags = menuFlags(args);
|
|
700
|
+
const variants = args.path
|
|
701
|
+
? [
|
|
702
|
+
['menu', 'click', '--path', String(args.path), ...flags, '--no-remote'],
|
|
703
|
+
['menu', 'click', '--path', String(args.path), ...flags],
|
|
704
|
+
]
|
|
705
|
+
: [
|
|
706
|
+
['menu', 'click', '--item', String(args.item), ...flags, '--no-remote'],
|
|
707
|
+
['menu', 'click', '--item', String(args.item), ...flags],
|
|
708
|
+
];
|
|
709
|
+
const res = await runVariants(p.bin, variants, { timeout: 30 });
|
|
664
710
|
return finish(res, 'menu-click');
|
|
665
711
|
},
|
|
666
712
|
},
|
|
@@ -676,6 +722,7 @@ export function macTools(cfg = {}) {
|
|
|
676
722
|
const res = await runVariants(
|
|
677
723
|
p.bin,
|
|
678
724
|
[
|
|
725
|
+
['space', 'list', '--no-remote', '--json'],
|
|
679
726
|
['space', 'list', '--json'],
|
|
680
727
|
['space', 'list'],
|
|
681
728
|
],
|
|
@@ -703,8 +750,8 @@ export function macTools(cfg = {}) {
|
|
|
703
750
|
const res = await runVariants(
|
|
704
751
|
p.bin,
|
|
705
752
|
[
|
|
706
|
-
['space', 'switch', '--to', String(args.to)],
|
|
707
|
-
['space', 'switch', String(args.to)],
|
|
753
|
+
['space', 'switch', '--to', String(args.to), '--foreground', '--no-remote'],
|
|
754
|
+
['space', 'switch', '--to', String(args.to), '--foreground'],
|
|
708
755
|
],
|
|
709
756
|
{ timeout: 30 },
|
|
710
757
|
);
|
|
@@ -784,6 +831,43 @@ export function macTools(cfg = {}) {
|
|
|
784
831
|
];
|
|
785
832
|
}
|
|
786
833
|
|
|
834
|
+
function menuFlags(args) {
|
|
835
|
+
const f = [];
|
|
836
|
+
if (args.app) f.push('--app', String(args.app));
|
|
837
|
+
if (args.pid) f.push('--pid', String(args.pid));
|
|
838
|
+
return f;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function menuHint(res) {
|
|
842
|
+
const text = (res.stderr || res.stdout || '').trim();
|
|
843
|
+
if (/multiple apps match/i.test(text)) {
|
|
844
|
+
return 'Several apps share that name — pass the process id from mac_apps: mac_menu {"pid":12345}.';
|
|
845
|
+
}
|
|
846
|
+
return text.slice(0, 600);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function windowsOfAllApps(bin) {
|
|
850
|
+
const res = await runVariants(
|
|
851
|
+
bin,
|
|
852
|
+
[
|
|
853
|
+
['app', 'list', '--json'],
|
|
854
|
+
['list', 'apps', '--json'],
|
|
855
|
+
],
|
|
856
|
+
{ timeout: 30 },
|
|
857
|
+
);
|
|
858
|
+
const json = parseJson(res.stdout);
|
|
859
|
+
const apps = json ? (json.apps || json.data?.apps || []) : [];
|
|
860
|
+
const out = [];
|
|
861
|
+
for (const app of Array.isArray(apps) ? apps.slice(0, 6) : []) {
|
|
862
|
+
if (!app || !app.pid) continue;
|
|
863
|
+
const w = await run(bin, ['window', 'list', '--pid', String(app.pid), '--json'], { timeout: 20 });
|
|
864
|
+
const data = parseJson(w.stdout);
|
|
865
|
+
const list = data ? (data.windows || data.data?.windows || []) : [];
|
|
866
|
+
for (const win of Array.isArray(list) ? list : []) out.push({ ...win, app: app.name, pid: app.pid });
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
|
|
787
871
|
function checkPeek(p) {
|
|
788
872
|
if (!p) return { ok: false, error: 'peekaboo_not_found', hint: NOT_FOUND_HINT };
|
|
789
873
|
return { ok: true, bin: p };
|
package/src/mcp.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export async function handleMcp(req, res, ctx) {
|
|
|
48
48
|
mcpResponse(id, {
|
|
49
49
|
protocolVersion: PROTOCOL_VERSION,
|
|
50
50
|
capabilities: { tools: { listChanged: false } },
|
|
51
|
-
serverInfo: { name: 'arena-bridge', version: '0.6.
|
|
51
|
+
serverInfo: { name: 'arena-bridge', version: '0.6.5' },
|
|
52
52
|
instructions:
|
|
53
53
|
'Tools operate the user\'s own machine. Dangerous operations may return ' +
|
|
54
54
|
'PROTECTED and require a local human approval.',
|
package/src/prompt.mjs
CHANGED
|
@@ -92,7 +92,7 @@ ${tools.join('\n')}
|
|
|
92
92
|
6. Stay out of the user's way: do not pass \`foreground\`, do not focus windows and do not move the
|
|
93
93
|
pointer unless the user asks for it. Background actions are delivered to the target window only.
|
|
94
94
|
7. \`mac_set\` writes a value straight into a control (no keystrokes, no focus change);
|
|
95
|
-
\`mac_menu_click\` triggers a menu item by its name — both are more reliable than typing.
|
|
95
|
+
\`mac_menu_click\` triggers a menu item by its name (it needs the \`app\` name) — both are more reliable than typing.
|
|
96
96
|
8. \`mac_wait\` waits for a control to appear or disappear instead of sleeping and guessing.
|
|
97
97
|
9. \`mac_spaces\` lists the macOS virtual desktops; \`mac_space_switch\` moves the agent to another
|
|
98
98
|
Space so the user keeps the screen they are looking at.
|