@solarisdk/mcp 0.3.1 → 0.3.2
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/README.md +34 -0
- package/dist/browser.d.ts +9 -0
- package/dist/browser.js +250 -0
- package/dist/solari-mcp-http.bundle.cjs +196 -1
- package/dist/solari-mcp.bundle.cjs +196 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,9 @@ Chat, Code and Cowork alike.
|
|
|
103
103
|
| `solari_browser_click` / `solari_browser_type` / `solari_browser_key` | Interact (selector or x/y) |
|
|
104
104
|
| `solari_browser_evaluate` | Run a JS expression on the page |
|
|
105
105
|
| `solari_browser_replay_url` | rrweb replay URL (needs `recording: true`) |
|
|
106
|
+
| `solari_browser_login` | Ask a HUMAN to sign in — returns a short-lived URL to show them |
|
|
107
|
+
| `solari_browser_await_login` | Wait for them to finish, then re-attach the session |
|
|
108
|
+
| `solari_browser_save_profile` | Persist the signed-in state to a profile (explicit — see below) |
|
|
106
109
|
| `solari_browser_close` | Release the session |
|
|
107
110
|
|
|
108
111
|
### Browser mode — stealth by default
|
|
@@ -138,6 +141,37 @@ asked for. Stealth ⇒ on, fast ⇒ off, explicit value always wins. Asking for
|
|
|
138
141
|
*together with* `mode: "fast"` is a real contradiction and is refused. The response echoes the
|
|
139
142
|
resolved `captcha` alongside `mode`.
|
|
140
143
|
|
|
144
|
+
### Login handoff — the agent never touches a password
|
|
145
|
+
|
|
146
|
+
When a session hits a login form or 2FA prompt, the agent calls
|
|
147
|
+
`solari_browser_login` and shows the returned URL to the user. They open it, get
|
|
148
|
+
a **live view of the exact page the agent is on**, and type their credentials
|
|
149
|
+
directly into it. The agent then calls `solari_browser_await_login` and carries
|
|
150
|
+
on where it left off — same page, now signed in.
|
|
151
|
+
|
|
152
|
+
**The agent is locked out for the duration.** The gateway refuses its CDP
|
|
153
|
+
upgrades *and* severs the socket it already holds, so it cannot watch the typing
|
|
154
|
+
even though it is mid-session. That is enforced at the proxy, not here: a check
|
|
155
|
+
in this layer would be advisory, because the agent speaks raw CDP.
|
|
156
|
+
|
|
157
|
+
Two consequences worth knowing:
|
|
158
|
+
|
|
159
|
+
- Other browser tools on that session fail while a handoff is open. That is the
|
|
160
|
+
freeze working, not a bug.
|
|
161
|
+
- `await_login` **reconnects** on success. The sever kills the stored puppeteer
|
|
162
|
+
handle, so resuming needs a fresh dial; without it the agent would "resume"
|
|
163
|
+
into a Target-closed error.
|
|
164
|
+
|
|
165
|
+
The link lives 5 minutes and is single-use. If it lapses, call
|
|
166
|
+
`solari_browser_login` again — the session is untouched.
|
|
167
|
+
|
|
168
|
+
**Saving the login for next time is explicit.** `solari_browser_save_profile`
|
|
169
|
+
captures the session's cookies + localStorage into a profile, and future runs
|
|
170
|
+
skip the human entirely via `create({profileId})`. It is deliberately *not*
|
|
171
|
+
automatic: those are live session cookies, which bypass 2FA, so the profile is a
|
|
172
|
+
credential store and should exist because the user chose it. The tool reports the
|
|
173
|
+
cookie count and origins it stored, so they can see what was persisted.
|
|
174
|
+
|
|
141
175
|
### Sandboxes + desktops
|
|
142
176
|
|
|
143
177
|
| Tool | What it does |
|
package/dist/browser.d.ts
CHANGED
|
@@ -26,6 +26,15 @@ interface BrowserEntry {
|
|
|
26
26
|
page: Page;
|
|
27
27
|
expiresAt: string;
|
|
28
28
|
recording: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The session's raw CDP endpoint, kept so the handoff flow can RECONNECT.
|
|
31
|
+
*
|
|
32
|
+
* A login handoff severs the agent's socket gateway-side — deliberately, so
|
|
33
|
+
* the agent cannot watch the human type. That kills this entry's puppeteer
|
|
34
|
+
* handle, so resuming afterwards needs a fresh connect rather than the dead
|
|
35
|
+
* one. Without this the agent "resumes" into a Target-closed error.
|
|
36
|
+
*/
|
|
37
|
+
cdpEndpoint: string;
|
|
29
38
|
}
|
|
30
39
|
export interface BrowserRegistry {
|
|
31
40
|
sessions: Map<string, BrowserEntry>;
|
package/dist/browser.js
CHANGED
|
@@ -12,6 +12,8 @@ const text = (o) => ({
|
|
|
12
12
|
const MAX_PAGE_TEXT = 30_000;
|
|
13
13
|
const MAX_LINKS = 200;
|
|
14
14
|
const CDP_DIAL_ATTEMPTS = 3;
|
|
15
|
+
/** How often await_login re-checks handoff status. */
|
|
16
|
+
const HANDOFF_POLL_MS = 2_000;
|
|
15
17
|
/** Signed ws/wss capability URLs must never reach the model or a log. */
|
|
16
18
|
const redact = (s) => s.replace(/wss?:\/\/[^\s"']+/gi, "[redacted-ws-url]");
|
|
17
19
|
async function api(cfg, method, path, body) {
|
|
@@ -237,6 +239,7 @@ export function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
|
|
|
237
239
|
page,
|
|
238
240
|
expiresAt: s.expiresAt,
|
|
239
241
|
recording: Boolean(a.recording),
|
|
242
|
+
cdpEndpoint: cdp,
|
|
240
243
|
});
|
|
241
244
|
}
|
|
242
245
|
catch (err) {
|
|
@@ -257,6 +260,253 @@ export function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
|
|
|
257
260
|
});
|
|
258
261
|
},
|
|
259
262
|
},
|
|
263
|
+
solari_browser_profiles: {
|
|
264
|
+
description: "List saved browser profiles for this account. A profile is a stored signed-in state " +
|
|
265
|
+
"(cookies + localStorage) that solari_browser_create({profileId}) replays, so the " +
|
|
266
|
+
"session starts already logged in.\n" +
|
|
267
|
+
"Check here FIRST when a task will need a login: if a profile for that site already " +
|
|
268
|
+
"exists, use it and no human is involved at all. `version` is 1 and `populated` is " +
|
|
269
|
+
"false for a profile nobody has signed into yet — that one still needs " +
|
|
270
|
+
"solari_browser_login({profileName}).",
|
|
271
|
+
inputSchema: {},
|
|
272
|
+
handler: async () => {
|
|
273
|
+
const res = await api(cfg, "GET", "/profiles");
|
|
274
|
+
if (res.status !== 200)
|
|
275
|
+
await apiError(res, "list profiles");
|
|
276
|
+
const rows = (await res.json());
|
|
277
|
+
return text({
|
|
278
|
+
profiles: rows.map((p) => ({
|
|
279
|
+
profileId: p.id,
|
|
280
|
+
name: p.name,
|
|
281
|
+
version: p.version,
|
|
282
|
+
populated: Boolean(p.storageStateS3Key),
|
|
283
|
+
lastUsedAt: p.lastUsedAt ?? null,
|
|
284
|
+
})),
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
solari_browser_login: {
|
|
289
|
+
description: "Ask a HUMAN to sign in, so you never handle credentials yourself. Two ways to call it:\n" +
|
|
290
|
+
"HOT — pass `sessionId` when you are ALREADY on a login wall mid-task. The user gets a " +
|
|
291
|
+
"live view of the exact page you are on and types into it; you resume on that same page.\n" +
|
|
292
|
+
"COLD — pass `profileName` when you know a task will need a login and no session is open " +
|
|
293
|
+
"yet. The user signs in once in a profile editor, and every later " +
|
|
294
|
+
"solari_browser_create({profileId}) starts already authenticated. Prefer this when you " +
|
|
295
|
+
"can: nobody has to be watching mid-run. The profile is created if it does not exist.\n" +
|
|
296
|
+
"Pass exactly one of the two. Either way, SHOW THE RETURNED URL TO THE USER, then call " +
|
|
297
|
+
"solari_browser_await_login.\n" +
|
|
298
|
+
"In the HOT case your access to that session is REVOKED while the handoff is open — " +
|
|
299
|
+
"deliberately, so you cannot observe what they type — and the link expires in 5 minutes. " +
|
|
300
|
+
"Cold links last longer and revoke nothing, because there is no session yet.\n" +
|
|
301
|
+
"`reason` is required and is shown to the user — say plainly which site is asking and " +
|
|
302
|
+
"what for, because they are being asked to type a password on your say-so.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
sessionId: z.string().optional(),
|
|
305
|
+
profileName: z.string().optional(),
|
|
306
|
+
reason: z.string(),
|
|
307
|
+
},
|
|
308
|
+
handler: async (a) => {
|
|
309
|
+
const sessionId = typeof a.sessionId === "string" ? a.sessionId : "";
|
|
310
|
+
const profileName = typeof a.profileName === "string" ? a.profileName.trim() : "";
|
|
311
|
+
if (Boolean(sessionId) === Boolean(profileName)) {
|
|
312
|
+
throw new Error("Pass exactly one of sessionId (rescue a live session) or profileName (seed a " +
|
|
313
|
+
"profile before you start).");
|
|
314
|
+
}
|
|
315
|
+
// ── COLD: seed a profile, no session involved ────────────────────
|
|
316
|
+
if (profileName) {
|
|
317
|
+
const listRes = await api(cfg, "GET", "/profiles");
|
|
318
|
+
if (listRes.status !== 200)
|
|
319
|
+
await apiError(listRes, "list profiles");
|
|
320
|
+
const rows = (await listRes.json());
|
|
321
|
+
const match = rows.find((p) => (p.name ?? "").toLowerCase() === profileName.toLowerCase());
|
|
322
|
+
let profileId = match?.id ?? "";
|
|
323
|
+
if (!profileId) {
|
|
324
|
+
const mk = await api(cfg, "POST", "/profiles", { name: profileName });
|
|
325
|
+
if (mk.status !== 200 && mk.status !== 201)
|
|
326
|
+
await apiError(mk, "create profile");
|
|
327
|
+
profileId = (await mk.json()).id ?? "";
|
|
328
|
+
if (!profileId)
|
|
329
|
+
throw new Error("profile created but no id returned");
|
|
330
|
+
}
|
|
331
|
+
const hRes = await api(cfg, "POST", `/profiles/${encodeURIComponent(profileId)}/login-handoff`, {
|
|
332
|
+
reason: a.reason,
|
|
333
|
+
});
|
|
334
|
+
if (hRes.status !== 200)
|
|
335
|
+
await apiError(hRes, "cold login request");
|
|
336
|
+
const h = (await hRes.json());
|
|
337
|
+
return text({
|
|
338
|
+
mode: "cold",
|
|
339
|
+
profileId,
|
|
340
|
+
profileName,
|
|
341
|
+
handoffId: h.handoffId,
|
|
342
|
+
url: h.url,
|
|
343
|
+
expiresAt: h.expiresAt,
|
|
344
|
+
// Echoed so await_login can tell a fresh save from the state it
|
|
345
|
+
// started in — the version bump IS the completion signal.
|
|
346
|
+
sinceVersion: h.version,
|
|
347
|
+
next: `Show the url to the user, then call solari_browser_await_login({ profileId: ` +
|
|
348
|
+
`"${profileId}", sinceVersion: ${h.version} }).`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// ── HOT: rescue the live session ─────────────────────────────────
|
|
352
|
+
const e = need(sessionId);
|
|
353
|
+
const res = await api(cfg, "POST", `/sessions/${encodeURIComponent(sessionId)}/handoff`, { reason: a.reason });
|
|
354
|
+
if (res.status !== 200)
|
|
355
|
+
await apiError(res, "handoff request");
|
|
356
|
+
const h = (await res.json());
|
|
357
|
+
// The gateway is severing our socket right now. Drop the local handle
|
|
358
|
+
// rather than leave a half-dead Browser object that throws confusing
|
|
359
|
+
// Target-closed errors on every subsequent tool call.
|
|
360
|
+
try {
|
|
361
|
+
await e.browser.close();
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
/* already gone — that is the point */
|
|
365
|
+
}
|
|
366
|
+
return text({
|
|
367
|
+
mode: "hot",
|
|
368
|
+
handoffId: h.handoffId,
|
|
369
|
+
url: h.url,
|
|
370
|
+
expiresAt: h.expiresAt,
|
|
371
|
+
next: "Show the url to the user, then call solari_browser_await_login.",
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
solari_browser_save_profile: {
|
|
376
|
+
description: "Save this session's signed-in state (cookies + localStorage) into an existing profile, " +
|
|
377
|
+
"so future sessions start already logged in and no human is needed again. Pass the " +
|
|
378
|
+
"profileId to overwrite.\n" +
|
|
379
|
+
"ONLY do this when the user has asked you to remember the login. It is deliberately not " +
|
|
380
|
+
"automatic: a saved profile holds live session cookies, which bypass 2FA — it is a " +
|
|
381
|
+
"credential store, and it should exist because someone chose it. If they have not said " +
|
|
382
|
+
"so, ask first.\n" +
|
|
383
|
+
"Returns what was captured (cookie count and origins) so the user can see what they " +
|
|
384
|
+
"just persisted.",
|
|
385
|
+
inputSchema: {
|
|
386
|
+
sessionId: z.string(),
|
|
387
|
+
profileId: z.string(),
|
|
388
|
+
},
|
|
389
|
+
handler: async (a) => {
|
|
390
|
+
need(a.sessionId);
|
|
391
|
+
const res = await api(cfg, "POST", `/sessions/${encodeURIComponent(a.sessionId)}/save-profile`, { profileId: a.profileId });
|
|
392
|
+
if (res.status !== 200)
|
|
393
|
+
await apiError(res, "save profile");
|
|
394
|
+
const body = (await res.json());
|
|
395
|
+
return text({
|
|
396
|
+
...body,
|
|
397
|
+
note: "Future sessions can use this with solari_browser_create({profileId}).",
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
solari_browser_await_login: {
|
|
402
|
+
description: "Wait for the human to finish the sign-in you requested with solari_browser_login. " +
|
|
403
|
+
"Blocks until they are done, the link expires, or timeoutMs elapses. Returns only a " +
|
|
404
|
+
"status — never anything the user typed. On 'completed' the session is yours again, " +
|
|
405
|
+
"on the same page, still signed in, and you can carry on where you left off. On " +
|
|
406
|
+
"'expired' or 'timeout' the user did not finish: ask if they still want to, and call " +
|
|
407
|
+
"solari_browser_login again for a fresh link rather than retrying this.",
|
|
408
|
+
inputSchema: {
|
|
409
|
+
sessionId: z.string().optional(),
|
|
410
|
+
profileId: z.string().optional(),
|
|
411
|
+
sinceVersion: z.number().optional(),
|
|
412
|
+
handoffId: z.string().optional(),
|
|
413
|
+
timeoutMs: z.number().optional(),
|
|
414
|
+
},
|
|
415
|
+
handler: async (a) => {
|
|
416
|
+
const sessionId = typeof a.sessionId === "string" ? a.sessionId : "";
|
|
417
|
+
const profileId = typeof a.profileId === "string" ? a.profileId : "";
|
|
418
|
+
if (Boolean(sessionId) === Boolean(profileId)) {
|
|
419
|
+
throw new Error("Pass exactly one of sessionId (hot handoff) or profileId (cold handoff) — the " +
|
|
420
|
+
"same one solari_browser_login returned.");
|
|
421
|
+
}
|
|
422
|
+
// Clamp: never spin forever, never poll so briefly the human has no
|
|
423
|
+
// chance. Defaults to the handoff's own 5-minute life.
|
|
424
|
+
const requested = typeof a.timeoutMs === "number" ? a.timeoutMs : 300_000;
|
|
425
|
+
const deadline = Date.now() + Math.min(Math.max(requested, 5_000), 600_000);
|
|
426
|
+
let status = "pending";
|
|
427
|
+
// ── COLD: watch the profile's version, which the editor's save bumps.
|
|
428
|
+
// Deliberately NOT "did a storageState appear": re-signing into a
|
|
429
|
+
// profile that already had one must also count as completed.
|
|
430
|
+
if (profileId) {
|
|
431
|
+
const since = typeof a.sinceVersion === "number" ? a.sinceVersion : 0;
|
|
432
|
+
let version = since;
|
|
433
|
+
while (Date.now() < deadline) {
|
|
434
|
+
const res = await api(cfg, "GET", "/profiles");
|
|
435
|
+
if (res.status !== 200)
|
|
436
|
+
await apiError(res, "profile status");
|
|
437
|
+
const rows = (await res.json());
|
|
438
|
+
const row = rows.find((p) => p.id === profileId);
|
|
439
|
+
if (!row)
|
|
440
|
+
throw new Error(`profile ${profileId} no longer exists`);
|
|
441
|
+
version = typeof row.version === "number" ? row.version : since;
|
|
442
|
+
if (version > since) {
|
|
443
|
+
status = "completed";
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
await new Promise((r) => setTimeout(r, HANDOFF_POLL_MS));
|
|
447
|
+
}
|
|
448
|
+
if (status !== "completed")
|
|
449
|
+
status = "timeout";
|
|
450
|
+
return text({
|
|
451
|
+
mode: "cold",
|
|
452
|
+
status,
|
|
453
|
+
profileId,
|
|
454
|
+
version,
|
|
455
|
+
next: status === "completed"
|
|
456
|
+
? `Signed in and saved. Use solari_browser_create({ profileId: "${profileId}" }) ` +
|
|
457
|
+
"and you will start already authenticated."
|
|
458
|
+
: "The user did not finish. Ask if they still want to, then call " +
|
|
459
|
+
"solari_browser_login again for a fresh link.",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
const e = need(sessionId);
|
|
463
|
+
while (Date.now() < deadline) {
|
|
464
|
+
const q = typeof a.handoffId === "string" && a.handoffId
|
|
465
|
+
? `?handoffId=${encodeURIComponent(a.handoffId)}`
|
|
466
|
+
: "";
|
|
467
|
+
const res = await api(cfg, "GET", `/sessions/${encodeURIComponent(sessionId)}/handoff${q}`);
|
|
468
|
+
if (res.status !== 200)
|
|
469
|
+
await apiError(res, "handoff status");
|
|
470
|
+
const body = (await res.json());
|
|
471
|
+
status = body.status ?? "none";
|
|
472
|
+
// "none" means no open handoff and no id to look up — treat as done
|
|
473
|
+
// rather than spinning; the caller can re-mint if that was wrong.
|
|
474
|
+
if (status !== "pending")
|
|
475
|
+
break;
|
|
476
|
+
await new Promise((r) => setTimeout(r, HANDOFF_POLL_MS));
|
|
477
|
+
}
|
|
478
|
+
if (status === "pending")
|
|
479
|
+
status = "timeout";
|
|
480
|
+
// Reconnect: the handoff severed our old socket on purpose, so the
|
|
481
|
+
// stored handle is dead even on success. Re-dial and re-adopt the page
|
|
482
|
+
// the human left us on, which is the whole point of a HOT handoff —
|
|
483
|
+
// the agent resumes mid-flow instead of starting over.
|
|
484
|
+
let resumed = false;
|
|
485
|
+
if (status === "completed" || status === "none") {
|
|
486
|
+
try {
|
|
487
|
+
const browser = await deps.connect(e.cdpEndpoint);
|
|
488
|
+
const pages = await browser.pages();
|
|
489
|
+
const page = pages.find((p) => !p.isClosed()) ?? (await browser.newPage());
|
|
490
|
+
e.browser = browser;
|
|
491
|
+
e.page = page;
|
|
492
|
+
resumed = true;
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
return text({
|
|
496
|
+
status,
|
|
497
|
+
resumed: false,
|
|
498
|
+
note: "the sign-in finished but the session could not be re-attached: " +
|
|
499
|
+
redact(err instanceof Error ? err.message : String(err)),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return text({
|
|
504
|
+
status,
|
|
505
|
+
resumed,
|
|
506
|
+
...(resumed ? { url: e.page.url() } : {}),
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
},
|
|
260
510
|
solari_browser_navigate: {
|
|
261
511
|
description: "Navigate the browser session to a URL. Returns final url, title and HTTP status.",
|
|
262
512
|
inputSchema: { sessionId: z.string(), url: z.string() },
|
|
@@ -113676,6 +113676,7 @@ var text = (o) => ({
|
|
|
113676
113676
|
var MAX_PAGE_TEXT = 3e4;
|
|
113677
113677
|
var MAX_LINKS = 200;
|
|
113678
113678
|
var CDP_DIAL_ATTEMPTS = 3;
|
|
113679
|
+
var HANDOFF_POLL_MS = 2e3;
|
|
113679
113680
|
var redact = (s) => s.replace(/wss?:\/\/[^\s"']+/gi, "[redacted-ws-url]");
|
|
113680
113681
|
async function api(cfg, method, path12, body) {
|
|
113681
113682
|
const res = await fetch(`${cfg.baseUrl}${path12}`, {
|
|
@@ -113821,7 +113822,8 @@ function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
|
|
|
113821
113822
|
browser,
|
|
113822
113823
|
page,
|
|
113823
113824
|
expiresAt: s.expiresAt,
|
|
113824
|
-
recording: Boolean(a2.recording)
|
|
113825
|
+
recording: Boolean(a2.recording),
|
|
113826
|
+
cdpEndpoint: cdp
|
|
113825
113827
|
});
|
|
113826
113828
|
} catch (err) {
|
|
113827
113829
|
await releaseBrowserSession(cfg, s.sessionId, api2).catch(() => {
|
|
@@ -113839,6 +113841,199 @@ function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
|
|
|
113839
113841
|
});
|
|
113840
113842
|
}
|
|
113841
113843
|
},
|
|
113844
|
+
solari_browser_profiles: {
|
|
113845
|
+
description: "List saved browser profiles for this account. A profile is a stored signed-in state (cookies + localStorage) that solari_browser_create({profileId}) replays, so the session starts already logged in.\nCheck here FIRST when a task will need a login: if a profile for that site already exists, use it and no human is involved at all. `version` is 1 and `populated` is false for a profile nobody has signed into yet \u2014 that one still needs solari_browser_login({profileName}).",
|
|
113846
|
+
inputSchema: {},
|
|
113847
|
+
handler: async () => {
|
|
113848
|
+
const res = await api2(cfg, "GET", "/profiles");
|
|
113849
|
+
if (res.status !== 200) await apiError(res, "list profiles");
|
|
113850
|
+
const rows = await res.json();
|
|
113851
|
+
return text({
|
|
113852
|
+
profiles: rows.map((p) => ({
|
|
113853
|
+
profileId: p.id,
|
|
113854
|
+
name: p.name,
|
|
113855
|
+
version: p.version,
|
|
113856
|
+
populated: Boolean(p.storageStateS3Key),
|
|
113857
|
+
lastUsedAt: p.lastUsedAt ?? null
|
|
113858
|
+
}))
|
|
113859
|
+
});
|
|
113860
|
+
}
|
|
113861
|
+
},
|
|
113862
|
+
solari_browser_login: {
|
|
113863
|
+
description: "Ask a HUMAN to sign in, so you never handle credentials yourself. Two ways to call it:\nHOT \u2014 pass `sessionId` when you are ALREADY on a login wall mid-task. The user gets a live view of the exact page you are on and types into it; you resume on that same page.\nCOLD \u2014 pass `profileName` when you know a task will need a login and no session is open yet. The user signs in once in a profile editor, and every later solari_browser_create({profileId}) starts already authenticated. Prefer this when you can: nobody has to be watching mid-run. The profile is created if it does not exist.\nPass exactly one of the two. Either way, SHOW THE RETURNED URL TO THE USER, then call solari_browser_await_login.\nIn the HOT case your access to that session is REVOKED while the handoff is open \u2014 deliberately, so you cannot observe what they type \u2014 and the link expires in 5 minutes. Cold links last longer and revoke nothing, because there is no session yet.\n`reason` is required and is shown to the user \u2014 say plainly which site is asking and what for, because they are being asked to type a password on your say-so.",
|
|
113864
|
+
inputSchema: {
|
|
113865
|
+
sessionId: external_exports.string().optional(),
|
|
113866
|
+
profileName: external_exports.string().optional(),
|
|
113867
|
+
reason: external_exports.string()
|
|
113868
|
+
},
|
|
113869
|
+
handler: async (a2) => {
|
|
113870
|
+
const sessionId = typeof a2.sessionId === "string" ? a2.sessionId : "";
|
|
113871
|
+
const profileName = typeof a2.profileName === "string" ? a2.profileName.trim() : "";
|
|
113872
|
+
if (Boolean(sessionId) === Boolean(profileName)) {
|
|
113873
|
+
throw new Error(
|
|
113874
|
+
"Pass exactly one of sessionId (rescue a live session) or profileName (seed a profile before you start)."
|
|
113875
|
+
);
|
|
113876
|
+
}
|
|
113877
|
+
if (profileName) {
|
|
113878
|
+
const listRes = await api2(cfg, "GET", "/profiles");
|
|
113879
|
+
if (listRes.status !== 200) await apiError(listRes, "list profiles");
|
|
113880
|
+
const rows = await listRes.json();
|
|
113881
|
+
const match = rows.find(
|
|
113882
|
+
(p) => (p.name ?? "").toLowerCase() === profileName.toLowerCase()
|
|
113883
|
+
);
|
|
113884
|
+
let profileId = match?.id ?? "";
|
|
113885
|
+
if (!profileId) {
|
|
113886
|
+
const mk = await api2(cfg, "POST", "/profiles", { name: profileName });
|
|
113887
|
+
if (mk.status !== 200 && mk.status !== 201) await apiError(mk, "create profile");
|
|
113888
|
+
profileId = (await mk.json()).id ?? "";
|
|
113889
|
+
if (!profileId) throw new Error("profile created but no id returned");
|
|
113890
|
+
}
|
|
113891
|
+
const hRes = await api2(cfg, "POST", `/profiles/${encodeURIComponent(profileId)}/login-handoff`, {
|
|
113892
|
+
reason: a2.reason
|
|
113893
|
+
});
|
|
113894
|
+
if (hRes.status !== 200) await apiError(hRes, "cold login request");
|
|
113895
|
+
const h2 = await hRes.json();
|
|
113896
|
+
return text({
|
|
113897
|
+
mode: "cold",
|
|
113898
|
+
profileId,
|
|
113899
|
+
profileName,
|
|
113900
|
+
handoffId: h2.handoffId,
|
|
113901
|
+
url: h2.url,
|
|
113902
|
+
expiresAt: h2.expiresAt,
|
|
113903
|
+
// Echoed so await_login can tell a fresh save from the state it
|
|
113904
|
+
// started in — the version bump IS the completion signal.
|
|
113905
|
+
sinceVersion: h2.version,
|
|
113906
|
+
next: `Show the url to the user, then call solari_browser_await_login({ profileId: "${profileId}", sinceVersion: ${h2.version} }).`
|
|
113907
|
+
});
|
|
113908
|
+
}
|
|
113909
|
+
const e = need(sessionId);
|
|
113910
|
+
const res = await api2(
|
|
113911
|
+
cfg,
|
|
113912
|
+
"POST",
|
|
113913
|
+
`/sessions/${encodeURIComponent(sessionId)}/handoff`,
|
|
113914
|
+
{ reason: a2.reason }
|
|
113915
|
+
);
|
|
113916
|
+
if (res.status !== 200) await apiError(res, "handoff request");
|
|
113917
|
+
const h = await res.json();
|
|
113918
|
+
try {
|
|
113919
|
+
await e.browser.close();
|
|
113920
|
+
} catch {
|
|
113921
|
+
}
|
|
113922
|
+
return text({
|
|
113923
|
+
mode: "hot",
|
|
113924
|
+
handoffId: h.handoffId,
|
|
113925
|
+
url: h.url,
|
|
113926
|
+
expiresAt: h.expiresAt,
|
|
113927
|
+
next: "Show the url to the user, then call solari_browser_await_login."
|
|
113928
|
+
});
|
|
113929
|
+
}
|
|
113930
|
+
},
|
|
113931
|
+
solari_browser_save_profile: {
|
|
113932
|
+
description: "Save this session's signed-in state (cookies + localStorage) into an existing profile, so future sessions start already logged in and no human is needed again. Pass the profileId to overwrite.\nONLY do this when the user has asked you to remember the login. It is deliberately not automatic: a saved profile holds live session cookies, which bypass 2FA \u2014 it is a credential store, and it should exist because someone chose it. If they have not said so, ask first.\nReturns what was captured (cookie count and origins) so the user can see what they just persisted.",
|
|
113933
|
+
inputSchema: {
|
|
113934
|
+
sessionId: external_exports.string(),
|
|
113935
|
+
profileId: external_exports.string()
|
|
113936
|
+
},
|
|
113937
|
+
handler: async (a2) => {
|
|
113938
|
+
need(a2.sessionId);
|
|
113939
|
+
const res = await api2(
|
|
113940
|
+
cfg,
|
|
113941
|
+
"POST",
|
|
113942
|
+
`/sessions/${encodeURIComponent(a2.sessionId)}/save-profile`,
|
|
113943
|
+
{ profileId: a2.profileId }
|
|
113944
|
+
);
|
|
113945
|
+
if (res.status !== 200) await apiError(res, "save profile");
|
|
113946
|
+
const body = await res.json();
|
|
113947
|
+
return text({
|
|
113948
|
+
...body,
|
|
113949
|
+
note: "Future sessions can use this with solari_browser_create({profileId})."
|
|
113950
|
+
});
|
|
113951
|
+
}
|
|
113952
|
+
},
|
|
113953
|
+
solari_browser_await_login: {
|
|
113954
|
+
description: "Wait for the human to finish the sign-in you requested with solari_browser_login. Blocks until they are done, the link expires, or timeoutMs elapses. Returns only a status \u2014 never anything the user typed. On 'completed' the session is yours again, on the same page, still signed in, and you can carry on where you left off. On 'expired' or 'timeout' the user did not finish: ask if they still want to, and call solari_browser_login again for a fresh link rather than retrying this.",
|
|
113955
|
+
inputSchema: {
|
|
113956
|
+
sessionId: external_exports.string().optional(),
|
|
113957
|
+
profileId: external_exports.string().optional(),
|
|
113958
|
+
sinceVersion: external_exports.number().optional(),
|
|
113959
|
+
handoffId: external_exports.string().optional(),
|
|
113960
|
+
timeoutMs: external_exports.number().optional()
|
|
113961
|
+
},
|
|
113962
|
+
handler: async (a2) => {
|
|
113963
|
+
const sessionId = typeof a2.sessionId === "string" ? a2.sessionId : "";
|
|
113964
|
+
const profileId = typeof a2.profileId === "string" ? a2.profileId : "";
|
|
113965
|
+
if (Boolean(sessionId) === Boolean(profileId)) {
|
|
113966
|
+
throw new Error(
|
|
113967
|
+
"Pass exactly one of sessionId (hot handoff) or profileId (cold handoff) \u2014 the same one solari_browser_login returned."
|
|
113968
|
+
);
|
|
113969
|
+
}
|
|
113970
|
+
const requested = typeof a2.timeoutMs === "number" ? a2.timeoutMs : 3e5;
|
|
113971
|
+
const deadline = Date.now() + Math.min(Math.max(requested, 5e3), 6e5);
|
|
113972
|
+
let status = "pending";
|
|
113973
|
+
if (profileId) {
|
|
113974
|
+
const since = typeof a2.sinceVersion === "number" ? a2.sinceVersion : 0;
|
|
113975
|
+
let version2 = since;
|
|
113976
|
+
while (Date.now() < deadline) {
|
|
113977
|
+
const res = await api2(cfg, "GET", "/profiles");
|
|
113978
|
+
if (res.status !== 200) await apiError(res, "profile status");
|
|
113979
|
+
const rows = await res.json();
|
|
113980
|
+
const row = rows.find((p) => p.id === profileId);
|
|
113981
|
+
if (!row) throw new Error(`profile ${profileId} no longer exists`);
|
|
113982
|
+
version2 = typeof row.version === "number" ? row.version : since;
|
|
113983
|
+
if (version2 > since) {
|
|
113984
|
+
status = "completed";
|
|
113985
|
+
break;
|
|
113986
|
+
}
|
|
113987
|
+
await new Promise((r) => setTimeout(r, HANDOFF_POLL_MS));
|
|
113988
|
+
}
|
|
113989
|
+
if (status !== "completed") status = "timeout";
|
|
113990
|
+
return text({
|
|
113991
|
+
mode: "cold",
|
|
113992
|
+
status,
|
|
113993
|
+
profileId,
|
|
113994
|
+
version: version2,
|
|
113995
|
+
next: status === "completed" ? `Signed in and saved. Use solari_browser_create({ profileId: "${profileId}" }) and you will start already authenticated.` : "The user did not finish. Ask if they still want to, then call solari_browser_login again for a fresh link."
|
|
113996
|
+
});
|
|
113997
|
+
}
|
|
113998
|
+
const e = need(sessionId);
|
|
113999
|
+
while (Date.now() < deadline) {
|
|
114000
|
+
const q2 = typeof a2.handoffId === "string" && a2.handoffId ? `?handoffId=${encodeURIComponent(a2.handoffId)}` : "";
|
|
114001
|
+
const res = await api2(
|
|
114002
|
+
cfg,
|
|
114003
|
+
"GET",
|
|
114004
|
+
`/sessions/${encodeURIComponent(sessionId)}/handoff${q2}`
|
|
114005
|
+
);
|
|
114006
|
+
if (res.status !== 200) await apiError(res, "handoff status");
|
|
114007
|
+
const body = await res.json();
|
|
114008
|
+
status = body.status ?? "none";
|
|
114009
|
+
if (status !== "pending") break;
|
|
114010
|
+
await new Promise((r) => setTimeout(r, HANDOFF_POLL_MS));
|
|
114011
|
+
}
|
|
114012
|
+
if (status === "pending") status = "timeout";
|
|
114013
|
+
let resumed = false;
|
|
114014
|
+
if (status === "completed" || status === "none") {
|
|
114015
|
+
try {
|
|
114016
|
+
const browser = await deps.connect(e.cdpEndpoint);
|
|
114017
|
+
const pages = await browser.pages();
|
|
114018
|
+
const page = pages.find((p) => !p.isClosed()) ?? await browser.newPage();
|
|
114019
|
+
e.browser = browser;
|
|
114020
|
+
e.page = page;
|
|
114021
|
+
resumed = true;
|
|
114022
|
+
} catch (err) {
|
|
114023
|
+
return text({
|
|
114024
|
+
status,
|
|
114025
|
+
resumed: false,
|
|
114026
|
+
note: "the sign-in finished but the session could not be re-attached: " + redact(err instanceof Error ? err.message : String(err))
|
|
114027
|
+
});
|
|
114028
|
+
}
|
|
114029
|
+
}
|
|
114030
|
+
return text({
|
|
114031
|
+
status,
|
|
114032
|
+
resumed,
|
|
114033
|
+
...resumed ? { url: e.page.url() } : {}
|
|
114034
|
+
});
|
|
114035
|
+
}
|
|
114036
|
+
},
|
|
113842
114037
|
solari_browser_navigate: {
|
|
113843
114038
|
description: "Navigate the browser session to a URL. Returns final url, title and HTTP status.",
|
|
113844
114039
|
inputSchema: { sessionId: external_exports.string(), url: external_exports.string() },
|
|
@@ -112345,6 +112345,7 @@ var text = (o) => ({
|
|
|
112345
112345
|
var MAX_PAGE_TEXT = 3e4;
|
|
112346
112346
|
var MAX_LINKS = 200;
|
|
112347
112347
|
var CDP_DIAL_ATTEMPTS = 3;
|
|
112348
|
+
var HANDOFF_POLL_MS = 2e3;
|
|
112348
112349
|
var redact = (s) => s.replace(/wss?:\/\/[^\s"']+/gi, "[redacted-ws-url]");
|
|
112349
112350
|
async function api(cfg, method, path12, body) {
|
|
112350
112351
|
const res = await fetch(`${cfg.baseUrl}${path12}`, {
|
|
@@ -112490,7 +112491,8 @@ function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
|
|
|
112490
112491
|
browser,
|
|
112491
112492
|
page,
|
|
112492
112493
|
expiresAt: s.expiresAt,
|
|
112493
|
-
recording: Boolean(a2.recording)
|
|
112494
|
+
recording: Boolean(a2.recording),
|
|
112495
|
+
cdpEndpoint: cdp
|
|
112494
112496
|
});
|
|
112495
112497
|
} catch (err) {
|
|
112496
112498
|
await releaseBrowserSession(cfg, s.sessionId, api2).catch(() => {
|
|
@@ -112508,6 +112510,199 @@ function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
|
|
|
112508
112510
|
});
|
|
112509
112511
|
}
|
|
112510
112512
|
},
|
|
112513
|
+
solari_browser_profiles: {
|
|
112514
|
+
description: "List saved browser profiles for this account. A profile is a stored signed-in state (cookies + localStorage) that solari_browser_create({profileId}) replays, so the session starts already logged in.\nCheck here FIRST when a task will need a login: if a profile for that site already exists, use it and no human is involved at all. `version` is 1 and `populated` is false for a profile nobody has signed into yet \u2014 that one still needs solari_browser_login({profileName}).",
|
|
112515
|
+
inputSchema: {},
|
|
112516
|
+
handler: async () => {
|
|
112517
|
+
const res = await api2(cfg, "GET", "/profiles");
|
|
112518
|
+
if (res.status !== 200) await apiError(res, "list profiles");
|
|
112519
|
+
const rows = await res.json();
|
|
112520
|
+
return text({
|
|
112521
|
+
profiles: rows.map((p) => ({
|
|
112522
|
+
profileId: p.id,
|
|
112523
|
+
name: p.name,
|
|
112524
|
+
version: p.version,
|
|
112525
|
+
populated: Boolean(p.storageStateS3Key),
|
|
112526
|
+
lastUsedAt: p.lastUsedAt ?? null
|
|
112527
|
+
}))
|
|
112528
|
+
});
|
|
112529
|
+
}
|
|
112530
|
+
},
|
|
112531
|
+
solari_browser_login: {
|
|
112532
|
+
description: "Ask a HUMAN to sign in, so you never handle credentials yourself. Two ways to call it:\nHOT \u2014 pass `sessionId` when you are ALREADY on a login wall mid-task. The user gets a live view of the exact page you are on and types into it; you resume on that same page.\nCOLD \u2014 pass `profileName` when you know a task will need a login and no session is open yet. The user signs in once in a profile editor, and every later solari_browser_create({profileId}) starts already authenticated. Prefer this when you can: nobody has to be watching mid-run. The profile is created if it does not exist.\nPass exactly one of the two. Either way, SHOW THE RETURNED URL TO THE USER, then call solari_browser_await_login.\nIn the HOT case your access to that session is REVOKED while the handoff is open \u2014 deliberately, so you cannot observe what they type \u2014 and the link expires in 5 minutes. Cold links last longer and revoke nothing, because there is no session yet.\n`reason` is required and is shown to the user \u2014 say plainly which site is asking and what for, because they are being asked to type a password on your say-so.",
|
|
112533
|
+
inputSchema: {
|
|
112534
|
+
sessionId: external_exports.string().optional(),
|
|
112535
|
+
profileName: external_exports.string().optional(),
|
|
112536
|
+
reason: external_exports.string()
|
|
112537
|
+
},
|
|
112538
|
+
handler: async (a2) => {
|
|
112539
|
+
const sessionId = typeof a2.sessionId === "string" ? a2.sessionId : "";
|
|
112540
|
+
const profileName = typeof a2.profileName === "string" ? a2.profileName.trim() : "";
|
|
112541
|
+
if (Boolean(sessionId) === Boolean(profileName)) {
|
|
112542
|
+
throw new Error(
|
|
112543
|
+
"Pass exactly one of sessionId (rescue a live session) or profileName (seed a profile before you start)."
|
|
112544
|
+
);
|
|
112545
|
+
}
|
|
112546
|
+
if (profileName) {
|
|
112547
|
+
const listRes = await api2(cfg, "GET", "/profiles");
|
|
112548
|
+
if (listRes.status !== 200) await apiError(listRes, "list profiles");
|
|
112549
|
+
const rows = await listRes.json();
|
|
112550
|
+
const match = rows.find(
|
|
112551
|
+
(p) => (p.name ?? "").toLowerCase() === profileName.toLowerCase()
|
|
112552
|
+
);
|
|
112553
|
+
let profileId = match?.id ?? "";
|
|
112554
|
+
if (!profileId) {
|
|
112555
|
+
const mk = await api2(cfg, "POST", "/profiles", { name: profileName });
|
|
112556
|
+
if (mk.status !== 200 && mk.status !== 201) await apiError(mk, "create profile");
|
|
112557
|
+
profileId = (await mk.json()).id ?? "";
|
|
112558
|
+
if (!profileId) throw new Error("profile created but no id returned");
|
|
112559
|
+
}
|
|
112560
|
+
const hRes = await api2(cfg, "POST", `/profiles/${encodeURIComponent(profileId)}/login-handoff`, {
|
|
112561
|
+
reason: a2.reason
|
|
112562
|
+
});
|
|
112563
|
+
if (hRes.status !== 200) await apiError(hRes, "cold login request");
|
|
112564
|
+
const h2 = await hRes.json();
|
|
112565
|
+
return text({
|
|
112566
|
+
mode: "cold",
|
|
112567
|
+
profileId,
|
|
112568
|
+
profileName,
|
|
112569
|
+
handoffId: h2.handoffId,
|
|
112570
|
+
url: h2.url,
|
|
112571
|
+
expiresAt: h2.expiresAt,
|
|
112572
|
+
// Echoed so await_login can tell a fresh save from the state it
|
|
112573
|
+
// started in — the version bump IS the completion signal.
|
|
112574
|
+
sinceVersion: h2.version,
|
|
112575
|
+
next: `Show the url to the user, then call solari_browser_await_login({ profileId: "${profileId}", sinceVersion: ${h2.version} }).`
|
|
112576
|
+
});
|
|
112577
|
+
}
|
|
112578
|
+
const e = need(sessionId);
|
|
112579
|
+
const res = await api2(
|
|
112580
|
+
cfg,
|
|
112581
|
+
"POST",
|
|
112582
|
+
`/sessions/${encodeURIComponent(sessionId)}/handoff`,
|
|
112583
|
+
{ reason: a2.reason }
|
|
112584
|
+
);
|
|
112585
|
+
if (res.status !== 200) await apiError(res, "handoff request");
|
|
112586
|
+
const h = await res.json();
|
|
112587
|
+
try {
|
|
112588
|
+
await e.browser.close();
|
|
112589
|
+
} catch {
|
|
112590
|
+
}
|
|
112591
|
+
return text({
|
|
112592
|
+
mode: "hot",
|
|
112593
|
+
handoffId: h.handoffId,
|
|
112594
|
+
url: h.url,
|
|
112595
|
+
expiresAt: h.expiresAt,
|
|
112596
|
+
next: "Show the url to the user, then call solari_browser_await_login."
|
|
112597
|
+
});
|
|
112598
|
+
}
|
|
112599
|
+
},
|
|
112600
|
+
solari_browser_save_profile: {
|
|
112601
|
+
description: "Save this session's signed-in state (cookies + localStorage) into an existing profile, so future sessions start already logged in and no human is needed again. Pass the profileId to overwrite.\nONLY do this when the user has asked you to remember the login. It is deliberately not automatic: a saved profile holds live session cookies, which bypass 2FA \u2014 it is a credential store, and it should exist because someone chose it. If they have not said so, ask first.\nReturns what was captured (cookie count and origins) so the user can see what they just persisted.",
|
|
112602
|
+
inputSchema: {
|
|
112603
|
+
sessionId: external_exports.string(),
|
|
112604
|
+
profileId: external_exports.string()
|
|
112605
|
+
},
|
|
112606
|
+
handler: async (a2) => {
|
|
112607
|
+
need(a2.sessionId);
|
|
112608
|
+
const res = await api2(
|
|
112609
|
+
cfg,
|
|
112610
|
+
"POST",
|
|
112611
|
+
`/sessions/${encodeURIComponent(a2.sessionId)}/save-profile`,
|
|
112612
|
+
{ profileId: a2.profileId }
|
|
112613
|
+
);
|
|
112614
|
+
if (res.status !== 200) await apiError(res, "save profile");
|
|
112615
|
+
const body = await res.json();
|
|
112616
|
+
return text({
|
|
112617
|
+
...body,
|
|
112618
|
+
note: "Future sessions can use this with solari_browser_create({profileId})."
|
|
112619
|
+
});
|
|
112620
|
+
}
|
|
112621
|
+
},
|
|
112622
|
+
solari_browser_await_login: {
|
|
112623
|
+
description: "Wait for the human to finish the sign-in you requested with solari_browser_login. Blocks until they are done, the link expires, or timeoutMs elapses. Returns only a status \u2014 never anything the user typed. On 'completed' the session is yours again, on the same page, still signed in, and you can carry on where you left off. On 'expired' or 'timeout' the user did not finish: ask if they still want to, and call solari_browser_login again for a fresh link rather than retrying this.",
|
|
112624
|
+
inputSchema: {
|
|
112625
|
+
sessionId: external_exports.string().optional(),
|
|
112626
|
+
profileId: external_exports.string().optional(),
|
|
112627
|
+
sinceVersion: external_exports.number().optional(),
|
|
112628
|
+
handoffId: external_exports.string().optional(),
|
|
112629
|
+
timeoutMs: external_exports.number().optional()
|
|
112630
|
+
},
|
|
112631
|
+
handler: async (a2) => {
|
|
112632
|
+
const sessionId = typeof a2.sessionId === "string" ? a2.sessionId : "";
|
|
112633
|
+
const profileId = typeof a2.profileId === "string" ? a2.profileId : "";
|
|
112634
|
+
if (Boolean(sessionId) === Boolean(profileId)) {
|
|
112635
|
+
throw new Error(
|
|
112636
|
+
"Pass exactly one of sessionId (hot handoff) or profileId (cold handoff) \u2014 the same one solari_browser_login returned."
|
|
112637
|
+
);
|
|
112638
|
+
}
|
|
112639
|
+
const requested = typeof a2.timeoutMs === "number" ? a2.timeoutMs : 3e5;
|
|
112640
|
+
const deadline = Date.now() + Math.min(Math.max(requested, 5e3), 6e5);
|
|
112641
|
+
let status = "pending";
|
|
112642
|
+
if (profileId) {
|
|
112643
|
+
const since = typeof a2.sinceVersion === "number" ? a2.sinceVersion : 0;
|
|
112644
|
+
let version2 = since;
|
|
112645
|
+
while (Date.now() < deadline) {
|
|
112646
|
+
const res = await api2(cfg, "GET", "/profiles");
|
|
112647
|
+
if (res.status !== 200) await apiError(res, "profile status");
|
|
112648
|
+
const rows = await res.json();
|
|
112649
|
+
const row = rows.find((p) => p.id === profileId);
|
|
112650
|
+
if (!row) throw new Error(`profile ${profileId} no longer exists`);
|
|
112651
|
+
version2 = typeof row.version === "number" ? row.version : since;
|
|
112652
|
+
if (version2 > since) {
|
|
112653
|
+
status = "completed";
|
|
112654
|
+
break;
|
|
112655
|
+
}
|
|
112656
|
+
await new Promise((r) => setTimeout(r, HANDOFF_POLL_MS));
|
|
112657
|
+
}
|
|
112658
|
+
if (status !== "completed") status = "timeout";
|
|
112659
|
+
return text({
|
|
112660
|
+
mode: "cold",
|
|
112661
|
+
status,
|
|
112662
|
+
profileId,
|
|
112663
|
+
version: version2,
|
|
112664
|
+
next: status === "completed" ? `Signed in and saved. Use solari_browser_create({ profileId: "${profileId}" }) and you will start already authenticated.` : "The user did not finish. Ask if they still want to, then call solari_browser_login again for a fresh link."
|
|
112665
|
+
});
|
|
112666
|
+
}
|
|
112667
|
+
const e = need(sessionId);
|
|
112668
|
+
while (Date.now() < deadline) {
|
|
112669
|
+
const q2 = typeof a2.handoffId === "string" && a2.handoffId ? `?handoffId=${encodeURIComponent(a2.handoffId)}` : "";
|
|
112670
|
+
const res = await api2(
|
|
112671
|
+
cfg,
|
|
112672
|
+
"GET",
|
|
112673
|
+
`/sessions/${encodeURIComponent(sessionId)}/handoff${q2}`
|
|
112674
|
+
);
|
|
112675
|
+
if (res.status !== 200) await apiError(res, "handoff status");
|
|
112676
|
+
const body = await res.json();
|
|
112677
|
+
status = body.status ?? "none";
|
|
112678
|
+
if (status !== "pending") break;
|
|
112679
|
+
await new Promise((r) => setTimeout(r, HANDOFF_POLL_MS));
|
|
112680
|
+
}
|
|
112681
|
+
if (status === "pending") status = "timeout";
|
|
112682
|
+
let resumed = false;
|
|
112683
|
+
if (status === "completed" || status === "none") {
|
|
112684
|
+
try {
|
|
112685
|
+
const browser = await deps.connect(e.cdpEndpoint);
|
|
112686
|
+
const pages = await browser.pages();
|
|
112687
|
+
const page = pages.find((p) => !p.isClosed()) ?? await browser.newPage();
|
|
112688
|
+
e.browser = browser;
|
|
112689
|
+
e.page = page;
|
|
112690
|
+
resumed = true;
|
|
112691
|
+
} catch (err) {
|
|
112692
|
+
return text({
|
|
112693
|
+
status,
|
|
112694
|
+
resumed: false,
|
|
112695
|
+
note: "the sign-in finished but the session could not be re-attached: " + redact(err instanceof Error ? err.message : String(err))
|
|
112696
|
+
});
|
|
112697
|
+
}
|
|
112698
|
+
}
|
|
112699
|
+
return text({
|
|
112700
|
+
status,
|
|
112701
|
+
resumed,
|
|
112702
|
+
...resumed ? { url: e.page.url() } : {}
|
|
112703
|
+
});
|
|
112704
|
+
}
|
|
112705
|
+
},
|
|
112511
112706
|
solari_browser_navigate: {
|
|
112512
112707
|
description: "Navigate the browser session to a URL. Returns final url, title and HTTP status.",
|
|
112513
112708
|
inputSchema: { sessionId: external_exports.string(), url: external_exports.string() },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solarisdk/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Model Context Protocol server for the Solari cloud browser, sandboxes + desktops \u2014 drive them from Claude Desktop/Cowork, Claude Code, Cursor, etc.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|