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 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-96%20passed-brightgreen.svg?style=flat-square" alt="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": "mac_click", "args": {"on": "B3"}} // клик по id из mac_see
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 12 инструментов chrome_*
299
- mac.mjs 13 инструментов mac_* (Peekaboo)
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.2",
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
@@ -29,7 +29,7 @@ export function createDirectServer(cfg, engine) {
29
29
  ok: true,
30
30
  mode: 'direct',
31
31
  device: cfg.device,
32
- version: '0.6.2',
32
+ version: '0.6.3',
33
33
  uptime_sec: Math.round(process.uptime()),
34
34
  });
35
35
  }
package/src/hub.mjs CHANGED
@@ -90,7 +90,7 @@ export function createHubServer(cfg) {
90
90
  return sendJson(res, 200, {
91
91
  ok: true,
92
92
  mode: 'hub',
93
- version: '0.6.2',
93
+ version: '0.6.3',
94
94
  devices: [...devices.keys()],
95
95
  });
96
96
  }
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 mode = args.mode === 'window' ? 'window' : 'screen';
203
- const variants = [
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 (args.include_raw && json) out.raw = json;
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.2' },
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}