wispy-cli 2.7.8 → 2.7.10
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/bin/wispy.mjs +163 -0
- package/core/browser.mjs +327 -0
- package/core/engine.mjs +239 -0
- package/core/memory.mjs +12 -0
- package/core/secrets.mjs +251 -0
- package/core/subagents.mjs +24 -1
- package/core/task-decomposer.mjs +375 -0
- package/core/task-router.mjs +2 -2
- package/core/tools.mjs +59 -0
- package/core/tts.mjs +194 -0
- package/package.json +1 -1
package/bin/wispy.mjs
CHANGED
|
@@ -49,6 +49,8 @@ Usage:
|
|
|
49
49
|
Manage configuration
|
|
50
50
|
wispy model Show or change AI model
|
|
51
51
|
wispy doctor Check system health
|
|
52
|
+
wispy browser [tabs|attach|navigate|screenshot|doctor]
|
|
53
|
+
Browser control via local-browser-bridge
|
|
52
54
|
wispy trust [level|log] Security level & audit
|
|
53
55
|
wispy ws [start-client|run-debug]
|
|
54
56
|
WebSocket operations
|
|
@@ -161,6 +163,27 @@ if (command === "doctor") {
|
|
|
161
163
|
for (const c of checks) {
|
|
162
164
|
console.log(` ${c.ok ? "✓" : "✗"} ${c.name}`);
|
|
163
165
|
}
|
|
166
|
+
|
|
167
|
+
// Browser bridge check
|
|
168
|
+
try {
|
|
169
|
+
const { BrowserBridge } = await import(join(rootDir, "core/browser.mjs"));
|
|
170
|
+
const bridge = new BrowserBridge();
|
|
171
|
+
const h = await bridge.health();
|
|
172
|
+
if (h?.ok || (!h?.error && h !== null)) {
|
|
173
|
+
const caps = await bridge.capabilities().catch(() => ({}));
|
|
174
|
+
const browsers = Array.isArray(caps?.browsers) ? caps.browsers : [];
|
|
175
|
+
const session = bridge._session;
|
|
176
|
+
const detail = browsers.length
|
|
177
|
+
? `${browsers.join(", ")} available${session ? ", session active" : ""}`
|
|
178
|
+
: "running";
|
|
179
|
+
console.log(` ✓ Browser bridge ${detail}`);
|
|
180
|
+
} else {
|
|
181
|
+
console.log(` ✗ Browser bridge not running (start with: npx local-browser-bridge serve)`);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
console.log(` ✗ Browser bridge not running (start with: npx local-browser-bridge serve)`);
|
|
185
|
+
}
|
|
186
|
+
|
|
164
187
|
console.log("");
|
|
165
188
|
|
|
166
189
|
if (!provider?.key) {
|
|
@@ -175,6 +198,146 @@ if (command === "doctor") {
|
|
|
175
198
|
process.exit(0);
|
|
176
199
|
}
|
|
177
200
|
|
|
201
|
+
// ── Browser ───────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
if (command === "browser") {
|
|
204
|
+
try {
|
|
205
|
+
const { BrowserBridge } = await import(join(rootDir, "core/browser.mjs"));
|
|
206
|
+
const bridge = new BrowserBridge();
|
|
207
|
+
const sub = args[1];
|
|
208
|
+
|
|
209
|
+
if (!sub || sub === "status") {
|
|
210
|
+
// wispy browser — show status
|
|
211
|
+
const h = await bridge.health();
|
|
212
|
+
const status = bridge.status();
|
|
213
|
+
const running = h?.ok || (!h?.error && h !== null);
|
|
214
|
+
console.log(`\n Browser Bridge`);
|
|
215
|
+
console.log(` URL: ${bridge.baseUrl}`);
|
|
216
|
+
console.log(` Status: ${running ? "✓ running" : "✗ not running"}`);
|
|
217
|
+
if (!running) {
|
|
218
|
+
console.log(` Hint: npx local-browser-bridge serve`);
|
|
219
|
+
} else {
|
|
220
|
+
const caps = await bridge.capabilities().catch(() => ({}));
|
|
221
|
+
if (caps?.browsers?.length) {
|
|
222
|
+
console.log(` Browsers: ${caps.browsers.join(", ")}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (status.session) {
|
|
226
|
+
console.log(` Session: ${status.session.id} (${status.session.browser ?? "unknown"})`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(` Session: none`);
|
|
229
|
+
}
|
|
230
|
+
console.log("");
|
|
231
|
+
} else if (sub === "tabs") {
|
|
232
|
+
// wispy browser tabs
|
|
233
|
+
const browser = args[2];
|
|
234
|
+
const result = await bridge.listTabs(browser);
|
|
235
|
+
if (result?.error) {
|
|
236
|
+
console.error(` ✗ ${result.error}`);
|
|
237
|
+
} else {
|
|
238
|
+
const tabs = result?.tabs ?? result;
|
|
239
|
+
console.log(`\n Open tabs:`);
|
|
240
|
+
if (Array.isArray(tabs)) {
|
|
241
|
+
for (const t of tabs) {
|
|
242
|
+
const title = t.title ?? t.name ?? "(no title)";
|
|
243
|
+
const url = t.url ?? "";
|
|
244
|
+
console.log(` • ${title}${url ? ` — ${url}` : ""}`);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
console.log(JSON.stringify(tabs, null, 2));
|
|
248
|
+
}
|
|
249
|
+
console.log("");
|
|
250
|
+
}
|
|
251
|
+
} else if (sub === "attach") {
|
|
252
|
+
// wispy browser attach [browser]
|
|
253
|
+
const browser = args[2];
|
|
254
|
+
console.log(`\n Attaching to ${browser ?? "best available browser"}...`);
|
|
255
|
+
let result;
|
|
256
|
+
if (browser) {
|
|
257
|
+
result = await bridge.attach(browser);
|
|
258
|
+
} else {
|
|
259
|
+
result = await bridge.autoAttach();
|
|
260
|
+
}
|
|
261
|
+
if (result?.error) {
|
|
262
|
+
console.error(` ✗ ${result.error}`);
|
|
263
|
+
} else {
|
|
264
|
+
const id = result?.id ?? result?.sessionId ?? "?";
|
|
265
|
+
const br = result?.browser ?? browser ?? "unknown";
|
|
266
|
+
console.log(` ✓ Attached (${br}, session: ${id})`);
|
|
267
|
+
}
|
|
268
|
+
console.log("");
|
|
269
|
+
} else if (sub === "navigate") {
|
|
270
|
+
// wispy browser navigate <url>
|
|
271
|
+
const url = args[2];
|
|
272
|
+
if (!url) { console.error(" Usage: wispy browser navigate <url>"); process.exit(1); }
|
|
273
|
+
console.log(`\n Navigating to ${url}...`);
|
|
274
|
+
const result = await bridge.navigate(url);
|
|
275
|
+
if (result?.error) {
|
|
276
|
+
console.error(` ✗ ${result.error}`);
|
|
277
|
+
} else {
|
|
278
|
+
console.log(` ✓ Navigated`);
|
|
279
|
+
}
|
|
280
|
+
console.log("");
|
|
281
|
+
} else if (sub === "screenshot") {
|
|
282
|
+
// wispy browser screenshot
|
|
283
|
+
const result = await bridge.screenshot();
|
|
284
|
+
if (result?.error) {
|
|
285
|
+
console.error(` ✗ ${result.error}`);
|
|
286
|
+
} else {
|
|
287
|
+
const data = result?.screenshot ?? result?.data;
|
|
288
|
+
if (data) {
|
|
289
|
+
console.log(`\n Screenshot captured (${data.length} base64 chars)`);
|
|
290
|
+
// Optionally save to file
|
|
291
|
+
const outFile = args[2] ?? `wispy-screenshot-${Date.now()}.png`;
|
|
292
|
+
const buf = Buffer.from(data, "base64");
|
|
293
|
+
const { writeFile: wf } = await import("node:fs/promises");
|
|
294
|
+
await wf(outFile, buf);
|
|
295
|
+
console.log(` Saved to: ${outFile}`);
|
|
296
|
+
} else {
|
|
297
|
+
console.log(` Screenshot result:`, JSON.stringify(result, null, 2));
|
|
298
|
+
}
|
|
299
|
+
console.log("");
|
|
300
|
+
}
|
|
301
|
+
} else if (sub === "doctor") {
|
|
302
|
+
// wispy browser doctor — full diagnostics
|
|
303
|
+
console.log(`\n Browser Bridge Diagnostics`);
|
|
304
|
+
console.log(` URL: ${bridge.baseUrl}`);
|
|
305
|
+
|
|
306
|
+
const h = await bridge.health();
|
|
307
|
+
const running = h?.ok || (!h?.error && h !== null);
|
|
308
|
+
console.log(` Health: ${running ? "✓ ok" : "✗ not running"}`);
|
|
309
|
+
|
|
310
|
+
if (running) {
|
|
311
|
+
const caps = await bridge.capabilities();
|
|
312
|
+
console.log(`\n Capabilities:`);
|
|
313
|
+
console.log(JSON.stringify(caps, null, 2).split("\n").map(l => " " + l).join("\n"));
|
|
314
|
+
|
|
315
|
+
const browsers = Array.isArray(caps?.browsers) ? caps.browsers : [];
|
|
316
|
+
for (const br of browsers) {
|
|
317
|
+
const diag = await bridge.diagnostics(br);
|
|
318
|
+
console.log(`\n Diagnostics (${br}):`);
|
|
319
|
+
console.log(JSON.stringify(diag, null, 2).split("\n").map(l => " " + l).join("\n"));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const sessions = await bridge.listSessions();
|
|
323
|
+
console.log(`\n Sessions:`);
|
|
324
|
+
console.log(JSON.stringify(sessions, null, 2).split("\n").map(l => " " + l).join("\n"));
|
|
325
|
+
} else {
|
|
326
|
+
console.log(` Start with: npx local-browser-bridge serve`);
|
|
327
|
+
}
|
|
328
|
+
console.log("");
|
|
329
|
+
} else {
|
|
330
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
331
|
+
console.log(` Usage: wispy browser [status|tabs|attach|navigate <url>|screenshot|doctor]`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.error("Browser command error:", err.message);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
|
|
178
341
|
// ── Trust ─────────────────────────────────────────────────────────────────────
|
|
179
342
|
|
|
180
343
|
if (command === "trust") {
|
package/core/browser.mjs
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/browser.mjs — Native browser integration via local-browser-bridge
|
|
3
|
+
*
|
|
4
|
+
* Connects to the local-browser-bridge HTTP API for browser control.
|
|
5
|
+
* Supports: health check, capabilities, diagnostics, tabs, attach,
|
|
6
|
+
* navigate, activate, screenshot, sessions.
|
|
7
|
+
*
|
|
8
|
+
* All HTTP calls use native fetch() (Node 18+).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(execFile);
|
|
15
|
+
const NOT_RUNNING_MSG =
|
|
16
|
+
"Bridge not running. Start with: npx local-browser-bridge serve";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* BrowserBridge — HTTP client for local-browser-bridge API.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* const bridge = new BrowserBridge();
|
|
23
|
+
* await bridge.ensureRunning();
|
|
24
|
+
* const session = await bridge.autoAttach();
|
|
25
|
+
* await bridge.navigate("https://example.com");
|
|
26
|
+
*/
|
|
27
|
+
export class BrowserBridge {
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.baseUrl = options.baseUrl ?? "http://127.0.0.1:3000";
|
|
30
|
+
this.defaultBrowser = options.browser ?? "safari";
|
|
31
|
+
this._session = null; // current active session
|
|
32
|
+
this._engine = options.engine ?? null; // optional WispyEngine ref for audit/trust
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Private helpers ──────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
async _get(path, params = {}) {
|
|
38
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
39
|
+
for (const [k, v] of Object.entries(params)) {
|
|
40
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, v);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url.toString(), {
|
|
44
|
+
signal: AbortSignal.timeout(10_000),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const body = await res.text().catch(() => "");
|
|
48
|
+
return { error: `HTTP ${res.status}: ${body.slice(0, 200)}` };
|
|
49
|
+
}
|
|
50
|
+
return res.json();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.name === "AbortError" || err.name === "TimeoutError") {
|
|
53
|
+
return { error: NOT_RUNNING_MSG };
|
|
54
|
+
}
|
|
55
|
+
if (err.code === "ECONNREFUSED" || err.cause?.code === "ECONNREFUSED") {
|
|
56
|
+
return { error: NOT_RUNNING_MSG };
|
|
57
|
+
}
|
|
58
|
+
return { error: err.message ?? NOT_RUNNING_MSG };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async _post(path, body = {}) {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
body: JSON.stringify(body),
|
|
68
|
+
signal: AbortSignal.timeout(30_000),
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const text = await res.text().catch(() => "");
|
|
72
|
+
return { error: `HTTP ${res.status}: ${text.slice(0, 200)}` };
|
|
73
|
+
}
|
|
74
|
+
return res.json();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.name === "AbortError" || err.name === "TimeoutError") {
|
|
77
|
+
return { error: NOT_RUNNING_MSG };
|
|
78
|
+
}
|
|
79
|
+
if (err.code === "ECONNREFUSED" || err.cause?.code === "ECONNREFUSED") {
|
|
80
|
+
return { error: NOT_RUNNING_MSG };
|
|
81
|
+
}
|
|
82
|
+
return { error: err.message ?? NOT_RUNNING_MSG };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_audit(action, details = {}) {
|
|
87
|
+
if (!this._engine?.audit) return;
|
|
88
|
+
this._engine.audit.log({
|
|
89
|
+
type: "BROWSER_ACTION",
|
|
90
|
+
action,
|
|
91
|
+
...details,
|
|
92
|
+
}).catch(() => {});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_trustLevel() {
|
|
96
|
+
if (!this._engine) return "careful";
|
|
97
|
+
return this._engine.config?.trustLevel ?? this._engine.permissions?.level ?? "careful";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_mayLog(action, detail) {
|
|
101
|
+
const level = this._trustLevel();
|
|
102
|
+
if (level !== "yolo") {
|
|
103
|
+
if (process.env.WISPY_DEBUG) {
|
|
104
|
+
process.stderr.write(`[browser] ${action}: ${detail}\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this._audit(action, { detail });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Health & discovery ───────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/** GET /health → { ok: true } */
|
|
113
|
+
async health() {
|
|
114
|
+
return this._get("/health");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** GET /v1/capabilities?browser=X */
|
|
118
|
+
async capabilities(browser) {
|
|
119
|
+
return this._get("/v1/capabilities", { browser });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** GET /v1/diagnostics?browser=X */
|
|
123
|
+
async diagnostics(browser) {
|
|
124
|
+
return this._get("/v1/diagnostics", { browser });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Tab inspection ───────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/** GET /v1/front-tab?browser=X */
|
|
130
|
+
async frontTab(browser) {
|
|
131
|
+
return this._get("/v1/front-tab", { browser: browser ?? this.defaultBrowser });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* GET /v1/tab?browser=X&...
|
|
136
|
+
* query: { browser?, signature?, windowIndex?, tabIndex?, url?, title? }
|
|
137
|
+
*/
|
|
138
|
+
async getTab(query = {}) {
|
|
139
|
+
const params = { browser: query.browser ?? this.defaultBrowser, ...query };
|
|
140
|
+
return this._get("/v1/tab", params);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** GET /v1/tabs?browser=X */
|
|
144
|
+
async listTabs(browser) {
|
|
145
|
+
return this._get("/v1/tabs", { browser: browser ?? this.defaultBrowser });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Session management ───────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* POST /v1/attach { browser, mode?, tabTarget? }
|
|
152
|
+
* Stores the resulting session as this._session.
|
|
153
|
+
*/
|
|
154
|
+
async attach(browser, opts = {}) {
|
|
155
|
+
const body = {
|
|
156
|
+
browser: browser ?? this.defaultBrowser,
|
|
157
|
+
...opts,
|
|
158
|
+
};
|
|
159
|
+
const result = await this._post("/v1/attach", body);
|
|
160
|
+
if (!result.error) {
|
|
161
|
+
this._session = result;
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** GET /v1/sessions */
|
|
167
|
+
async listSessions() {
|
|
168
|
+
return this._get("/v1/sessions");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** GET /v1/sessions/:id */
|
|
172
|
+
async getSession(id) {
|
|
173
|
+
return this._get(`/v1/sessions/${encodeURIComponent(id)}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** POST /v1/sessions/:id/resume */
|
|
177
|
+
async resumeSession(id) {
|
|
178
|
+
const result = await this._post(`/v1/sessions/${encodeURIComponent(id)}/resume`);
|
|
179
|
+
if (!result.error) {
|
|
180
|
+
this._session = result;
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Actions (require active session) ─────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Navigate to a URL.
|
|
189
|
+
* Uses session-scoped endpoint if sessionId/this._session is available.
|
|
190
|
+
*/
|
|
191
|
+
async navigate(url, sessionId) {
|
|
192
|
+
this._mayLog("navigate", url);
|
|
193
|
+
|
|
194
|
+
const sid = sessionId ?? this._session?.id ?? this._session?.sessionId;
|
|
195
|
+
if (sid) {
|
|
196
|
+
return this._post(`/v1/sessions/${encodeURIComponent(sid)}/navigate`, { url });
|
|
197
|
+
}
|
|
198
|
+
return this._post("/v1/navigate", { url });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Bring browser tab to front / focus.
|
|
203
|
+
*/
|
|
204
|
+
async activate(sessionId) {
|
|
205
|
+
this._mayLog("activate", sessionId ?? this._session?.id ?? "default");
|
|
206
|
+
|
|
207
|
+
const sid = sessionId ?? this._session?.id ?? this._session?.sessionId;
|
|
208
|
+
if (sid) {
|
|
209
|
+
return this._post(`/v1/sessions/${encodeURIComponent(sid)}/activate`);
|
|
210
|
+
}
|
|
211
|
+
return this._post("/v1/activate");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Take a screenshot.
|
|
216
|
+
* Returns { screenshot: "<base64>" } or { error }
|
|
217
|
+
*/
|
|
218
|
+
async screenshot(sessionId) {
|
|
219
|
+
const sid = sessionId ?? this._session?.id ?? this._session?.sessionId;
|
|
220
|
+
if (sid) {
|
|
221
|
+
// Some implementations support GET, some POST
|
|
222
|
+
return this._get(`/v1/sessions/${encodeURIComponent(sid)}/screenshot`);
|
|
223
|
+
}
|
|
224
|
+
return this._post("/v1/screenshot");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Lifecycle ────────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check health, attempt to start bridge if not running.
|
|
231
|
+
* Returns { ok: boolean, started: boolean }
|
|
232
|
+
*/
|
|
233
|
+
async ensureRunning() {
|
|
234
|
+
// 1. Try health
|
|
235
|
+
const h = await this.health();
|
|
236
|
+
if (h?.ok || (!h?.error && h !== null)) {
|
|
237
|
+
return { ok: true, started: false };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2. Try to start the bridge
|
|
241
|
+
let started = false;
|
|
242
|
+
try {
|
|
243
|
+
execFile("/bin/sh", ["-c", "npx local-browser-bridge serve --port 3000 &"], {
|
|
244
|
+
detached: true,
|
|
245
|
+
stdio: "ignore",
|
|
246
|
+
});
|
|
247
|
+
started = true;
|
|
248
|
+
} catch {
|
|
249
|
+
// ignore — may not have npx or may already be running
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 3. Wait up to 5 seconds for health to respond
|
|
253
|
+
const deadline = Date.now() + 5000;
|
|
254
|
+
while (Date.now() < deadline) {
|
|
255
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
256
|
+
const h2 = await this.health();
|
|
257
|
+
if (h2?.ok || (!h2?.error && h2 !== null)) {
|
|
258
|
+
return { ok: true, started };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { ok: false, started };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Auto-discover and attach to the best available browser.
|
|
267
|
+
* Order: safari-actionable > safari-readonly > chrome-actionable > chrome-readonly
|
|
268
|
+
*/
|
|
269
|
+
async autoAttach() {
|
|
270
|
+
// 1. Get capabilities
|
|
271
|
+
const caps = await this.capabilities();
|
|
272
|
+
if (caps?.error) return { error: caps.error };
|
|
273
|
+
|
|
274
|
+
const browsers = Array.isArray(caps?.browsers)
|
|
275
|
+
? caps.browsers
|
|
276
|
+
: Object.keys(caps ?? {}).filter((k) => k !== "error");
|
|
277
|
+
|
|
278
|
+
if (browsers.length === 0) {
|
|
279
|
+
return { error: "No browsers available via local-browser-bridge" };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 2. Get diagnostics for each browser
|
|
283
|
+
const diagResults = [];
|
|
284
|
+
for (const browser of browsers) {
|
|
285
|
+
const d = await this.diagnostics(browser);
|
|
286
|
+
diagResults.push({ browser, diag: d });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 3. Pick the best actionable browser
|
|
290
|
+
const priority = [
|
|
291
|
+
(d) => d.browser === "safari" && d.diag?.actionable === true,
|
|
292
|
+
(d) => d.browser === "chrome" && d.diag?.actionable === true,
|
|
293
|
+
(d) => d.diag?.actionable === true,
|
|
294
|
+
(d) => d.browser === "safari",
|
|
295
|
+
(d) => true,
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
let chosen = null;
|
|
299
|
+
for (const pred of priority) {
|
|
300
|
+
chosen = diagResults.find(pred);
|
|
301
|
+
if (chosen) break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!chosen) return { error: "No suitable browser found" };
|
|
305
|
+
|
|
306
|
+
// 4. Attach
|
|
307
|
+
const session = await this.attach(chosen.browser);
|
|
308
|
+
return session;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Summary status for display */
|
|
312
|
+
status() {
|
|
313
|
+
return {
|
|
314
|
+
baseUrl: this.baseUrl,
|
|
315
|
+
defaultBrowser: this.defaultBrowser,
|
|
316
|
+
session: this._session
|
|
317
|
+
? {
|
|
318
|
+
id: this._session.id ?? this._session.sessionId,
|
|
319
|
+
browser: this._session.browser,
|
|
320
|
+
mode: this._session.mode,
|
|
321
|
+
}
|
|
322
|
+
: null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default BrowserBridge;
|