arena-bridge 0.6.2 → 0.6.4
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 +22 -0
- package/README.md +35 -7
- package/package.json +1 -1
- package/src/direct.mjs +1 -1
- package/src/hub.mjs +1 -1
- package/src/mac.mjs +294 -20
- package/src/mcp.mjs +1 -1
- package/src/prompt.mjs +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.4
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- The accessibility element map is read from Peekaboo's real payload (`data.ui_elements`), so
|
|
11
|
+
`mac_see` and `mac_element` now return the controls of a window instead of an empty list.
|
|
12
|
+
Elements carry their label, description, identifier and value-settable flag.
|
|
13
|
+
- Commands follow the Peekaboo v4 grammar: `set-value "<value>" --on <id>`, `menu click --item|--path`
|
|
14
|
+
with an explicit app, `space switch --to <n> --foreground`, and the tree-only read
|
|
15
|
+
`see --tree --no-screenshot --json` (no screenshot artifact for pure lookups).
|
|
16
|
+
|
|
17
|
+
## 0.6.3
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Desktop control now works through the macOS accessibility API first, not by pixel guessing:
|
|
21
|
+
- `mac_element` finds a control by its visible name, role or value and returns its id;
|
|
22
|
+
- `mac_set` writes a value straight into a control — no keystrokes, no focus change;
|
|
23
|
+
- `mac_menu` / `mac_menu_click` list and trigger menu items by name;
|
|
24
|
+
- `mac_wait` waits until a control appears or disappears instead of sleeping;
|
|
25
|
+
- `mac_spaces` / `mac_space_switch` move the agent to another macOS Space (virtual desktop).
|
|
26
|
+
- `mac_see` asks for the accessibility element map explicitly and explains what to do when the
|
|
27
|
+
element list is empty (missing Accessibility permission).
|
|
28
|
+
|
|
7
29
|
## 0.6.2
|
|
8
30
|
|
|
9
31
|
### 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": {"app": "Safari", "item": "Save"}} // пункт меню по названию
|
|
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,26 @@ 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
|
+
| как заполнить поле | свайп + клавиатура + фокус | `mac_set {"on":"<id>","value":"..."}` — значение пишется в элемент |
|
|
176
|
+
| чужой ввод и фокус | мешает вам | действия уходят в фоновое окно |
|
|
177
|
+
|
|
178
|
+
Практический вывод: с выданным Accessibility агент работает заметно точнее и тише. Разрешение
|
|
179
|
+
даётся один раз в **Системные настройки → Конфиденциальность и безопасность → Универсальный
|
|
180
|
+
доступ** (для терминала, из которого запущен мост). `mac_permissions` показывает текущее
|
|
181
|
+
состояние разрешений.
|
|
182
|
+
|
|
155
183
|
---
|
|
156
184
|
|
|
157
185
|
## Доверенное окно: работать без подтверждений
|
|
@@ -199,7 +227,7 @@ arena-bridge browser close work # закрыть окно этого пр
|
|
|
199
227
|
| --- | --- |
|
|
200
228
|
| окно агента занимает экран | по умолчанию агент работает в фоне; видимое окно можно сделать маленьким и в углу: `chrome_launch {"headless":false,"width":900,"height":700,"x":-920,"y":40}` |
|
|
201
229
|
| агент перехватывает фокус и курсор | действия в нативных приложениях идут «в фон» — в нужное окно, без переключения на вас; курсор двигается только по вашей явной просьбе |
|
|
202
|
-
| окно агента на вашем рабочем столе | macOS умеет несколько рабочих столов:
|
|
230
|
+
| окно агента на вашем рабочем столе | macOS умеет несколько рабочих столов: агент переключается на отдельный стол через `mac_space_switch`, вы остаётесь на своём |
|
|
203
231
|
| нужен отдельный «второй монитор» | в macOS виртуальный монитор штатно не создаётся; сторонние утилиты (BetterDisplay, DisplayLink) умеют — тогда окно агента всегда там, где вы не работаете |
|
|
204
232
|
| нужен полностью независимый курсор и стол | только отдельная виртуальная машина; на 16 ГБ и Intel 2019 года она заметно тормозит и съедает 30–60 ГБ — как правило, не нужно |
|
|
205
233
|
|
|
@@ -295,8 +323,8 @@ npm run test:python # 44 теста Python: то же самое
|
|
|
295
323
|
src/ Node-реализация (zero dependencies)
|
|
296
324
|
ws.mjs минимальный WebSocket-клиент
|
|
297
325
|
cdp.mjs клиент Chrome DevTools Protocol
|
|
298
|
-
browser.mjs
|
|
299
|
-
mac.mjs
|
|
326
|
+
browser.mjs 13 инструментов chrome_*
|
|
327
|
+
mac.mjs 20 инструментов mac_* (Peekaboo, Accessibility API)
|
|
300
328
|
trust.mjs окно доверия
|
|
301
329
|
policy.mjs проверка путей (roots / protected)
|
|
302
330
|
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.4",
|
|
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;
|
|
@@ -175,6 +185,53 @@ export function macTools(cfg = {}) {
|
|
|
175
185
|
return f;
|
|
176
186
|
};
|
|
177
187
|
|
|
188
|
+
const seeVariants = (args, outPath) => {
|
|
189
|
+
const mode = args.mode === 'window' ? 'window' : 'screen';
|
|
190
|
+
const base = ['see', '--mode', mode, ...targetFlags(args)];
|
|
191
|
+
if (!outPath) {
|
|
192
|
+
return [
|
|
193
|
+
['see', '--tree', '--no-screenshot', '--json', ...targetFlags(args)],
|
|
194
|
+
['see', '--tree', '--json', ...targetFlags(args)],
|
|
195
|
+
['see', '--json', ...targetFlags(args)],
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
return [
|
|
199
|
+
[...base, '--json', '--path', outPath],
|
|
200
|
+
[...base, '--path', outPath, '--json'],
|
|
201
|
+
['image', '--mode', mode, ...targetFlags(args), '--json', '--path', outPath],
|
|
202
|
+
];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const collectSee = async (args, outPath) => {
|
|
206
|
+
let best = null;
|
|
207
|
+
for (const variant of seeVariants(args, outPath)) {
|
|
208
|
+
const res = await run(peek(), variant, { timeout: 60 });
|
|
209
|
+
const json = parseJson(res.stdout);
|
|
210
|
+
const cand = { res, json, elements: collectElements(json), variant };
|
|
211
|
+
if (!best) best = cand;
|
|
212
|
+
if (res.exit_code === 0 && cand.elements.length) return cand;
|
|
213
|
+
if (res.exit_code === 0 && best.res.exit_code !== 0) best = cand;
|
|
214
|
+
if (res.exit_code !== 0 && !UNKNOWN.test(res.stderr || '')) break;
|
|
215
|
+
}
|
|
216
|
+
return best;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const matchElement = (e, args) => {
|
|
220
|
+
const q = (v) => String(v ?? '').toLowerCase();
|
|
221
|
+
const text = [e.title, e.label, e.description, e.identifier, e.value, e.id]
|
|
222
|
+
.map(q)
|
|
223
|
+
.join('\n');
|
|
224
|
+
if (args.name && !text.includes(q(args.name))) return false;
|
|
225
|
+
if (args.role && !q(e.role).includes(q(args.role))) return false;
|
|
226
|
+
if (args.value && !q(e.value).includes(q(args.value))) return false;
|
|
227
|
+
return true;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const searchPath = () => path.join(os.tmpdir(), 'arena-bridge-see', 'see.png');
|
|
231
|
+
|
|
232
|
+
const searchSee = (args) =>
|
|
233
|
+
collectSee({ app: args.app, window_title: args.window_title, mode: args.mode || 'screen' }, null);
|
|
234
|
+
|
|
178
235
|
return [
|
|
179
236
|
{
|
|
180
237
|
name: 'mac_see',
|
|
@@ -199,15 +256,8 @@ export function macTools(cfg = {}) {
|
|
|
199
256
|
const outPath = args.path ? checkPath(String(args.path), cfg, ctx.base) : { ok: true, abs: defaultShotPath(cfg) };
|
|
200
257
|
if (!outPath.ok) return { ok: false, error: outPath.error, reason: outPath.reason };
|
|
201
258
|
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);
|
|
259
|
+
const seen = await collectSee(args, outPath.abs);
|
|
260
|
+
const elements = seen.elements.slice(0, 300);
|
|
211
261
|
const saved = fs.existsSync(outPath.abs);
|
|
212
262
|
if (!saved) {
|
|
213
263
|
const fb = await captureScreenFallback(outPath.abs);
|
|
@@ -215,7 +265,7 @@ export function macTools(cfg = {}) {
|
|
|
215
265
|
return {
|
|
216
266
|
ok: false,
|
|
217
267
|
error: 'capture_failed',
|
|
218
|
-
reason: (res.stderr || '').trim().slice(0, 800),
|
|
268
|
+
reason: (seen.res.stderr || '').trim().slice(0, 800),
|
|
219
269
|
hint: 'Grant Screen Recording permission to the app that runs the bridge (System Settings → Privacy & Security → Screen Recording).',
|
|
220
270
|
};
|
|
221
271
|
}
|
|
@@ -226,9 +276,12 @@ export function macTools(cfg = {}) {
|
|
|
226
276
|
saved,
|
|
227
277
|
elements,
|
|
228
278
|
element_count: elements.length,
|
|
229
|
-
hint: 'Click by element id: mac_click {"on":"<id>"} or by visible text: mac_click {"text":"Save"}',
|
|
279
|
+
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
280
|
};
|
|
231
|
-
if (
|
|
281
|
+
if (!elements.length) {
|
|
282
|
+
out.hint += ' No elements found — run mac_permissions: the element list comes from the macOS Accessibility API.';
|
|
283
|
+
}
|
|
284
|
+
if (args.include_raw && seen.json) out.raw = seen.json;
|
|
232
285
|
return out;
|
|
233
286
|
},
|
|
234
287
|
},
|
|
@@ -517,6 +570,227 @@ export function macTools(cfg = {}) {
|
|
|
517
570
|
return finish(res, 'quit');
|
|
518
571
|
},
|
|
519
572
|
},
|
|
573
|
+
{
|
|
574
|
+
name: 'mac_element',
|
|
575
|
+
description:
|
|
576
|
+
'Find interface elements by visible name, role or value through the macOS accessibility API. No screenshots, no pixel guessing. Read-only.',
|
|
577
|
+
approval: false,
|
|
578
|
+
schema: S(
|
|
579
|
+
{
|
|
580
|
+
name: { type: 'string', description: 'visible text or title to look for' },
|
|
581
|
+
role: { type: 'string', description: 'e.g. button, textField, menuItem, staticText' },
|
|
582
|
+
value: { type: 'string' },
|
|
583
|
+
app: { type: 'string' },
|
|
584
|
+
window_title: { type: 'string' },
|
|
585
|
+
limit: { type: 'number' },
|
|
586
|
+
},
|
|
587
|
+
[],
|
|
588
|
+
),
|
|
589
|
+
required: [],
|
|
590
|
+
handler: async (args) => {
|
|
591
|
+
const p = checkPeek(peek());
|
|
592
|
+
if (!p.ok) return p;
|
|
593
|
+
const seen = await searchSee(args);
|
|
594
|
+
const matches = seen.elements.filter((e) => matchElement(e, args)).slice(0, Number(args.limit) || 25);
|
|
595
|
+
return {
|
|
596
|
+
ok: true,
|
|
597
|
+
matches,
|
|
598
|
+
count: matches.length,
|
|
599
|
+
scanned: seen.elements.length,
|
|
600
|
+
hint: 'Act on a match by id: mac_click {"on":"<id>"} or mac_set {"on":"<id>","value":"..."}.',
|
|
601
|
+
};
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: 'mac_set',
|
|
606
|
+
description:
|
|
607
|
+
'Set the value of an element directly through the accessibility API — fills a field without focusing the window and without simulated keystrokes.',
|
|
608
|
+
approval: true,
|
|
609
|
+
schema: S(
|
|
610
|
+
{
|
|
611
|
+
on: { type: 'string', description: 'element id from mac_see / mac_element' },
|
|
612
|
+
value: { type: 'string' },
|
|
613
|
+
app: { type: 'string' },
|
|
614
|
+
window_id: { type: 'string' },
|
|
615
|
+
},
|
|
616
|
+
['on', 'value'],
|
|
617
|
+
),
|
|
618
|
+
required: ['on', 'value'],
|
|
619
|
+
handler: async (args) => {
|
|
620
|
+
const p = checkPeek(peek());
|
|
621
|
+
if (!p.ok) return p;
|
|
622
|
+
const flags = targetFlags(args);
|
|
623
|
+
const res = await runVariants(
|
|
624
|
+
p.bin,
|
|
625
|
+
[
|
|
626
|
+
['set-value', String(args.value), '--on', String(args.on), ...flags],
|
|
627
|
+
['set-value', String(args.value), '--on', String(args.on)],
|
|
628
|
+
['set-value', '--on', String(args.on), String(args.value), ...flags],
|
|
629
|
+
['set-value', '--on', String(args.on), '--value', String(args.value), ...flags],
|
|
630
|
+
],
|
|
631
|
+
{ timeout: 30 },
|
|
632
|
+
);
|
|
633
|
+
return finish(res, 'set-value');
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: 'mac_menu',
|
|
638
|
+
description: 'List the menu items of an application through the accessibility API. Read-only.',
|
|
639
|
+
approval: false,
|
|
640
|
+
schema: S({ app: { type: 'string' } }, []),
|
|
641
|
+
required: [],
|
|
642
|
+
handler: async (args) => {
|
|
643
|
+
const p = checkPeek(peek());
|
|
644
|
+
if (!p.ok) return p;
|
|
645
|
+
const flags = args.app ? ['--app', String(args.app)] : [];
|
|
646
|
+
const res = await runVariants(
|
|
647
|
+
p.bin,
|
|
648
|
+
[
|
|
649
|
+
['menu', 'list', ...flags, '--json'],
|
|
650
|
+
['menu', 'list', ...flags],
|
|
651
|
+
],
|
|
652
|
+
{ timeout: 30 },
|
|
653
|
+
);
|
|
654
|
+
const json = parseJson(res.stdout);
|
|
655
|
+
return {
|
|
656
|
+
ok: res.exit_code === 0,
|
|
657
|
+
menus: json ? (json.menus || json.data?.menus || json) : undefined,
|
|
658
|
+
output: json ? undefined : res.stdout.slice(0, 4000),
|
|
659
|
+
error: res.exit_code === 0 ? undefined : (res.stderr || '').trim().slice(0, 600),
|
|
660
|
+
};
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
name: 'mac_menu_click',
|
|
665
|
+
description:
|
|
666
|
+
'Trigger a menu item by name (e.g. "Save", "Copy Link") or a nested path ("File > Export") — no pointer, no coordinates.',
|
|
667
|
+
approval: true,
|
|
668
|
+
schema: S(
|
|
669
|
+
{
|
|
670
|
+
item: { type: 'string', description: 'name of the menu item' },
|
|
671
|
+
path: { type: 'string', description: 'nested path, e.g. "File > Export"' },
|
|
672
|
+
app: { type: 'string', description: 'required: the background menu click needs a target app' },
|
|
673
|
+
},
|
|
674
|
+
[],
|
|
675
|
+
),
|
|
676
|
+
required: [],
|
|
677
|
+
handler: async (args) => {
|
|
678
|
+
const p = checkPeek(peek());
|
|
679
|
+
if (!p.ok) return p;
|
|
680
|
+
if (!args.item && !args.path) {
|
|
681
|
+
return { ok: false, error: 'missing_item', hint: 'Pass item ("Save") or path ("File > Export").' };
|
|
682
|
+
}
|
|
683
|
+
if (!args.app) {
|
|
684
|
+
return {
|
|
685
|
+
ok: false,
|
|
686
|
+
error: 'missing_app',
|
|
687
|
+
hint: 'A background menu click needs an explicit app: mac_menu_click {"app":"Safari","item":"Save"}.',
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const flags = ['--app', String(args.app)];
|
|
691
|
+
const variants = args.path
|
|
692
|
+
? [
|
|
693
|
+
['menu', 'click', '--path', String(args.path), ...flags],
|
|
694
|
+
['menu', 'click', String(args.path), ...flags],
|
|
695
|
+
]
|
|
696
|
+
: [
|
|
697
|
+
['menu', 'click', '--item', String(args.item), ...flags],
|
|
698
|
+
['menu', 'click', String(args.item), ...flags],
|
|
699
|
+
];
|
|
700
|
+
const res = await runVariants(p.bin, variants, { timeout: 30 });
|
|
701
|
+
return finish(res, 'menu-click');
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: 'mac_spaces',
|
|
706
|
+
description: 'List the macOS Spaces (virtual desktops) and which one is active. Read-only.',
|
|
707
|
+
approval: false,
|
|
708
|
+
schema: S({}, []),
|
|
709
|
+
required: [],
|
|
710
|
+
handler: async () => {
|
|
711
|
+
const p = checkPeek(peek());
|
|
712
|
+
if (!p.ok) return p;
|
|
713
|
+
const res = await runVariants(
|
|
714
|
+
p.bin,
|
|
715
|
+
[
|
|
716
|
+
['space', 'list', '--json'],
|
|
717
|
+
['space', 'list'],
|
|
718
|
+
],
|
|
719
|
+
{ timeout: 30 },
|
|
720
|
+
);
|
|
721
|
+
const json = parseJson(res.stdout);
|
|
722
|
+
return {
|
|
723
|
+
ok: res.exit_code === 0,
|
|
724
|
+
spaces: json ? (json.spaces || json.data?.spaces || json) : undefined,
|
|
725
|
+
output: json ? undefined : res.stdout.slice(0, 4000),
|
|
726
|
+
error: res.exit_code === 0 ? undefined : (res.stderr || '').trim().slice(0, 600),
|
|
727
|
+
};
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
name: 'mac_space_switch',
|
|
732
|
+
description:
|
|
733
|
+
'Move to another Space so the agent works on a separate virtual desktop and the user keeps the screen they are on.',
|
|
734
|
+
approval: true,
|
|
735
|
+
schema: S({ to: { type: 'string', description: 'space id or index from mac_spaces' } }, ['to']),
|
|
736
|
+
required: ['to'],
|
|
737
|
+
handler: async (args) => {
|
|
738
|
+
const p = checkPeek(peek());
|
|
739
|
+
if (!p.ok) return p;
|
|
740
|
+
const res = await runVariants(
|
|
741
|
+
p.bin,
|
|
742
|
+
[
|
|
743
|
+
['space', 'switch', '--to', String(args.to), '--foreground'],
|
|
744
|
+
['space', 'switch', String(args.to), '--foreground'],
|
|
745
|
+
],
|
|
746
|
+
{ timeout: 30 },
|
|
747
|
+
);
|
|
748
|
+
return finish(res, 'space-switch');
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: 'mac_wait',
|
|
753
|
+
description:
|
|
754
|
+
'Wait until an element with the given name/role appears (or disappears) and report what was seen. Read-only.',
|
|
755
|
+
approval: false,
|
|
756
|
+
schema: S(
|
|
757
|
+
{
|
|
758
|
+
name: { type: 'string' },
|
|
759
|
+
role: { type: 'string' },
|
|
760
|
+
value: { type: 'string' },
|
|
761
|
+
disappear: { type: 'boolean', description: 'wait until the element is gone' },
|
|
762
|
+
timeout: { type: 'number', description: 'seconds, default 20, max 120' },
|
|
763
|
+
app: { type: 'string' },
|
|
764
|
+
},
|
|
765
|
+
[],
|
|
766
|
+
),
|
|
767
|
+
required: [],
|
|
768
|
+
handler: async (args) => {
|
|
769
|
+
const p = checkPeek(peek());
|
|
770
|
+
if (!p.ok) return p;
|
|
771
|
+
const limit = Math.min(120, Math.max(1, Number(args.timeout) || 20));
|
|
772
|
+
const deadline = Date.now() + limit * 1000;
|
|
773
|
+
let scanned = 0;
|
|
774
|
+
while (Date.now() < deadline) {
|
|
775
|
+
const seen = await searchSee(args);
|
|
776
|
+
scanned = seen.elements.length;
|
|
777
|
+
const hit = seen.elements.find((e) => matchElement(e, args));
|
|
778
|
+
if (args.disappear) {
|
|
779
|
+
if (!hit) return { ok: true, gone: true, found: false, scanned };
|
|
780
|
+
} else if (hit) {
|
|
781
|
+
return { ok: true, found: true, element: hit, scanned };
|
|
782
|
+
}
|
|
783
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
ok: false,
|
|
787
|
+
found: false,
|
|
788
|
+
error: 'not_found',
|
|
789
|
+
reason: `no matching element within ${limit}s`,
|
|
790
|
+
scanned,
|
|
791
|
+
};
|
|
792
|
+
},
|
|
793
|
+
},
|
|
520
794
|
{
|
|
521
795
|
name: 'mac_permissions',
|
|
522
796
|
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.4' },
|
|
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 (it needs the \`app\` 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}
|