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 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-110%20passed-brightgreen.svg?style=flat-square" alt="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": {"item": "Save", "app": "Safari"}} // пункт меню по названию
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",
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
@@ -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.3',
32
+ version: '0.6.5',
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.3',
93
+ version: '0.6.5',
94
94
  devices: [...devices.keys()],
95
95
  });
96
96
  }
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 > 6) return out;
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
- if (node.elements && Array.isArray(node.elements)) {
110
- for (const e of node.elements) {
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.elementId ?? e.element_id ?? null,
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
- value: typeof e.value === 'string' ? e.value.slice(0, 200) : null,
116
- bounds: e.frame ?? e.bounds ?? e.rect ?? null,
117
- enabled: e.enabled ?? null,
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
- 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;
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 = 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
- };
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 = args.app ? ['--app', String(args.app)] : [];
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: 'Click a menu item by its visible name (e.g. "Save", "Copy Link") — no pointer, no coordinates.',
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({ item: { type: 'string' }, app: { type: 'string' } }, ['item']),
651
- required: ['item'],
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
- 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
- );
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.3' },
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.