@empir3/empir3-bridge 0.3.21
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 +1531 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +464 -0
- package/SECURITY.md +130 -0
- package/assets/accuracy-lab.html +2639 -0
- package/assets/api-clis-real.jpg +0 -0
- package/assets/bridge-console-hero.jpg +0 -0
- package/assets/browser-privacy.svg +151 -0
- package/assets/demo-orchestration.svg +74 -0
- package/assets/desktop-select-region.jpg +0 -0
- package/assets/in-page-chat.gif +0 -0
- package/assets/orchestration-hero.svg +126 -0
- package/assets/social-preview.png +0 -0
- package/assets/zara-accent.png +0 -0
- package/build/bootstrap.js +548 -0
- package/build/build.js +680 -0
- package/build/payload-entry.js +649 -0
- package/build/payload-signing-pub.json +7 -0
- package/docs/AGENT_GUIDE.md +259 -0
- package/docs/RELEASE.md +106 -0
- package/docs/SAFETY.md +112 -0
- package/docs/TESTING.md +181 -0
- package/installer/server.js +231 -0
- package/installer/ui/app.js +278 -0
- package/installer/ui/index.html +24 -0
- package/installer/ui/styles.css +146 -0
- package/package.json +95 -0
- package/scripts/bootstrap-e2e.mjs +650 -0
- package/scripts/certify-bridge.mjs +636 -0
- package/scripts/check-companion-surface.mjs +118 -0
- package/scripts/extract-welcome.mjs +64 -0
- package/scripts/gh-route-handler-check.mjs +57 -0
- package/scripts/gh-wire-test.mjs +107 -0
- package/scripts/publish-downloads.mjs +180 -0
- package/scripts/smoke-all-tools.mjs +509 -0
- package/scripts/smoke-live-bridge.mjs +696 -0
- package/scripts/splice-welcome.mjs +63 -0
- package/scripts/welcome-body.txt +2733 -0
- package/src/anthropic-client.ts +192 -0
- package/src/bootstrap-exe.ts +69 -0
- package/src/bridge.ts +2444 -0
- package/src/chat.ts +345 -0
- package/src/cli-runner.ts +239 -0
- package/src/cli.ts +649 -0
- package/src/config.ts +199 -0
- package/src/desktop-overlay.ps1 +121 -0
- package/src/executable-resolver.ts +330 -0
- package/src/handlers/agy-imagegen.ts +179 -0
- package/src/handlers/github-cli.ts +399 -0
- package/src/handlers/higgsfield-cli.ts +783 -0
- package/src/launch.js +337 -0
- package/src/mcp-server.ts +1265 -0
- package/src/pair-claim.ts +218 -0
- package/src/payload-daemon.ts +168 -0
- package/src/server.ts +21036 -0
- package/src/tool-defaults.ts +230 -0
- package/src/update-check.js +136 -0
- package/tray/build.py +76 -0
- package/tray/requirements.txt +2 -0
- package/tray/tray.py +1843 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empir3 Bridge CLI Client
|
|
3
|
+
*
|
|
4
|
+
* Drives the browser bridge from a terminal or subagent script.
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx tsx src/cli.ts status
|
|
7
|
+
* npx tsx src/cli.ts chat "Hello from Claude!"
|
|
8
|
+
* npx tsx src/cli.ts navigate "https://example.com"
|
|
9
|
+
* npx tsx src/cli.ts click "button.submit" (CSS selector)
|
|
10
|
+
* npx tsx src/cli.ts click-ref "e5" (element ref from snapshot)
|
|
11
|
+
* npx tsx src/cli.ts click-xy 500 320 (viewport coordinates)
|
|
12
|
+
* npx tsx src/cli.ts type "input#email" "user@test.com"
|
|
13
|
+
* npx tsx src/cli.ts type-ref "e3" "user@test.com" (type into element ref)
|
|
14
|
+
* npx tsx src/cli.ts screenshot [path] (defaults to feedback/claude-<ts>.jpg)
|
|
15
|
+
* npx tsx src/cli.ts desktop-monitors (physical monitor bounds)
|
|
16
|
+
* npx tsx src/cli.ts desktop-screenshot [monitor] (all, primary, DISPLAY1...)
|
|
17
|
+
* npx tsx src/cli.ts desktop-click <x> <y> [monitor] [--double]
|
|
18
|
+
* npx tsx src/cli.ts desktop-hover <x> <y> [monitor]
|
|
19
|
+
* npx tsx src/cli.ts desktop-snapshot [--all] (UIA enumerate refs)
|
|
20
|
+
* npx tsx src/cli.ts desktop-click-ref <ref> (click by ref)
|
|
21
|
+
* npx tsx src/cli.ts desktop-hover-ref <ref> (hover by ref)
|
|
22
|
+
* npx tsx src/cli.ts desktop-overlay [show|hide] (toggle labeled-box overlay)
|
|
23
|
+
* npx tsx src/cli.ts desktop-drag <x1> <y1> <x2> <y2> [monitor]
|
|
24
|
+
* npx tsx src/cli.ts reliability-status (health + recent action receipts)
|
|
25
|
+
* npx tsx src/cli.ts reliability-smoke (run bridge reliability checks)
|
|
26
|
+
* npx tsx src/cli.ts smoke-plan (print standard bridge smoke plan)
|
|
27
|
+
* npx tsx src/cli.ts action-log (recent command receipts)
|
|
28
|
+
* npx tsx src/cli.ts safety-status (read/write control state)
|
|
29
|
+
* npx tsx src/cli.ts revoke-control (disable write-control tools)
|
|
30
|
+
* npx tsx src/cli.ts desktop-test (open safe desktop test page)
|
|
31
|
+
* npx tsx src/cli.ts evaluate "1+1"
|
|
32
|
+
* npx tsx src/cli.ts refresh
|
|
33
|
+
* npx tsx src/cli.ts highlight "div.card"
|
|
34
|
+
* npx tsx src/cli.ts snapshot (get interactive element refs)
|
|
35
|
+
* npx tsx src/cli.ts snapshot all (get full page snapshot)
|
|
36
|
+
* npx tsx src/cli.ts text (extract page text)
|
|
37
|
+
* npx tsx src/cli.ts read-chat [last N]
|
|
38
|
+
* npx tsx src/cli.ts read-feedback [last N]
|
|
39
|
+
* npx tsx src/cli.ts listen (streams messages in real-time)
|
|
40
|
+
* npx tsx src/cli.ts record start
|
|
41
|
+
* npx tsx src/cli.ts record stop "Login Flow"
|
|
42
|
+
* npx tsx src/cli.ts record status
|
|
43
|
+
* npx tsx src/cli.ts play "login_flow" [speed] [EMAIL=test@test.com PASSWORD=secret]
|
|
44
|
+
* npx tsx src/cli.ts recordings (list saved recordings)
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { readFileSync } from 'fs';
|
|
48
|
+
import { homedir } from 'os';
|
|
49
|
+
import { join } from 'path';
|
|
50
|
+
|
|
51
|
+
const BRIDGE_URL = process.env.BRIDGE_URL || 'http://localhost:3006';
|
|
52
|
+
const BRIDGE_WS = (process.env.BRIDGE_URL ? process.env.BRIDGE_URL.replace(/^http/, 'ws') : 'ws://localhost:3006') + '?role=cli';
|
|
53
|
+
|
|
54
|
+
function readBridgeNonce(): string {
|
|
55
|
+
const explicit = process.env.EMPIR3_BRIDGE_NONCE || process.env.BRIDGE_NONCE;
|
|
56
|
+
if (explicit?.trim()) return explicit.trim();
|
|
57
|
+
try {
|
|
58
|
+
return readFileSync(join(homedir(), '.empir3-bridge', 'nonce'), 'utf-8').trim();
|
|
59
|
+
} catch {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function bridgeHeaders(): Record<string, string> {
|
|
65
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
66
|
+
const nonce = readBridgeNonce();
|
|
67
|
+
if (nonce) headers['X-Empir3-Nonce'] = nonce;
|
|
68
|
+
return headers;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function api(path: string, method = 'GET', body?: any) {
|
|
72
|
+
const opts: any = { method, headers: bridgeHeaders() };
|
|
73
|
+
if (body) opts.body = JSON.stringify(body);
|
|
74
|
+
const res = await fetch(`${BRIDGE_URL}${path}`, opts);
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new Error(`Bridge ${path}: ${res.status} ${await res.text()}`);
|
|
77
|
+
}
|
|
78
|
+
return res.json();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function command(cmd: any) {
|
|
82
|
+
const result = await api('/api/command', 'POST', { action: cmd.type, ...cmd });
|
|
83
|
+
if (!result.ok) throw new Error(result.error || 'Command failed');
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
const [, , action, ...args] = process.argv;
|
|
89
|
+
|
|
90
|
+
if (!action) {
|
|
91
|
+
console.log('Usage: npx tsx src/cli.ts <command> [args...]');
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log('Browser Control:');
|
|
94
|
+
console.log(' status Show bridge daemon status');
|
|
95
|
+
console.log(' navigate <url> Navigate to URL');
|
|
96
|
+
console.log(' click <selector> Click by CSS selector');
|
|
97
|
+
console.log(' click-ref <ref> Click by element ref (e.g., e5)');
|
|
98
|
+
console.log(' click-xy <x> <y> Click viewport coordinates without DOM');
|
|
99
|
+
console.log(' type <selector> <text> Type into CSS selector');
|
|
100
|
+
console.log(' type-ref <ref> <text> Type into element ref');
|
|
101
|
+
console.log(' screenshot [path] Take screenshot (saves to path if given)');
|
|
102
|
+
console.log(' desktop-monitors Show DPI-aware physical monitor bounds');
|
|
103
|
+
console.log(' desktop-screenshot [monitor] Capture desktop monitor(s): all, primary, DISPLAY1');
|
|
104
|
+
console.log(' desktop-click <x> <y> [monitor] Click desktop coordinates; monitor makes x/y monitor-relative');
|
|
105
|
+
console.log(' desktop-hover <x> <y> [monitor] Move cursor to desktop coordinates');
|
|
106
|
+
console.log(' desktop-snapshot [--all] Enumerate UI elements via UI Automation; returns refs');
|
|
107
|
+
console.log(' desktop-snapshot-som SoM: numbered boxes drawn on a focus screenshot');
|
|
108
|
+
console.log(' desktop-click-ref <ref> Click by snapshot ref (e.g. d3)');
|
|
109
|
+
console.log(' desktop-hover-ref <ref> Hover by snapshot ref');
|
|
110
|
+
console.log(' desktop-overlay [show|hide] Toggle click-through labeled-box overlay');
|
|
111
|
+
console.log(' desktop-select-region User drags a rectangle → sets agent focus');
|
|
112
|
+
console.log(' desktop-release-focus Clear the agent focus region');
|
|
113
|
+
console.log(' desktop-focus-status Report current focus state');
|
|
114
|
+
console.log(' desktop-drag <x1> <y1> <x2> <y2> [monitor] Drag between desktop coordinates');
|
|
115
|
+
console.log(' reliability-status Show bridge health, tools, and recent action receipts');
|
|
116
|
+
console.log(' reliability-smoke Run monitor, screenshot, and trusted click checks');
|
|
117
|
+
console.log(' smoke-plan Print the standard bridge smoke test plan');
|
|
118
|
+
console.log(' action-log Show recent command receipts');
|
|
119
|
+
console.log(' safety-status Show read/write control state');
|
|
120
|
+
console.log(' revoke-control Disable browser interact, desktop, eval, and recording tools');
|
|
121
|
+
console.log(' desktop-test Open the safe desktop click/drag test page');
|
|
122
|
+
console.log(' evaluate <js> Run arbitrary JavaScript in the page');
|
|
123
|
+
console.log(' refresh Reload page');
|
|
124
|
+
console.log(' highlight <selector> Highlight element');
|
|
125
|
+
console.log(' snapshot [all|interactive] Get element refs from accessibility tree');
|
|
126
|
+
console.log(' text Extract page text');
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log('Chat:');
|
|
129
|
+
console.log(' chat "message" Send message to browser overlay');
|
|
130
|
+
console.log(' read-chat [N] Read last N chat messages');
|
|
131
|
+
console.log(' read-feedback [N] Read last N feedback items');
|
|
132
|
+
console.log(' listen Stream messages in real-time');
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log('Recording/Playback:');
|
|
135
|
+
console.log(' record start Start recording user actions');
|
|
136
|
+
console.log(' record stop [name] Stop and save recording');
|
|
137
|
+
console.log(' record status Check recording status');
|
|
138
|
+
console.log(' play <name> [speed] [VAR=val] Play a recording');
|
|
139
|
+
console.log(' recordings List saved recordings');
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log('Pairing:');
|
|
142
|
+
console.log(' pair <code> Redeem a pre-authorized Empir3 pairing code (writes bridge-auth.json)');
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
switch (action) {
|
|
148
|
+
case 'pair': {
|
|
149
|
+
// Redeem a pre-authorized Empir3 pairing code (the `--pair <code>` install
|
|
150
|
+
// path), writing bridge-auth.json. Talks to Empir3 directly, not the local
|
|
151
|
+
// daemon. Override the target with EMPIR3_SERVER for local-dev testing.
|
|
152
|
+
const code = args[0];
|
|
153
|
+
if (!code) {
|
|
154
|
+
console.error('Usage: pair <code> (set EMPIR3_SERVER to target a non-prod Empir3)');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const { claimPairingCode } = await import('./pair-claim.js');
|
|
158
|
+
const result = await claimPairingCode(code, { log: (m) => console.log(`[pair] ${m}`) });
|
|
159
|
+
console.log(JSON.stringify(result, null, 2));
|
|
160
|
+
process.exit(result.ok ? 0 : 1);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case 'status': {
|
|
164
|
+
const result = await api('/api/status');
|
|
165
|
+
console.log(JSON.stringify(result, null, 2));
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'chat': {
|
|
170
|
+
const message = args.join(' ');
|
|
171
|
+
if (!message) { console.error('Usage: chat "message"'); process.exit(1); }
|
|
172
|
+
await command({ type: 'chat', message });
|
|
173
|
+
console.log('Sent:', message);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'navigate': {
|
|
178
|
+
const url = args[0];
|
|
179
|
+
if (!url) { console.error('Usage: navigate <url>'); process.exit(1); }
|
|
180
|
+
const result = await command({ type: 'navigate', url });
|
|
181
|
+
console.log('Navigated to:', result.result?.url);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case 'click': {
|
|
186
|
+
const selector = args[0];
|
|
187
|
+
if (!selector) { console.error('Usage: click <selector>'); process.exit(1); }
|
|
188
|
+
await command({ type: 'click', selector });
|
|
189
|
+
console.log('Clicked:', selector);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'click-ref': {
|
|
194
|
+
const ref = args[0];
|
|
195
|
+
if (!ref) { console.error('Usage: click-ref <ref> (e.g., e5)'); process.exit(1); }
|
|
196
|
+
await command({ type: 'click_ref', ref });
|
|
197
|
+
console.log('Clicked ref:', ref);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case 'click-xy': {
|
|
202
|
+
const x = Number(args[0]);
|
|
203
|
+
const y = Number(args[1]);
|
|
204
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) { console.error('Usage: click-xy <x> <y>'); process.exit(1); }
|
|
205
|
+
await command({ type: 'click_xy', x, y });
|
|
206
|
+
console.log(`Clicked coordinates: ${x},${y}`);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'type': {
|
|
211
|
+
const [selector, ...textParts] = args;
|
|
212
|
+
const text = textParts.join(' ');
|
|
213
|
+
if (!selector || !text) { console.error('Usage: type <selector> <text>'); process.exit(1); }
|
|
214
|
+
await command({ type: 'type', selector, text });
|
|
215
|
+
console.log('Typed into', selector);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case 'type-ref': {
|
|
220
|
+
const [ref, ...textParts] = args;
|
|
221
|
+
const text = textParts.join(' ');
|
|
222
|
+
if (!ref || !text) { console.error('Usage: type-ref <ref> <text>'); process.exit(1); }
|
|
223
|
+
await command({ type: 'type_ref', ref, text });
|
|
224
|
+
console.log('Typed into ref:', ref);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'screenshot': {
|
|
229
|
+
const destPath = args[0];
|
|
230
|
+
const result = await command({ type: 'screenshot' });
|
|
231
|
+
const sourcePath = result.result?.path;
|
|
232
|
+
if (destPath && sourcePath) {
|
|
233
|
+
// Honor explicit path arg — copy the freshly-written JPEG there.
|
|
234
|
+
const { copyFileSync, mkdirSync } = await import('fs');
|
|
235
|
+
const { dirname } = await import('path');
|
|
236
|
+
try { mkdirSync(dirname(destPath), { recursive: true }); } catch {}
|
|
237
|
+
copyFileSync(sourcePath, destPath);
|
|
238
|
+
console.log('Screenshot saved:', destPath);
|
|
239
|
+
} else {
|
|
240
|
+
console.log('Screenshot saved:', sourcePath);
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
case 'desktop-monitors': {
|
|
246
|
+
const result = await command({ type: 'desktop_monitors' });
|
|
247
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case 'desktop-screenshot': {
|
|
252
|
+
const regionArg = args.find(a => a.startsWith('--region='));
|
|
253
|
+
let region: any;
|
|
254
|
+
if (regionArg) {
|
|
255
|
+
const parts = regionArg.split('=')[1].split(',').map(Number);
|
|
256
|
+
if (parts.length !== 4 || parts.some(n => !Number.isFinite(n))) {
|
|
257
|
+
console.error('Usage: --region=x,y,width,height (all integers, virtual-screen coords)');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
region = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
261
|
+
}
|
|
262
|
+
const gridArg = args.find(a => a === '--grid' || a.startsWith('--grid='));
|
|
263
|
+
let grid: any;
|
|
264
|
+
if (gridArg === '--grid') grid = true;
|
|
265
|
+
else if (gridArg) {
|
|
266
|
+
const stepMatch = gridArg.split('=')[1];
|
|
267
|
+
const step = Number(stepMatch);
|
|
268
|
+
grid = Number.isFinite(step) ? { step } : true;
|
|
269
|
+
}
|
|
270
|
+
const markerArg = args.find(a => a.startsWith('--marker='));
|
|
271
|
+
let marker: any;
|
|
272
|
+
if (markerArg) {
|
|
273
|
+
const mp = markerArg.split('=')[1].split(',').map(Number);
|
|
274
|
+
if (mp.length !== 2 || mp.some(n => !Number.isFinite(n))) {
|
|
275
|
+
console.error('Usage: --marker=x,y (virtual-screen coords)');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
marker = { x: mp[0], y: mp[1] };
|
|
279
|
+
}
|
|
280
|
+
const monitor = args.find(a => !a.startsWith('--')) || 'all';
|
|
281
|
+
const result = await command({ type: 'desktop_screenshot', monitor, region, grid, marker });
|
|
282
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
case 'desktop-click': {
|
|
287
|
+
const x = Number(args[0]);
|
|
288
|
+
const y = Number(args[1]);
|
|
289
|
+
const monitor = args.find(a => a && !a.startsWith('--') && a !== args[0] && a !== args[1]);
|
|
290
|
+
const double = args.includes('--double');
|
|
291
|
+
const buttonArg = args.find(a => a.startsWith('--button='));
|
|
292
|
+
const button = buttonArg ? buttonArg.split('=')[1] : 'left';
|
|
293
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
294
|
+
console.error('Usage: desktop-click <x> <y> [monitor] [--double] [--button=left|right|middle]');
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
const result = await command({
|
|
298
|
+
type: 'desktop_click',
|
|
299
|
+
x,
|
|
300
|
+
y,
|
|
301
|
+
monitor,
|
|
302
|
+
space: monitor ? 'monitor' : 'desktop',
|
|
303
|
+
double,
|
|
304
|
+
button,
|
|
305
|
+
});
|
|
306
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'desktop-hover': {
|
|
311
|
+
const x = Number(args[0]);
|
|
312
|
+
const y = Number(args[1]);
|
|
313
|
+
const monitor = args.find(a => a && !a.startsWith('--') && a !== args[0] && a !== args[1]);
|
|
314
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
315
|
+
console.error('Usage: desktop-hover <x> <y> [monitor]');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
const result = await command({
|
|
319
|
+
type: 'desktop_hover',
|
|
320
|
+
x,
|
|
321
|
+
y,
|
|
322
|
+
monitor,
|
|
323
|
+
space: monitor ? 'monitor' : 'desktop',
|
|
324
|
+
});
|
|
325
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
case 'desktop-drag': {
|
|
330
|
+
const x = Number(args[0]);
|
|
331
|
+
const y = Number(args[1]);
|
|
332
|
+
const toX = Number(args[2]);
|
|
333
|
+
const toY = Number(args[3]);
|
|
334
|
+
const monitor = args.find((a, i) => i > 3 && a && !a.startsWith('--'));
|
|
335
|
+
const buttonArg = args.find(a => a.startsWith('--button='));
|
|
336
|
+
const durationArg = args.find(a => a.startsWith('--duration='));
|
|
337
|
+
const stepsArg = args.find(a => a.startsWith('--steps='));
|
|
338
|
+
const button = buttonArg ? buttonArg.split('=')[1] : 'left';
|
|
339
|
+
const durationMs = durationArg ? Number(durationArg.split('=')[1]) : undefined;
|
|
340
|
+
const steps = stepsArg ? Number(stepsArg.split('=')[1]) : undefined;
|
|
341
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(toX) || !Number.isFinite(toY)) {
|
|
342
|
+
console.error('Usage: desktop-drag <x1> <y1> <x2> <y2> [monitor] [--button=left|right|middle] [--duration=500] [--steps=24]');
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
const result = await command({
|
|
346
|
+
type: 'desktop_drag',
|
|
347
|
+
x,
|
|
348
|
+
y,
|
|
349
|
+
toX,
|
|
350
|
+
toY,
|
|
351
|
+
monitor,
|
|
352
|
+
space: monitor ? 'monitor' : 'desktop',
|
|
353
|
+
button,
|
|
354
|
+
durationMs,
|
|
355
|
+
steps,
|
|
356
|
+
});
|
|
357
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
case 'desktop-snapshot': {
|
|
362
|
+
const scopeArg = args.find(a => a === '--all' || a === '--foreground');
|
|
363
|
+
const scope = scopeArg === '--all' ? 'all-windows' : 'foreground';
|
|
364
|
+
const maxArg = args.find(a => a.startsWith('--max='));
|
|
365
|
+
const maxElements = maxArg ? Number(maxArg.split('=')[1]) : 200;
|
|
366
|
+
const result = await command({ type: 'desktop_snapshot', scope, maxElements });
|
|
367
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case 'desktop-snapshot-som': {
|
|
372
|
+
const maxArg = args.find(a => a.startsWith('--max='));
|
|
373
|
+
const maxElements = maxArg ? Number(maxArg.split('=')[1]) : 200;
|
|
374
|
+
const result = await command({ type: 'desktop_snapshot_som', maxElements });
|
|
375
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
case 'desktop-click-ref': {
|
|
380
|
+
const ref = args[0];
|
|
381
|
+
const buttonArg = args.find(a => a.startsWith('--button='));
|
|
382
|
+
const button = buttonArg ? buttonArg.split('=')[1] : 'left';
|
|
383
|
+
const double = args.includes('--double');
|
|
384
|
+
if (!ref) { console.error('Usage: desktop-click-ref <ref> [--button=left|right|middle] [--double]'); process.exit(1); }
|
|
385
|
+
const result = await command({ type: 'desktop_click_ref', ref, button, double });
|
|
386
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
case 'desktop-hover-ref': {
|
|
391
|
+
const ref = args[0];
|
|
392
|
+
if (!ref) { console.error('Usage: desktop-hover-ref <ref>'); process.exit(1); }
|
|
393
|
+
const result = await command({ type: 'desktop_hover_ref', ref });
|
|
394
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
case 'desktop-overlay': {
|
|
399
|
+
const action = args[0] || 'toggle';
|
|
400
|
+
if (!['show','hide','toggle','status'].includes(action)) {
|
|
401
|
+
console.error('Usage: desktop-overlay [show|hide|toggle|status]');
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
const result = await command({ type: 'desktop_overlay', action });
|
|
405
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case 'desktop-select-region': {
|
|
410
|
+
const tArg = args.find(a => a.startsWith('--timeout='));
|
|
411
|
+
const timeoutMs = tArg ? Number(tArg.split('=')[1]) : undefined;
|
|
412
|
+
const result = await command({ type: 'desktop_select_region', timeoutMs });
|
|
413
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case 'desktop-release-focus': {
|
|
418
|
+
const result = await command({ type: 'desktop_release_focus' });
|
|
419
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case 'desktop-focus-status': {
|
|
424
|
+
const result = await command({ type: 'desktop_focus_status' });
|
|
425
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
case 'reliability-status': {
|
|
430
|
+
const result = await command({ type: 'reliability_status' });
|
|
431
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case 'reliability-smoke': {
|
|
436
|
+
const result = await command({ type: 'reliability_smoke' });
|
|
437
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
case 'smoke-plan': {
|
|
442
|
+
const result = await api('/api/bridge-smoke-test-plan');
|
|
443
|
+
console.log(JSON.stringify(result, null, 2));
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
case 'action-log': {
|
|
448
|
+
const result = await command({ type: 'action_log' });
|
|
449
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
case 'safety-status': {
|
|
454
|
+
const result = await command({ type: 'safety_status' });
|
|
455
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
case 'revoke-control': {
|
|
460
|
+
const result = await command({ type: 'safety_lockdown' });
|
|
461
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
case 'desktop-test': {
|
|
466
|
+
const result = await command({ type: 'navigate', url: `${BRIDGE_URL}/desktop-test` });
|
|
467
|
+
console.log('Opened desktop test page:', result.result?.url);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
case 'evaluate': {
|
|
472
|
+
const script = args.join(' ');
|
|
473
|
+
if (!script) { console.error('Usage: evaluate <js>'); process.exit(1); }
|
|
474
|
+
const result = await command({ type: 'evaluate', script });
|
|
475
|
+
console.log(JSON.stringify(result.result?.result ?? result.result, null, 2));
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
case 'refresh': {
|
|
480
|
+
await command({ type: 'refresh' });
|
|
481
|
+
console.log('Page refreshed');
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
case 'highlight': {
|
|
486
|
+
const selector = args[0];
|
|
487
|
+
if (!selector) { console.error('Usage: highlight <selector>'); process.exit(1); }
|
|
488
|
+
await command({ type: 'highlight', selector });
|
|
489
|
+
console.log('Highlighted:', selector);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
case 'snapshot': {
|
|
494
|
+
const filter = args[0] || 'interactive';
|
|
495
|
+
const format = args[1] || 'compact';
|
|
496
|
+
const result = await command({ type: 'snapshot', filter, format });
|
|
497
|
+
const snapshot = result.result?.snapshot;
|
|
498
|
+
if (typeof snapshot === 'string') {
|
|
499
|
+
// Compact format — print as-is
|
|
500
|
+
console.log(snapshot);
|
|
501
|
+
} else if (Array.isArray(snapshot)) {
|
|
502
|
+
// JSON format — pretty print element refs
|
|
503
|
+
for (const el of snapshot) {
|
|
504
|
+
const bounds = el.bounds ? ` (${el.bounds.x},${el.bounds.y} ${el.bounds.width}x${el.bounds.height})` : '';
|
|
505
|
+
console.log(` ${el.ref} [${el.role}] "${el.name || ''}"${bounds}`);
|
|
506
|
+
}
|
|
507
|
+
console.log(`\n${snapshot.length} elements`);
|
|
508
|
+
} else {
|
|
509
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
case 'text': {
|
|
515
|
+
const result = await command({ type: 'text' });
|
|
516
|
+
console.log(result.result?.text || '(no text)');
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
case 'read-chat': {
|
|
521
|
+
const limit = parseInt(args[0]) || 20;
|
|
522
|
+
const messages = await api('/api/chat');
|
|
523
|
+
const recent = messages.slice(-limit);
|
|
524
|
+
for (const msg of recent) {
|
|
525
|
+
const time = new Date(msg.timestamp).toLocaleTimeString();
|
|
526
|
+
const prefix = msg.from === 'user' ? ' You' : ' Claude';
|
|
527
|
+
console.log(`[${time}] ${prefix}: ${msg.text}`);
|
|
528
|
+
if (msg.screenshot) console.log(` [screenshot: ${msg.screenshot}]`);
|
|
529
|
+
if (msg.selector) console.log(` [element: ${msg.selector}]`);
|
|
530
|
+
}
|
|
531
|
+
if (recent.length === 0) console.log('No messages yet.');
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
case 'read-feedback': {
|
|
536
|
+
const { readFileSync, existsSync } = await import('fs');
|
|
537
|
+
const { resolve } = await import('path');
|
|
538
|
+
const fbFile = resolve(__dirname, '..', 'feedback', 'feedback.jsonl');
|
|
539
|
+
if (!existsSync(fbFile)) { console.log('No feedback yet.'); break; }
|
|
540
|
+
const lines = readFileSync(fbFile, 'utf-8').trim().split('\n').filter(Boolean);
|
|
541
|
+
const limit = parseInt(args[0]) || 10;
|
|
542
|
+
const recent = lines.slice(-limit).map(l => JSON.parse(l));
|
|
543
|
+
for (const entry of recent) {
|
|
544
|
+
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
545
|
+
console.log(`[${time}] "${entry.comment}" on ${entry.selector}`);
|
|
546
|
+
if (entry.screenshotPath) console.log(` [screenshot: ${entry.screenshotName || entry.screenshotPath}]`);
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
case 'listen': {
|
|
552
|
+
console.log('Listening for messages... (Ctrl+C to stop)\n');
|
|
553
|
+
const { WebSocket } = await import('ws');
|
|
554
|
+
const ws = new WebSocket(BRIDGE_WS);
|
|
555
|
+
ws.on('open', () => console.log('Connected to bridge.\n'));
|
|
556
|
+
ws.on('message', (data) => {
|
|
557
|
+
const msg = JSON.parse(data.toString());
|
|
558
|
+
const time = new Date().toLocaleTimeString();
|
|
559
|
+
if (msg.type === 'user_message') {
|
|
560
|
+
console.log(`[${time}] User: ${msg.message.text}`);
|
|
561
|
+
if (msg.message.screenshot) console.log(` [screenshot: ${msg.message.screenshot}]`);
|
|
562
|
+
if (msg.message.selector) console.log(` [element: ${msg.message.selector}]`);
|
|
563
|
+
if (msg.message.url) console.log(` [url: ${msg.message.url}]`);
|
|
564
|
+
} else if (msg.type === 'feedback') {
|
|
565
|
+
console.log(`[${time}] Feedback: "${msg.entry.comment}" on ${msg.entry.selector}`);
|
|
566
|
+
} else {
|
|
567
|
+
console.log(`[${time}] Event: ${msg.type}`);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
ws.on('close', () => { console.log('Disconnected.'); process.exit(0); });
|
|
571
|
+
await new Promise(() => {});
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case 'record': {
|
|
576
|
+
const subAction = args[0] || 'status';
|
|
577
|
+
if (subAction === 'start') {
|
|
578
|
+
const result = await command({ type: 'record_start' });
|
|
579
|
+
console.log('Recording started at', result.result?.startUrl, '(engine: empir3)');
|
|
580
|
+
} else if (subAction === 'stop') {
|
|
581
|
+
const name = args.slice(1).join(' ') || undefined;
|
|
582
|
+
const result = await command({ type: 'record_stop', text: name });
|
|
583
|
+
const r = result.result;
|
|
584
|
+
console.log(`Recording saved: ${r?.saved} (${r?.actionCount} actions, ${(r?.duration / 1000).toFixed(1)}s)`);
|
|
585
|
+
if (r?.refCount !== undefined) console.log(`Element refs: ${r.refCount}/${r.actionCount} actions have refs`);
|
|
586
|
+
if (r?.variables?.length) console.log('Variables:', r.variables.join(', '));
|
|
587
|
+
} else if (subAction === 'status') {
|
|
588
|
+
const result = await api('/api/recording-status');
|
|
589
|
+
console.log(JSON.stringify(result, null, 2));
|
|
590
|
+
} else {
|
|
591
|
+
console.error('Usage: record start | record stop [name] | record status');
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
case 'play': {
|
|
597
|
+
const recording = args[0];
|
|
598
|
+
if (!recording) { console.error('Usage: play <recording-name> [speed] [VAR=value ...]'); process.exit(1); }
|
|
599
|
+
const speed = parseFloat(args[1]) || 1;
|
|
600
|
+
const variables: Record<string, string> = {};
|
|
601
|
+
for (const arg of args.slice(2)) {
|
|
602
|
+
const eq = arg.indexOf('=');
|
|
603
|
+
if (eq > 0) variables[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
604
|
+
}
|
|
605
|
+
console.log(`Playing "${recording}" at ${speed}x speed...`);
|
|
606
|
+
if (Object.keys(variables).length) console.log('Variables:', variables);
|
|
607
|
+
const result = await command({ type: 'play', recording, speed, variables });
|
|
608
|
+
if (result.ok) {
|
|
609
|
+
const r = result.result;
|
|
610
|
+
console.log(`\nDone: ${r.passed}/${r.total} steps passed, ${r.failed} failed.`);
|
|
611
|
+
for (const step of r.results) {
|
|
612
|
+
const icon = step.ok ? '\u2713' : '\u2717';
|
|
613
|
+
const method = step.method ? ` [${step.method}]` : '';
|
|
614
|
+
console.log(` ${icon} Step ${step.step}: ${step.action}${method}${step.error ? ' \u2014 ' + step.error : ''}`);
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
console.error('Playback failed:', result.error);
|
|
618
|
+
}
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
case 'recordings': {
|
|
623
|
+
const recordings = await api('/api/recordings');
|
|
624
|
+
if (recordings.length === 0) { console.log('No recordings yet.'); break; }
|
|
625
|
+
for (const r of recordings) {
|
|
626
|
+
const engine = r.engine === 'empir3' ? ' [empir3]' : ' [legacy]';
|
|
627
|
+
console.log(` ${r.name} (${r.actionCount} actions, ${(r.duration / 1000).toFixed(1)}s)${engine} \u2014 ${r.startUrl}`);
|
|
628
|
+
if (r.description) console.log(` ${r.description}`);
|
|
629
|
+
if (r.variables?.length) console.log(` Variables: ${r.variables.join(', ')}`);
|
|
630
|
+
}
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
default:
|
|
635
|
+
console.error('Unknown command:', action);
|
|
636
|
+
console.error('Run without args for help.');
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
} catch (e: any) {
|
|
640
|
+
if (e.cause?.code === 'ECONNREFUSED') {
|
|
641
|
+
console.error('Bridge server not running. Start it with: npm start (in the bridge repo)');
|
|
642
|
+
} else {
|
|
643
|
+
console.error('Error:', e.message);
|
|
644
|
+
}
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
main();
|