arena-bridge 0.6.2 → 0.6.3
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 +12 -0
- package/README.md +34 -7
- package/package.json +1 -1
- package/src/direct.mjs +1 -1
- package/src/hub.mjs +1 -1
- package/src/mac.mjs +249 -12
- package/src/mcp.mjs +1 -1
- package/src/prompt.mjs +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ 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.3
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Desktop control now works through the macOS accessibility API first, not by pixel guessing:
|
|
11
|
+
- `mac_element` finds a control by its visible name, role or value and returns its id;
|
|
12
|
+
- `mac_set` writes a value straight into a control — no keystrokes, no focus change;
|
|
13
|
+
- `mac_menu` / `mac_menu_click` list and trigger menu items by name;
|
|
14
|
+
- `mac_wait` waits until a control appears or disappears instead of sleeping;
|
|
15
|
+
- `mac_spaces` / `mac_space_switch` move the agent to another macOS Space (virtual desktop).
|
|
16
|
+
- `mac_see` asks for the accessibility element map explicitly and explains what to do when the
|
|
17
|
+
element list is empty (missing Accessibility permission).
|
|
18
|
+
|
|
7
19
|
## 0.6.2
|
|
8
20
|
|
|
9
21
|
### 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-110%20passed-brightgreen.svg?style=flat-square" alt="tests">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -82,7 +82,7 @@ arena-bridge setup --root ~/projects
|
|
|
82
82
|
| Шелл | `bash` `sysinfo` | команды и сведения о машине |
|
|
83
83
|
| Процессы | `start_process` `read_process_output` `write_process` `kill_process` | фоновые дев-серверы |
|
|
84
84
|
| Chrome | `chrome_launch` `chrome_profiles` `chrome_attach` `chrome_open` `chrome_navigate` `chrome_snapshot` `chrome_click` `chrome_type` `chrome_press` `chrome_screenshot` `chrome_evaluate` `chrome_tabs` `chrome_stop` | браузер через CDP |
|
|
85
|
-
| Десктоп | `mac_see` `mac_click` `mac_type` `mac_key` `mac_scroll` `mac_move` `mac_drag` `mac_apps` `mac_windows` `mac_app_launch` `mac_app_focus` `mac_app_quit` `mac_permissions` | macOS через Peekaboo |
|
|
85
|
+
| Десктоп | `mac_see` `mac_element` `mac_click` `mac_set` `mac_type` `mac_key` `mac_scroll` `mac_move` `mac_drag` `mac_menu` `mac_menu_click` `mac_wait` `mac_spaces` `mac_space_switch` `mac_apps` `mac_windows` `mac_app_launch` `mac_app_focus` `mac_app_quit` `mac_permissions` | macOS через Peekaboo |
|
|
86
86
|
|
|
87
87
|
Полный каталог с JSON-схемами: `GET /tools` или `arena-bridge tools`.
|
|
88
88
|
|
|
@@ -93,7 +93,11 @@ arena-bridge setup --root ~/projects
|
|
|
93
93
|
| прочитать страницу | `chrome_snapshot` (`{"mode":"ax"}` — роли и имена, `{"mode":"text"}`, `{"mode":"html"}`) |
|
|
94
94
|
| кликнуть на странице | `chrome_click` (CSS-селектор или видимый текст) |
|
|
95
95
|
| работать в вашем залогиненном Chrome | `chrome_attach` (порт 9222) и те же `chrome_*` |
|
|
96
|
-
| посмотреть на экран или нативное приложение | `mac_see` (скриншот + id
|
|
96
|
+
| посмотреть на экран или нативное приложение | `mac_see` (скриншот + id элементов) |
|
|
97
|
+
| найти контрол по названию, без скриншота | `mac_element` (через Accessibility API: имя, роль, значение) |
|
|
98
|
+
| заполнить поле, не печатая и не забирая фокус | `mac_set` (значение пишется напрямую в элемент) |
|
|
99
|
+
| открыть пункт меню | `mac_menu` (список) → `mac_menu_click` (по названию) |
|
|
100
|
+
| дождаться, пока контрол появится | `mac_wait` |
|
|
97
101
|
| кликнуть в нативном приложении | `mac_click` (id элемента, видимый текст или координаты) |
|
|
98
102
|
| напечатать | `mac_type`, затем `mac_key` для Enter/Tab/cmd+s |
|
|
99
103
|
| посмотреть файлы | `list_dir` (только верхний уровень; глубже — `{"recursive":true}`) |
|
|
@@ -141,9 +145,13 @@ cookies и localStorage нет.
|
|
|
141
145
|
|
|
142
146
|
```jsonc
|
|
143
147
|
{"tool": "mac_see", "args": {"app": "Safari", "mode": "window"}} // скриншот + id элементов
|
|
144
|
-
{"tool": "
|
|
148
|
+
{"tool": "mac_element", "args": {"name": "Save"}} // найти кнопку по названию
|
|
149
|
+
{"tool": "mac_click", "args": {"on": "B3"}} // клик по id из mac_see
|
|
150
|
+
{"tool": "mac_set", "args": {"on": "B2", "value": "hello"}} // вписать значение напрямую
|
|
145
151
|
{"tool": "mac_type", "args": {"text": "hello", "submit": true}}
|
|
146
152
|
{"tool": "mac_key", "args": {"key": "cmd+s"}}
|
|
153
|
+
{"tool": "mac_menu_click", "args": {"item": "Save", "app": "Safari"}} // пункт меню по названию
|
|
154
|
+
{"tool": "mac_wait", "args": {"name": "Saved", "timeout": 20}} // дождаться результата
|
|
147
155
|
{"tool": "mac_apps", "args": {}} // что запущено
|
|
148
156
|
{"tool": "mac_permissions", "args": {}} // чего не хватает в macOS
|
|
149
157
|
```
|
|
@@ -152,6 +160,25 @@ cookies и localStorage нет.
|
|
|
152
160
|
macOS спросит разрешения **Screen Recording** и **Accessibility** для терминала, из которого
|
|
153
161
|
запущен мост — без них скриншоты и клики не работают; `mac_permissions` покажет, что не так.
|
|
154
162
|
|
|
163
|
+
### Что даёт доступ Accessibility
|
|
164
|
+
|
|
165
|
+
macOS отдаёт приложениям два разных слоя: картинку экрана и **Accessibility API** — дерево
|
|
166
|
+
элементов интерфейса с названиями, ролями и значениями. Второй слой полезен агенту сильнее:
|
|
167
|
+
|
|
168
|
+
| | по картинке | через Accessibility API |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| как найти кнопку «Сохранить» | угадать координаты | `mac_element {"name":"Сохранить"}` → точный элемент |
|
|
171
|
+
| как нажать | клик по координате, может промахнуться | `mac_click {"on":"<id>"}` — по самому элементу |
|
|
172
|
+
| как вписать текст | свайп, фокус, клавиатура | `mac_set {"on":"<id>","value":"..."}` — без фокуса и без клавиш |
|
|
173
|
+
| как открыть «Файл → Экспорт» | перемещать курсор по меню | `mac_menu_click {"item":"Экспорт"}` — по названию |
|
|
174
|
+
| как убедиться, что получилось | смотреть скриншот | `mac_wait {"name":"Готово"}` — ждать появления |
|
|
175
|
+
| чужой ввод и фокус | мешает вам | действия уходят в фоновое окно |
|
|
176
|
+
|
|
177
|
+
Практический вывод: с выданным Accessibility агент работает заметно точнее и тише. Разрешение
|
|
178
|
+
даётся один раз в **Системные настройки → Конфиденциальность и безопасность → Универсальный
|
|
179
|
+
доступ** (для терминала, из которого запущен мост). `mac_permissions` показывает текущее
|
|
180
|
+
состояние разрешений.
|
|
181
|
+
|
|
155
182
|
---
|
|
156
183
|
|
|
157
184
|
## Доверенное окно: работать без подтверждений
|
|
@@ -199,7 +226,7 @@ arena-bridge browser close work # закрыть окно этого пр
|
|
|
199
226
|
| --- | --- |
|
|
200
227
|
| окно агента занимает экран | по умолчанию агент работает в фоне; видимое окно можно сделать маленьким и в углу: `chrome_launch {"headless":false,"width":900,"height":700,"x":-920,"y":40}` |
|
|
201
228
|
| агент перехватывает фокус и курсор | действия в нативных приложениях идут «в фон» — в нужное окно, без переключения на вас; курсор двигается только по вашей явной просьбе |
|
|
202
|
-
| окно агента на вашем рабочем столе | macOS умеет несколько рабочих столов:
|
|
229
|
+
| окно агента на вашем рабочем столе | macOS умеет несколько рабочих столов: агент переключается на отдельный стол через `mac_space_switch`, вы остаётесь на своём |
|
|
203
230
|
| нужен отдельный «второй монитор» | в macOS виртуальный монитор штатно не создаётся; сторонние утилиты (BetterDisplay, DisplayLink) умеют — тогда окно агента всегда там, где вы не работаете |
|
|
204
231
|
| нужен полностью независимый курсор и стол | только отдельная виртуальная машина; на 16 ГБ и Intel 2019 года она заметно тормозит и съедает 30–60 ГБ — как правило, не нужно |
|
|
205
232
|
|
|
@@ -295,8 +322,8 @@ npm run test:python # 44 теста Python: то же самое
|
|
|
295
322
|
src/ Node-реализация (zero dependencies)
|
|
296
323
|
ws.mjs минимальный WebSocket-клиент
|
|
297
324
|
cdp.mjs клиент Chrome DevTools Protocol
|
|
298
|
-
browser.mjs
|
|
299
|
-
mac.mjs
|
|
325
|
+
browser.mjs 13 инструментов chrome_*
|
|
326
|
+
mac.mjs 20 инструментов mac_* (Peekaboo, Accessibility API)
|
|
300
327
|
trust.mjs окно доверия
|
|
301
328
|
policy.mjs проверка путей (roots / protected)
|
|
302
329
|
engine.mjs каталог, гейт подтверждений, аудит
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arena-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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
|
@@ -175,6 +175,47 @@ export function macTools(cfg = {}) {
|
|
|
175
175
|
return f;
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
const seeVariants = (args, outPath) => {
|
|
179
|
+
const mode = args.mode === 'window' ? 'window' : 'screen';
|
|
180
|
+
const base = ['see', '--mode', mode, ...targetFlags(args)];
|
|
181
|
+
return [
|
|
182
|
+
[...base, '--json', '--path', outPath, '--snapshot'],
|
|
183
|
+
[...base, '--json', '--path', outPath, '--tree'],
|
|
184
|
+
[...base, '--json', '--path', outPath],
|
|
185
|
+
['image', '--mode', mode, ...targetFlags(args), '--json', '--path', outPath],
|
|
186
|
+
];
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const collectSee = async (args, outPath) => {
|
|
190
|
+
let best = null;
|
|
191
|
+
for (const variant of seeVariants(args, outPath)) {
|
|
192
|
+
const res = await run(peek(), variant, { timeout: 60 });
|
|
193
|
+
const json = parseJson(res.stdout);
|
|
194
|
+
const cand = { res, json, elements: collectElements(json), variant };
|
|
195
|
+
if (!best) best = cand;
|
|
196
|
+
if (res.exit_code === 0 && cand.elements.length) return cand;
|
|
197
|
+
if (res.exit_code === 0 && best.res.exit_code !== 0) best = cand;
|
|
198
|
+
if (res.exit_code !== 0 && !UNKNOWN.test(res.stderr || '')) break;
|
|
199
|
+
}
|
|
200
|
+
return best;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const matchElement = (e, args) => {
|
|
204
|
+
const q = (v) => String(v ?? '').toLowerCase();
|
|
205
|
+
if (args.name && !q(e.title).includes(q(args.name)) && !q(e.id).includes(q(args.name)) && !q(e.value).includes(q(args.name))) return false;
|
|
206
|
+
if (args.role && !q(e.role).includes(q(args.role))) return false;
|
|
207
|
+
if (args.value && !q(e.value).includes(q(args.value))) return false;
|
|
208
|
+
return true;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const searchPath = () => path.join(os.tmpdir(), 'arena-bridge-see', 'see.png');
|
|
212
|
+
|
|
213
|
+
const searchSee = async (args) => {
|
|
214
|
+
const out = searchPath();
|
|
215
|
+
ensureDir(path.dirname(out));
|
|
216
|
+
return collectSee({ app: args.app, window_title: args.window_title, mode: args.mode || 'screen' }, out);
|
|
217
|
+
};
|
|
218
|
+
|
|
178
219
|
return [
|
|
179
220
|
{
|
|
180
221
|
name: 'mac_see',
|
|
@@ -199,15 +240,8 @@ export function macTools(cfg = {}) {
|
|
|
199
240
|
const outPath = args.path ? checkPath(String(args.path), cfg, ctx.base) : { ok: true, abs: defaultShotPath(cfg) };
|
|
200
241
|
if (!outPath.ok) return { ok: false, error: outPath.error, reason: outPath.reason };
|
|
201
242
|
ensureDir(path.dirname(outPath.abs));
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
['see', '--mode', mode, ...targetFlags(args), '--json', '--path', outPath.abs],
|
|
205
|
-
['see', '--mode', mode, ...targetFlags(args), '--path', outPath.abs, '--json'],
|
|
206
|
-
['image', '--mode', mode, ...targetFlags(args), '--json', '--path', outPath.abs],
|
|
207
|
-
];
|
|
208
|
-
const res = await runVariants(p.bin, variants, { timeout: 60 });
|
|
209
|
-
const json = parseJson(res.stdout);
|
|
210
|
-
const elements = collectElements(json).slice(0, 300);
|
|
243
|
+
const seen = await collectSee(args, outPath.abs);
|
|
244
|
+
const elements = seen.elements.slice(0, 300);
|
|
211
245
|
const saved = fs.existsSync(outPath.abs);
|
|
212
246
|
if (!saved) {
|
|
213
247
|
const fb = await captureScreenFallback(outPath.abs);
|
|
@@ -215,7 +249,7 @@ export function macTools(cfg = {}) {
|
|
|
215
249
|
return {
|
|
216
250
|
ok: false,
|
|
217
251
|
error: 'capture_failed',
|
|
218
|
-
reason: (res.stderr || '').trim().slice(0, 800),
|
|
252
|
+
reason: (seen.res.stderr || '').trim().slice(0, 800),
|
|
219
253
|
hint: 'Grant Screen Recording permission to the app that runs the bridge (System Settings → Privacy & Security → Screen Recording).',
|
|
220
254
|
};
|
|
221
255
|
}
|
|
@@ -226,9 +260,12 @@ export function macTools(cfg = {}) {
|
|
|
226
260
|
saved,
|
|
227
261
|
elements,
|
|
228
262
|
element_count: elements.length,
|
|
229
|
-
hint: 'Click by element id: mac_click {"on":"<id>"} or by visible text: mac_click {"text":"Save"}',
|
|
263
|
+
hint: 'Click by element id: mac_click {"on":"<id>"} or by visible text: mac_click {"text":"Save"}. Find elements by name with mac_element.',
|
|
230
264
|
};
|
|
231
|
-
if (
|
|
265
|
+
if (!elements.length) {
|
|
266
|
+
out.hint += ' No elements found — run mac_permissions: the element list comes from the macOS Accessibility API.';
|
|
267
|
+
}
|
|
268
|
+
if (args.include_raw && seen.json) out.raw = seen.json;
|
|
232
269
|
return out;
|
|
233
270
|
},
|
|
234
271
|
},
|
|
@@ -517,6 +554,206 @@ export function macTools(cfg = {}) {
|
|
|
517
554
|
return finish(res, 'quit');
|
|
518
555
|
},
|
|
519
556
|
},
|
|
557
|
+
{
|
|
558
|
+
name: 'mac_element',
|
|
559
|
+
description:
|
|
560
|
+
'Find interface elements by visible name, role or value through the macOS accessibility API. No screenshots, no pixel guessing. Read-only.',
|
|
561
|
+
approval: false,
|
|
562
|
+
schema: S(
|
|
563
|
+
{
|
|
564
|
+
name: { type: 'string', description: 'visible text or title to look for' },
|
|
565
|
+
role: { type: 'string', description: 'e.g. button, textField, menuItem, staticText' },
|
|
566
|
+
value: { type: 'string' },
|
|
567
|
+
app: { type: 'string' },
|
|
568
|
+
window_title: { type: 'string' },
|
|
569
|
+
limit: { type: 'number' },
|
|
570
|
+
},
|
|
571
|
+
[],
|
|
572
|
+
),
|
|
573
|
+
required: [],
|
|
574
|
+
handler: async (args) => {
|
|
575
|
+
const p = checkPeek(peek());
|
|
576
|
+
if (!p.ok) return p;
|
|
577
|
+
const seen = await searchSee(args);
|
|
578
|
+
const matches = seen.elements.filter((e) => matchElement(e, args)).slice(0, Number(args.limit) || 25);
|
|
579
|
+
return {
|
|
580
|
+
ok: true,
|
|
581
|
+
matches,
|
|
582
|
+
count: matches.length,
|
|
583
|
+
scanned: seen.elements.length,
|
|
584
|
+
hint: 'Act on a match by id: mac_click {"on":"<id>"} or mac_set {"on":"<id>","value":"..."}.',
|
|
585
|
+
};
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: 'mac_set',
|
|
590
|
+
description:
|
|
591
|
+
'Set the value of an element directly through the accessibility API — fills a field without focusing the window and without simulated keystrokes.',
|
|
592
|
+
approval: true,
|
|
593
|
+
schema: S(
|
|
594
|
+
{
|
|
595
|
+
on: { type: 'string', description: 'element id from mac_see / mac_element' },
|
|
596
|
+
value: { type: 'string' },
|
|
597
|
+
app: { type: 'string' },
|
|
598
|
+
window_id: { type: 'string' },
|
|
599
|
+
},
|
|
600
|
+
['on', 'value'],
|
|
601
|
+
),
|
|
602
|
+
required: ['on', 'value'],
|
|
603
|
+
handler: async (args) => {
|
|
604
|
+
const p = checkPeek(peek());
|
|
605
|
+
if (!p.ok) return p;
|
|
606
|
+
const flags = targetFlags(args);
|
|
607
|
+
const res = await runVariants(
|
|
608
|
+
p.bin,
|
|
609
|
+
[
|
|
610
|
+
['set-value', '--on', String(args.on), String(args.value), ...flags],
|
|
611
|
+
['set-value', '--on', String(args.on), '--value', String(args.value), ...flags],
|
|
612
|
+
['set-value', String(args.on), String(args.value), ...flags],
|
|
613
|
+
],
|
|
614
|
+
{ timeout: 30 },
|
|
615
|
+
);
|
|
616
|
+
return finish(res, 'set-value');
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'mac_menu',
|
|
621
|
+
description: 'List the menu items of an application through the accessibility API. Read-only.',
|
|
622
|
+
approval: false,
|
|
623
|
+
schema: S({ app: { type: 'string' } }, []),
|
|
624
|
+
required: [],
|
|
625
|
+
handler: async (args) => {
|
|
626
|
+
const p = checkPeek(peek());
|
|
627
|
+
if (!p.ok) return p;
|
|
628
|
+
const flags = args.app ? ['--app', String(args.app)] : [];
|
|
629
|
+
const res = await runVariants(
|
|
630
|
+
p.bin,
|
|
631
|
+
[
|
|
632
|
+
['menu', 'list', ...flags, '--json'],
|
|
633
|
+
['menu', 'list', ...flags],
|
|
634
|
+
],
|
|
635
|
+
{ timeout: 30 },
|
|
636
|
+
);
|
|
637
|
+
const json = parseJson(res.stdout);
|
|
638
|
+
return {
|
|
639
|
+
ok: res.exit_code === 0,
|
|
640
|
+
menus: json ? (json.menus || json.data?.menus || json) : undefined,
|
|
641
|
+
output: json ? undefined : res.stdout.slice(0, 4000),
|
|
642
|
+
error: res.exit_code === 0 ? undefined : (res.stderr || '').trim().slice(0, 600),
|
|
643
|
+
};
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: 'mac_menu_click',
|
|
648
|
+
description: 'Click a menu item by its visible name (e.g. "Save", "Copy Link") — no pointer, no coordinates.',
|
|
649
|
+
approval: true,
|
|
650
|
+
schema: S({ item: { type: 'string' }, app: { type: 'string' } }, ['item']),
|
|
651
|
+
required: ['item'],
|
|
652
|
+
handler: async (args) => {
|
|
653
|
+
const p = checkPeek(peek());
|
|
654
|
+
if (!p.ok) return p;
|
|
655
|
+
const flags = args.app ? ['--app', String(args.app)] : [];
|
|
656
|
+
const res = await runVariants(
|
|
657
|
+
p.bin,
|
|
658
|
+
[
|
|
659
|
+
['menu', 'click', '--item', String(args.item), ...flags],
|
|
660
|
+
['menu', 'click', String(args.item), ...flags],
|
|
661
|
+
],
|
|
662
|
+
{ timeout: 30 },
|
|
663
|
+
);
|
|
664
|
+
return finish(res, 'menu-click');
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: 'mac_spaces',
|
|
669
|
+
description: 'List the macOS Spaces (virtual desktops) and which one is active. Read-only.',
|
|
670
|
+
approval: false,
|
|
671
|
+
schema: S({}, []),
|
|
672
|
+
required: [],
|
|
673
|
+
handler: async () => {
|
|
674
|
+
const p = checkPeek(peek());
|
|
675
|
+
if (!p.ok) return p;
|
|
676
|
+
const res = await runVariants(
|
|
677
|
+
p.bin,
|
|
678
|
+
[
|
|
679
|
+
['space', 'list', '--json'],
|
|
680
|
+
['space', 'list'],
|
|
681
|
+
],
|
|
682
|
+
{ timeout: 30 },
|
|
683
|
+
);
|
|
684
|
+
const json = parseJson(res.stdout);
|
|
685
|
+
return {
|
|
686
|
+
ok: res.exit_code === 0,
|
|
687
|
+
spaces: json ? (json.spaces || json.data?.spaces || json) : undefined,
|
|
688
|
+
output: json ? undefined : res.stdout.slice(0, 4000),
|
|
689
|
+
error: res.exit_code === 0 ? undefined : (res.stderr || '').trim().slice(0, 600),
|
|
690
|
+
};
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: 'mac_space_switch',
|
|
695
|
+
description:
|
|
696
|
+
'Move to another Space so the agent works on a separate virtual desktop and the user keeps the screen they are on.',
|
|
697
|
+
approval: true,
|
|
698
|
+
schema: S({ to: { type: 'string', description: 'space id or index from mac_spaces' } }, ['to']),
|
|
699
|
+
required: ['to'],
|
|
700
|
+
handler: async (args) => {
|
|
701
|
+
const p = checkPeek(peek());
|
|
702
|
+
if (!p.ok) return p;
|
|
703
|
+
const res = await runVariants(
|
|
704
|
+
p.bin,
|
|
705
|
+
[
|
|
706
|
+
['space', 'switch', '--to', String(args.to)],
|
|
707
|
+
['space', 'switch', String(args.to)],
|
|
708
|
+
],
|
|
709
|
+
{ timeout: 30 },
|
|
710
|
+
);
|
|
711
|
+
return finish(res, 'space-switch');
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
name: 'mac_wait',
|
|
716
|
+
description:
|
|
717
|
+
'Wait until an element with the given name/role appears (or disappears) and report what was seen. Read-only.',
|
|
718
|
+
approval: false,
|
|
719
|
+
schema: S(
|
|
720
|
+
{
|
|
721
|
+
name: { type: 'string' },
|
|
722
|
+
role: { type: 'string' },
|
|
723
|
+
value: { type: 'string' },
|
|
724
|
+
disappear: { type: 'boolean', description: 'wait until the element is gone' },
|
|
725
|
+
timeout: { type: 'number', description: 'seconds, default 20, max 120' },
|
|
726
|
+
app: { type: 'string' },
|
|
727
|
+
},
|
|
728
|
+
[],
|
|
729
|
+
),
|
|
730
|
+
required: [],
|
|
731
|
+
handler: async (args) => {
|
|
732
|
+
const p = checkPeek(peek());
|
|
733
|
+
if (!p.ok) return p;
|
|
734
|
+
const limit = Math.min(120, Math.max(1, Number(args.timeout) || 20));
|
|
735
|
+
const deadline = Date.now() + limit * 1000;
|
|
736
|
+
let scanned = 0;
|
|
737
|
+
while (Date.now() < deadline) {
|
|
738
|
+
const seen = await searchSee(args);
|
|
739
|
+
scanned = seen.elements.length;
|
|
740
|
+
const hit = seen.elements.find((e) => matchElement(e, args));
|
|
741
|
+
if (args.disappear) {
|
|
742
|
+
if (!hit) return { ok: true, gone: true, found: false, scanned };
|
|
743
|
+
} else if (hit) {
|
|
744
|
+
return { ok: true, found: true, element: hit, scanned };
|
|
745
|
+
}
|
|
746
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
found: false,
|
|
751
|
+
error: 'not_found',
|
|
752
|
+
reason: `no matching element within ${limit}s`,
|
|
753
|
+
scanned,
|
|
754
|
+
};
|
|
755
|
+
},
|
|
756
|
+
},
|
|
520
757
|
{
|
|
521
758
|
name: 'mac_permissions',
|
|
522
759
|
description:
|
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.3' },
|
|
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
|
@@ -59,6 +59,11 @@ ${tools.join('\n')}
|
|
|
59
59
|
| click a native app | \`mac_click\` with the element id from \`mac_see\`, a visible text, or \`{"x":..,"y":..}\` |
|
|
60
60
|
| type into a native app | \`mac_type\`, then \`mac_key\` for Enter/Tab/cmd+s |
|
|
61
61
|
| list or open applications | \`mac_apps\`, \`mac_app_launch\`, \`mac_app_focus\` |
|
|
62
|
+
| find a control by its name, without a screenshot | \`mac_element\` (accessibility API: name, role, value) |
|
|
63
|
+
| fill a field without typing and without focus | \`mac_set\` (sets the value through the accessibility API) |
|
|
64
|
+
| use the application menu | \`mac_menu\` to list, \`mac_menu_click\` to trigger an item by name |
|
|
65
|
+
| wait for a control to appear or vanish | \`mac_wait\` |
|
|
66
|
+
| work on a separate virtual desktop | \`mac_spaces\`, \`mac_space_switch\` |
|
|
62
67
|
| look at files | \`list_dir\` (top level only; add \`{"recursive":true}\` to go deeper), \`read_file\`, \`search_files\` |
|
|
63
68
|
|
|
64
69
|
## Browser control (Chrome over CDP)
|
|
@@ -75,6 +80,8 @@ ${tools.join('\n')}
|
|
|
75
80
|
6. A visible window can be kept small and out of the way: \`chrome_launch {"headless":false,"width":900,"height":700,"x":-920,"y":40}\`.
|
|
76
81
|
|
|
77
82
|
## Desktop control (macOS, through Peekaboo)
|
|
83
|
+
0. Prefer the accessibility API over pixels: controls are recognised by name and role, not by guessing
|
|
84
|
+
coordinates. \`mac_element\` finds them, \`mac_see\` returns the same elements with ids plus a screenshot.
|
|
78
85
|
1. \`mac_see\` is the entry point: it takes a screenshot of the screen (or of one app window with
|
|
79
86
|
\`{"app": "Safari", "mode": "window"}\`) and returns the clickable elements with their ids.
|
|
80
87
|
2. Then \`mac_click\` (by id, by visible text or by coordinates) → \`mac_type\` → \`mac_key\` → \`mac_scroll\`.
|
|
@@ -84,6 +91,11 @@ ${tools.join('\n')}
|
|
|
84
91
|
5. Screenshots are saved as PNG inside the authorized roots; read them back with \`file_get\`.
|
|
85
92
|
6. Stay out of the user's way: do not pass \`foreground\`, do not focus windows and do not move the
|
|
86
93
|
pointer unless the user asks for it. Background actions are delivered to the target window only.
|
|
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.
|
|
96
|
+
8. \`mac_wait\` waits for a control to appear or disappear instead of sleeping and guessing.
|
|
97
|
+
9. \`mac_spaces\` lists the macOS virtual desktops; \`mac_space_switch\` moves the agent to another
|
|
98
|
+
Space so the user keeps the screen they are looking at.
|
|
87
99
|
|
|
88
100
|
## Approvals
|
|
89
101
|
${trustLine}
|