gologin-agent-browser-cli 0.2.0

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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.js +336 -0
  5. package/dist/commands/back.d.ts +2 -0
  6. package/dist/commands/back.js +12 -0
  7. package/dist/commands/check.d.ts +2 -0
  8. package/dist/commands/check.js +17 -0
  9. package/dist/commands/click.d.ts +2 -0
  10. package/dist/commands/click.js +17 -0
  11. package/dist/commands/close.d.ts +2 -0
  12. package/dist/commands/close.js +12 -0
  13. package/dist/commands/cookies.d.ts +2 -0
  14. package/dist/commands/cookies.js +23 -0
  15. package/dist/commands/cookiesClear.d.ts +2 -0
  16. package/dist/commands/cookiesClear.js +12 -0
  17. package/dist/commands/cookiesImport.d.ts +2 -0
  18. package/dist/commands/cookiesImport.js +18 -0
  19. package/dist/commands/current.d.ts +2 -0
  20. package/dist/commands/current.js +9 -0
  21. package/dist/commands/dblclick.d.ts +2 -0
  22. package/dist/commands/dblclick.js +17 -0
  23. package/dist/commands/doctor.d.ts +2 -0
  24. package/dist/commands/doctor.js +31 -0
  25. package/dist/commands/eval.d.ts +2 -0
  26. package/dist/commands/eval.js +30 -0
  27. package/dist/commands/fill.d.ts +2 -0
  28. package/dist/commands/fill.js +18 -0
  29. package/dist/commands/find.d.ts +2 -0
  30. package/dist/commands/find.js +86 -0
  31. package/dist/commands/focus.d.ts +2 -0
  32. package/dist/commands/focus.js +17 -0
  33. package/dist/commands/forward.d.ts +2 -0
  34. package/dist/commands/forward.js +12 -0
  35. package/dist/commands/get.d.ts +2 -0
  36. package/dist/commands/get.js +19 -0
  37. package/dist/commands/hover.d.ts +2 -0
  38. package/dist/commands/hover.js +17 -0
  39. package/dist/commands/open.d.ts +2 -0
  40. package/dist/commands/open.js +67 -0
  41. package/dist/commands/pdf.d.ts +2 -0
  42. package/dist/commands/pdf.js +18 -0
  43. package/dist/commands/press.d.ts +2 -0
  44. package/dist/commands/press.js +19 -0
  45. package/dist/commands/reload.d.ts +2 -0
  46. package/dist/commands/reload.js +12 -0
  47. package/dist/commands/screenshot.d.ts +2 -0
  48. package/dist/commands/screenshot.js +22 -0
  49. package/dist/commands/scroll.d.ts +2 -0
  50. package/dist/commands/scroll.js +25 -0
  51. package/dist/commands/scrollIntoView.d.ts +2 -0
  52. package/dist/commands/scrollIntoView.js +17 -0
  53. package/dist/commands/select.d.ts +2 -0
  54. package/dist/commands/select.js +18 -0
  55. package/dist/commands/sessions.d.ts +2 -0
  56. package/dist/commands/sessions.js +15 -0
  57. package/dist/commands/shared.d.ts +7 -0
  58. package/dist/commands/shared.js +51 -0
  59. package/dist/commands/snapshot.d.ts +2 -0
  60. package/dist/commands/snapshot.js +16 -0
  61. package/dist/commands/storageClear.d.ts +2 -0
  62. package/dist/commands/storageClear.js +13 -0
  63. package/dist/commands/storageExport.d.ts +2 -0
  64. package/dist/commands/storageExport.js +24 -0
  65. package/dist/commands/storageImport.d.ts +2 -0
  66. package/dist/commands/storageImport.js +20 -0
  67. package/dist/commands/tabClose.d.ts +2 -0
  68. package/dist/commands/tabClose.js +21 -0
  69. package/dist/commands/tabFocus.d.ts +2 -0
  70. package/dist/commands/tabFocus.js +21 -0
  71. package/dist/commands/tabOpen.d.ts +2 -0
  72. package/dist/commands/tabOpen.js +13 -0
  73. package/dist/commands/tabs.d.ts +2 -0
  74. package/dist/commands/tabs.js +17 -0
  75. package/dist/commands/type.d.ts +2 -0
  76. package/dist/commands/type.js +18 -0
  77. package/dist/commands/uncheck.d.ts +2 -0
  78. package/dist/commands/uncheck.js +17 -0
  79. package/dist/commands/upload.d.ts +2 -0
  80. package/dist/commands/upload.js +18 -0
  81. package/dist/commands/wait.d.ts +2 -0
  82. package/dist/commands/wait.js +41 -0
  83. package/dist/daemon/browser.d.ts +67 -0
  84. package/dist/daemon/browser.js +818 -0
  85. package/dist/daemon/refStore.d.ts +9 -0
  86. package/dist/daemon/refStore.js +26 -0
  87. package/dist/daemon/server.d.ts +1 -0
  88. package/dist/daemon/server.js +313 -0
  89. package/dist/daemon/sessionManager.d.ts +66 -0
  90. package/dist/daemon/sessionManager.js +684 -0
  91. package/dist/daemon/snapshot.d.ts +8 -0
  92. package/dist/daemon/snapshot.js +285 -0
  93. package/dist/lib/config.d.ts +3 -0
  94. package/dist/lib/config.js +58 -0
  95. package/dist/lib/daemon.d.ts +9 -0
  96. package/dist/lib/daemon.js +197 -0
  97. package/dist/lib/errors.d.ts +12 -0
  98. package/dist/lib/errors.js +63 -0
  99. package/dist/lib/types.d.ts +407 -0
  100. package/dist/lib/types.js +2 -0
  101. package/dist/lib/utils.d.ts +27 -0
  102. package/dist/lib/utils.js +165 -0
  103. package/examples/agent-flow.sh +19 -0
  104. package/package.json +61 -0
@@ -0,0 +1,818 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildConnectUrl = buildConnectUrl;
4
+ exports.toPlaywrightCdpUrl = toPlaywrightCdpUrl;
5
+ exports.createQuickProfile = createQuickProfile;
6
+ exports.createManagedProfile = createManagedProfile;
7
+ exports.deleteProfile = deleteProfile;
8
+ exports.getCloudLiveViewUrl = getCloudLiveViewUrl;
9
+ exports.getCloudProfileProxy = getCloudProfileProxy;
10
+ exports.connectToBrowser = connectToBrowser;
11
+ exports.navigatePage = navigatePage;
12
+ exports.resolveDescriptorLocator = resolveDescriptorLocator;
13
+ exports.resolveSelectorLocator = resolveSelectorLocator;
14
+ exports.resolveSemanticLocator = resolveSemanticLocator;
15
+ exports.performLocatorAction = performLocatorAction;
16
+ exports.actionMutatesSnapshot = actionMutatesSnapshot;
17
+ exports.pressKey = pressKey;
18
+ exports.waitForTarget = waitForTarget;
19
+ exports.waitForPageText = waitForPageText;
20
+ exports.waitForPageUrl = waitForPageUrl;
21
+ exports.waitForPageLoad = waitForPageLoad;
22
+ exports.readLocatorValue = readLocatorValue;
23
+ exports.scrollPage = scrollPage;
24
+ exports.scrollElement = scrollElement;
25
+ exports.scrollLocatorIntoView = scrollLocatorIntoView;
26
+ exports.uploadFiles = uploadFiles;
27
+ exports.savePdf = savePdf;
28
+ exports.captureScreenshot = captureScreenshot;
29
+ exports.listTabs = listTabs;
30
+ exports.openTab = openTab;
31
+ exports.focusTab = focusTab;
32
+ exports.closeTab = closeTab;
33
+ exports.navigateHistory = navigateHistory;
34
+ exports.readCookies = readCookies;
35
+ exports.importCookies = importCookies;
36
+ exports.clearCookies = clearCookies;
37
+ exports.exportStorageState = exportStorageState;
38
+ exports.importStorageState = importStorageState;
39
+ exports.clearStorageState = clearStorageState;
40
+ exports.evaluateExpression = evaluateExpression;
41
+ exports.annotatePageWithRefs = annotatePageWithRefs;
42
+ exports.clearPageAnnotations = clearPageAnnotations;
43
+ exports.closeSessionHandles = closeSessionHandles;
44
+ const playwright_1 = require("playwright");
45
+ const errors_1 = require("../lib/errors");
46
+ function asAriaRole(role) {
47
+ return role && role !== "paragraph" ? role : undefined;
48
+ }
49
+ const GOLOGIN_API_BASE = "https://api.gologin.com";
50
+ function trimToUndefined(value) {
51
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
52
+ }
53
+ function normalizeProxySummary(proxy, fallback) {
54
+ if (!proxy || typeof proxy !== "object") {
55
+ return fallback
56
+ ? {
57
+ mode: fallback.mode,
58
+ country: fallback.country,
59
+ host: fallback.host,
60
+ port: fallback.port,
61
+ username: fallback.username
62
+ }
63
+ : undefined;
64
+ }
65
+ const value = proxy;
66
+ const mode = trimToUndefined(value.mode) ?? fallback?.mode;
67
+ if (!mode) {
68
+ return undefined;
69
+ }
70
+ const host = trimToUndefined(value.host) ?? fallback?.host;
71
+ const country = trimToUndefined(value.autoProxyRegion) ?? trimToUndefined(value.country) ?? fallback?.country;
72
+ const username = trimToUndefined(value.username) ?? fallback?.username;
73
+ const port = typeof value.port === "number"
74
+ ? value.port
75
+ : typeof fallback?.port === "number"
76
+ ? fallback.port
77
+ : undefined;
78
+ return {
79
+ mode,
80
+ country,
81
+ host,
82
+ port,
83
+ username
84
+ };
85
+ }
86
+ function buildCloudProfileBody(sessionId, proxy) {
87
+ const body = {
88
+ name: `gologin-agent-browser-${sessionId}`,
89
+ os: "lin"
90
+ };
91
+ if (!proxy) {
92
+ return body;
93
+ }
94
+ if (proxy.mode === "gologin") {
95
+ body.proxy = {
96
+ mode: "gologin",
97
+ autoProxyRegion: (proxy.country ?? "us").toLowerCase()
98
+ };
99
+ return body;
100
+ }
101
+ body.proxy = {
102
+ mode: proxy.mode,
103
+ host: proxy.host,
104
+ port: proxy.port,
105
+ username: proxy.username ?? "",
106
+ password: proxy.password ?? ""
107
+ };
108
+ return body;
109
+ }
110
+ async function pickContextAndPage(browser) {
111
+ const existingContext = browser.contexts()[0] ?? (await browser.newContext());
112
+ const page = existingContext.pages()[0] ?? (await existingContext.newPage());
113
+ return { context: existingContext, page };
114
+ }
115
+ async function ensureLocatorReady(locator, description, timeoutMs) {
116
+ const count = await locator.count();
117
+ if (count === 0) {
118
+ throw new errors_1.AppError("BAD_REQUEST", `${description} did not match any element`, 404);
119
+ }
120
+ const resolved = locator.first();
121
+ await resolved.waitFor({ state: "visible", timeout: timeoutMs });
122
+ return resolved;
123
+ }
124
+ function buildConnectUrl(config, token, profileId) {
125
+ const url = new URL(config.connectBase);
126
+ url.searchParams.set("token", token);
127
+ if (profileId) {
128
+ url.searchParams.set("profile", profileId);
129
+ }
130
+ return url.toString();
131
+ }
132
+ function toPlaywrightCdpUrl(connectUrl) {
133
+ const url = new URL(connectUrl);
134
+ if (url.protocol === "https:") {
135
+ url.protocol = "wss:";
136
+ }
137
+ else if (url.protocol === "http:") {
138
+ url.protocol = "ws:";
139
+ }
140
+ return url.toString();
141
+ }
142
+ function extractProfileId(payload) {
143
+ if (typeof payload === "string" && payload.length > 0) {
144
+ return payload;
145
+ }
146
+ if (!payload || typeof payload !== "object") {
147
+ return undefined;
148
+ }
149
+ const value = payload;
150
+ if (typeof value.id === "string" && value.id.length > 0) {
151
+ return value.id;
152
+ }
153
+ if (typeof value._id === "string" && value._id.length > 0) {
154
+ return value._id;
155
+ }
156
+ return undefined;
157
+ }
158
+ async function createQuickProfile(token, sessionId) {
159
+ const response = await fetch(`${GOLOGIN_API_BASE}/browser/quick`, {
160
+ method: "POST",
161
+ headers: {
162
+ Authorization: `Bearer ${token}`,
163
+ "Content-Type": "application/json"
164
+ },
165
+ body: JSON.stringify({
166
+ name: `gologin-agent-browser-${sessionId}`,
167
+ os: "lin"
168
+ })
169
+ });
170
+ if (!response.ok) {
171
+ throw new errors_1.AppError("BROWSER_CONNECTION_FAILED", `Failed to create Gologin profile: ${response.status}`, 502);
172
+ }
173
+ const payload = (await response.json());
174
+ const profileId = extractProfileId(payload);
175
+ if (!profileId) {
176
+ throw new errors_1.AppError("BROWSER_CONNECTION_FAILED", "Gologin profile creation returned no profile id", 502);
177
+ }
178
+ return profileId;
179
+ }
180
+ async function createManagedProfile(token, sessionId, proxy) {
181
+ if (!proxy) {
182
+ return {
183
+ profileId: await createQuickProfile(token, sessionId)
184
+ };
185
+ }
186
+ const response = await fetch(`${GOLOGIN_API_BASE}/browser/custom`, {
187
+ method: "POST",
188
+ headers: {
189
+ Authorization: `Bearer ${token}`,
190
+ "Content-Type": "application/json"
191
+ },
192
+ body: JSON.stringify(buildCloudProfileBody(sessionId, proxy))
193
+ });
194
+ if (!response.ok) {
195
+ throw new errors_1.AppError("BROWSER_CONNECTION_FAILED", `Failed to create Gologin cloud profile: ${response.status}`, 502);
196
+ }
197
+ const payload = (await response.json());
198
+ const profileId = extractProfileId(payload);
199
+ if (!profileId) {
200
+ throw new errors_1.AppError("BROWSER_CONNECTION_FAILED", "Gologin profile creation returned no profile id", 502);
201
+ }
202
+ let proxySummary;
203
+ if (payload && typeof payload === "object") {
204
+ proxySummary = normalizeProxySummary(payload.proxy, proxy);
205
+ }
206
+ return {
207
+ profileId,
208
+ proxy: proxySummary ?? normalizeProxySummary(undefined, proxy)
209
+ };
210
+ }
211
+ async function deleteProfile(token, profileId) {
212
+ const response = await fetch(`${GOLOGIN_API_BASE}/browser/${profileId}`, {
213
+ method: "DELETE",
214
+ headers: {
215
+ Authorization: `Bearer ${token}`
216
+ }
217
+ });
218
+ if (!response.ok) {
219
+ throw new errors_1.AppError("BROWSER_CONNECTION_FAILED", `Failed to delete Gologin profile ${profileId}`, 502, {
220
+ profileId
221
+ });
222
+ }
223
+ }
224
+ async function getCloudLiveViewUrl(token, profileId) {
225
+ const response = await fetch(`${GOLOGIN_API_BASE}/browser/${profileId}/web`, {
226
+ method: "POST",
227
+ headers: {
228
+ Authorization: `Bearer ${token}`,
229
+ "Content-Type": "application/json"
230
+ },
231
+ body: JSON.stringify({})
232
+ });
233
+ if (!response.ok) {
234
+ return undefined;
235
+ }
236
+ const payload = (await response.json());
237
+ return trimToUndefined(payload.remoteOrbitaUrl);
238
+ }
239
+ async function getCloudProfileProxy(token, profileId) {
240
+ const response = await fetch(`${GOLOGIN_API_BASE}/browser/${profileId}`, {
241
+ headers: {
242
+ Authorization: `Bearer ${token}`
243
+ }
244
+ });
245
+ if (!response.ok) {
246
+ return undefined;
247
+ }
248
+ const payload = (await response.json());
249
+ return normalizeProxySummary(payload.proxy);
250
+ }
251
+ async function connectToBrowser(config, token, profileId) {
252
+ const connectUrl = toPlaywrightCdpUrl(buildConnectUrl(config, token, profileId));
253
+ try {
254
+ const browser = await playwright_1.chromium.connectOverCDP(connectUrl);
255
+ const { context, page } = await pickContextAndPage(browser);
256
+ return { browser, context, page, connectUrl };
257
+ }
258
+ catch (error) {
259
+ throw new errors_1.AppError("BROWSER_CONNECTION_FAILED", error instanceof Error ? error.message : String(error), 502, {
260
+ profileId
261
+ });
262
+ }
263
+ }
264
+ async function navigatePage(page, url, timeoutMs) {
265
+ try {
266
+ await page.goto(url, {
267
+ waitUntil: "domcontentloaded",
268
+ timeout: timeoutMs
269
+ });
270
+ await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
271
+ return page.url();
272
+ }
273
+ catch (error) {
274
+ throw new errors_1.AppError("NAVIGATION_TIMEOUT", error instanceof Error ? error.message : `Failed to navigate to ${url}`, 504, {
275
+ url
276
+ });
277
+ }
278
+ }
279
+ async function locatorFromRole(page, descriptor) {
280
+ const role = asAriaRole(descriptor.role);
281
+ const name = descriptor.accessibleName ?? descriptor.text;
282
+ if (!role || !name) {
283
+ return undefined;
284
+ }
285
+ const locator = page.getByRole(role, { name });
286
+ const count = await locator.count();
287
+ if (count === 0) {
288
+ return undefined;
289
+ }
290
+ if (count === 1) {
291
+ return locator.first();
292
+ }
293
+ if (descriptor.nth !== undefined && descriptor.nth < count) {
294
+ return locator.nth(descriptor.nth);
295
+ }
296
+ return undefined;
297
+ }
298
+ async function locatorFromLabel(page, descriptor) {
299
+ const label = descriptor.ariaLabel ?? descriptor.accessibleName;
300
+ if (!label) {
301
+ return undefined;
302
+ }
303
+ const locator = page.getByLabel(label);
304
+ const count = await locator.count();
305
+ if (count === 0) {
306
+ return undefined;
307
+ }
308
+ if (count === 1) {
309
+ return locator.first();
310
+ }
311
+ if (descriptor.nth !== undefined && descriptor.nth < count) {
312
+ return locator.nth(descriptor.nth);
313
+ }
314
+ return undefined;
315
+ }
316
+ async function locatorFromPlaceholder(page, descriptor) {
317
+ if (!descriptor.placeholder) {
318
+ return undefined;
319
+ }
320
+ const locator = page.getByPlaceholder(descriptor.placeholder);
321
+ const count = await locator.count();
322
+ if (count === 0) {
323
+ return undefined;
324
+ }
325
+ if (count === 1) {
326
+ return locator.first();
327
+ }
328
+ if (descriptor.nth !== undefined && descriptor.nth < count) {
329
+ return locator.nth(descriptor.nth);
330
+ }
331
+ return undefined;
332
+ }
333
+ async function locatorFromTagAndText(page, descriptor) {
334
+ const text = descriptor.text ?? descriptor.accessibleName;
335
+ if (!text) {
336
+ return undefined;
337
+ }
338
+ const locator = page.locator(descriptor.tag).filter({ hasText: text });
339
+ const count = await locator.count();
340
+ if (count === 0) {
341
+ return undefined;
342
+ }
343
+ if (count === 1) {
344
+ return locator.first();
345
+ }
346
+ if (descriptor.nth !== undefined && descriptor.nth < count) {
347
+ return locator.nth(descriptor.nth);
348
+ }
349
+ return undefined;
350
+ }
351
+ async function locatorFromFallbackTag(page, descriptor) {
352
+ const locator = page.locator(descriptor.tag);
353
+ const count = await locator.count();
354
+ if (count === 0) {
355
+ return undefined;
356
+ }
357
+ if (count === 1) {
358
+ return locator.first();
359
+ }
360
+ if (descriptor.nth !== undefined && descriptor.nth < count) {
361
+ return locator.nth(descriptor.nth);
362
+ }
363
+ return undefined;
364
+ }
365
+ async function resolveDescriptorLocator(page, descriptor) {
366
+ const strategies = [
367
+ locatorFromRole,
368
+ locatorFromLabel,
369
+ locatorFromPlaceholder,
370
+ locatorFromTagAndText,
371
+ locatorFromFallbackTag
372
+ ];
373
+ for (const strategy of strategies) {
374
+ const locator = await strategy(page, descriptor);
375
+ if (locator) {
376
+ return locator;
377
+ }
378
+ }
379
+ throw new errors_1.AppError("REF_NOT_FOUND", `ref ${descriptor.ref} is stale or unavailable on the current page; run snapshot again`, 404, {
380
+ ref: descriptor.ref
381
+ });
382
+ }
383
+ async function resolveSelectorLocator(page, selector, timeoutMs) {
384
+ return ensureLocatorReady(page.locator(selector), `selector ${selector}`, timeoutMs);
385
+ }
386
+ async function resolveSemanticLocator(page, query, timeoutMs) {
387
+ switch (query.strategy) {
388
+ case "role":
389
+ if (!query.role) {
390
+ throw new errors_1.AppError("BAD_REQUEST", "find role requires a role value", 400);
391
+ }
392
+ return ensureLocatorReady(query.name
393
+ ? page.getByRole(query.role, { name: query.name, exact: query.exact })
394
+ : page.getByRole(query.role), `role ${query.role}`, timeoutMs);
395
+ case "text":
396
+ if (!query.text) {
397
+ throw new errors_1.AppError("BAD_REQUEST", "find text requires a text value", 400);
398
+ }
399
+ return ensureLocatorReady(page.getByText(query.text, { exact: query.exact }), `text ${query.text}`, timeoutMs);
400
+ case "label":
401
+ if (!query.label) {
402
+ throw new errors_1.AppError("BAD_REQUEST", "find label requires a label value", 400);
403
+ }
404
+ return ensureLocatorReady(page.getByLabel(query.label, { exact: query.exact }), `label ${query.label}`, timeoutMs);
405
+ case "placeholder":
406
+ if (!query.placeholder) {
407
+ throw new errors_1.AppError("BAD_REQUEST", "find placeholder requires a placeholder value", 400);
408
+ }
409
+ return ensureLocatorReady(page.getByPlaceholder(query.placeholder, { exact: query.exact }), `placeholder ${query.placeholder}`, timeoutMs);
410
+ case "first":
411
+ if (!query.selector) {
412
+ throw new errors_1.AppError("BAD_REQUEST", "find first requires a selector value", 400);
413
+ }
414
+ return ensureLocatorReady(page.locator(query.selector).first(), `first ${query.selector}`, timeoutMs);
415
+ case "last":
416
+ if (!query.selector) {
417
+ throw new errors_1.AppError("BAD_REQUEST", "find last requires a selector value", 400);
418
+ }
419
+ return ensureLocatorReady(page.locator(query.selector).last(), `last ${query.selector}`, timeoutMs);
420
+ case "nth":
421
+ if (!query.selector || query.nth === undefined) {
422
+ throw new errors_1.AppError("BAD_REQUEST", "find nth requires both an index and selector", 400);
423
+ }
424
+ return ensureLocatorReady(page.locator(query.selector).nth(query.nth), `nth ${query.nth} ${query.selector}`, timeoutMs);
425
+ }
426
+ }
427
+ async function performLocatorAction(page, locator, action, timeoutMs, value) {
428
+ switch (action) {
429
+ case "click":
430
+ await locator.click({ timeout: timeoutMs });
431
+ await page.waitForLoadState("domcontentloaded", { timeout: 5_000 }).catch(() => undefined);
432
+ return undefined;
433
+ case "fill":
434
+ await locator.fill(value ?? "", { timeout: timeoutMs });
435
+ return undefined;
436
+ case "type":
437
+ await locator.pressSequentially(value ?? "", { timeout: timeoutMs });
438
+ return undefined;
439
+ case "hover":
440
+ await locator.hover({ timeout: timeoutMs });
441
+ return undefined;
442
+ case "focus":
443
+ await locator.focus({ timeout: timeoutMs });
444
+ return undefined;
445
+ case "dblclick":
446
+ await locator.dblclick({ timeout: timeoutMs });
447
+ await page.waitForLoadState("domcontentloaded", { timeout: 5_000 }).catch(() => undefined);
448
+ return undefined;
449
+ case "select":
450
+ await locator.selectOption(value ?? "", { timeout: timeoutMs });
451
+ return undefined;
452
+ case "check":
453
+ await locator.check({ timeout: timeoutMs });
454
+ return undefined;
455
+ case "uncheck":
456
+ await locator.uncheck({ timeout: timeoutMs });
457
+ return undefined;
458
+ case "text":
459
+ return await locator.innerText({ timeout: timeoutMs });
460
+ }
461
+ }
462
+ function actionMutatesSnapshot(action) {
463
+ return action !== "text";
464
+ }
465
+ async function pressKey(page, key, timeoutMs) {
466
+ void timeoutMs;
467
+ await page.keyboard.press(key);
468
+ }
469
+ async function waitForTarget(locator, timeoutMs) {
470
+ await locator.waitFor({ state: "visible", timeout: timeoutMs });
471
+ }
472
+ async function waitForPageText(page, text, timeoutMs, exact = false) {
473
+ await page.getByText(text, { exact }).first().waitFor({ state: "visible", timeout: timeoutMs });
474
+ }
475
+ async function waitForPageUrl(page, urlPattern, timeoutMs) {
476
+ await page.waitForURL(urlPattern, { timeout: timeoutMs });
477
+ }
478
+ async function waitForPageLoad(page, state, timeoutMs) {
479
+ await page.waitForLoadState(state, { timeout: timeoutMs });
480
+ }
481
+ async function readLocatorValue(locator, kind, timeoutMs) {
482
+ switch (kind) {
483
+ case "text":
484
+ return await locator.innerText({ timeout: timeoutMs });
485
+ case "value":
486
+ return await locator.inputValue({ timeout: timeoutMs });
487
+ case "html":
488
+ return await locator.innerHTML({ timeout: timeoutMs });
489
+ default:
490
+ throw new errors_1.AppError("BAD_REQUEST", `Unsupported get kind for element target: ${kind}`, 400);
491
+ }
492
+ }
493
+ function scrollDelta(direction, pixels) {
494
+ switch (direction) {
495
+ case "up":
496
+ return { dx: 0, dy: -pixels };
497
+ case "down":
498
+ return { dx: 0, dy: pixels };
499
+ case "left":
500
+ return { dx: -pixels, dy: 0 };
501
+ case "right":
502
+ return { dx: pixels, dy: 0 };
503
+ }
504
+ }
505
+ async function scrollPage(page, direction, pixels) {
506
+ const { dx, dy } = scrollDelta(direction, pixels);
507
+ await page.mouse.wheel(dx, dy);
508
+ }
509
+ async function scrollElement(locator, direction, pixels) {
510
+ const { dx, dy } = scrollDelta(direction, pixels);
511
+ await locator.evaluate((element, delta) => {
512
+ if (element instanceof HTMLElement) {
513
+ element.scrollBy(delta.dx, delta.dy);
514
+ }
515
+ }, { dx, dy });
516
+ }
517
+ async function scrollLocatorIntoView(locator, timeoutMs) {
518
+ await locator.scrollIntoViewIfNeeded({ timeout: timeoutMs });
519
+ }
520
+ async function uploadFiles(locator, files, timeoutMs) {
521
+ await locator.setInputFiles(files, { timeout: timeoutMs });
522
+ }
523
+ async function savePdf(page, targetPath) {
524
+ await page.pdf({
525
+ path: targetPath,
526
+ format: "A4",
527
+ printBackground: true
528
+ });
529
+ }
530
+ async function captureScreenshot(page, targetPath, timeoutMs) {
531
+ await page.screenshot({
532
+ path: targetPath,
533
+ fullPage: true,
534
+ timeout: timeoutMs
535
+ });
536
+ }
537
+ function resolvePageOrigin(page) {
538
+ const currentUrl = page.url();
539
+ try {
540
+ const origin = new URL(currentUrl).origin;
541
+ if (!origin || origin === "null") {
542
+ throw new Error("non-origin URL");
543
+ }
544
+ return origin;
545
+ }
546
+ catch {
547
+ throw new errors_1.AppError("BAD_REQUEST", `Current tab URL ${currentUrl || "about:blank"} does not have a usable origin for storage operations`, 400);
548
+ }
549
+ }
550
+ function normalizeStorageScope(scope) {
551
+ return scope ?? "both";
552
+ }
553
+ function hasLocalScope(scope) {
554
+ return scope === "local" || scope === "both";
555
+ }
556
+ function hasSessionScope(scope) {
557
+ return scope === "session" || scope === "both";
558
+ }
559
+ async function listTabs(session) {
560
+ const pages = session.context.pages();
561
+ return Promise.all(pages.map(async (page, index) => ({
562
+ index: index + 1,
563
+ url: page.url(),
564
+ title: await page.title().catch(() => undefined),
565
+ active: page === session.page
566
+ })));
567
+ }
568
+ function requireTabIndex(session, index) {
569
+ if (!Number.isInteger(index) || index <= 0) {
570
+ throw new errors_1.AppError("BAD_REQUEST", "tab index must be a positive integer", 400);
571
+ }
572
+ const page = session.context.pages()[index - 1];
573
+ if (!page) {
574
+ throw new errors_1.AppError("BAD_REQUEST", `tab ${index} does not exist`, 404, { tabIndex: index });
575
+ }
576
+ return page;
577
+ }
578
+ async function openTab(session, url, timeoutMs) {
579
+ const page = await session.context.newPage();
580
+ if (url) {
581
+ await navigatePage(page, url, timeoutMs);
582
+ }
583
+ await page.bringToFront().catch(() => undefined);
584
+ const tabIndex = session.context.pages().indexOf(page) + 1;
585
+ return {
586
+ page,
587
+ tabIndex
588
+ };
589
+ }
590
+ async function focusTab(session, index) {
591
+ const page = requireTabIndex(session, index);
592
+ await page.bringToFront().catch(() => undefined);
593
+ return {
594
+ page,
595
+ tabIndex: index
596
+ };
597
+ }
598
+ async function closeTab(session, index) {
599
+ const pagesBefore = session.context.pages();
600
+ const currentIndex = pagesBefore.indexOf(session.page) + 1;
601
+ const resolvedIndex = index ?? currentIndex;
602
+ const target = requireTabIndex(session, resolvedIndex);
603
+ if (pagesBefore.length === 1) {
604
+ const replacement = await session.context.newPage();
605
+ await replacement.goto("about:blank").catch(() => undefined);
606
+ await target.close({ runBeforeUnload: false });
607
+ await replacement.bringToFront().catch(() => undefined);
608
+ return {
609
+ page: replacement,
610
+ closedTabIndex: resolvedIndex,
611
+ activeTabIndex: 1
612
+ };
613
+ }
614
+ await target.close({ runBeforeUnload: false });
615
+ const pagesAfter = session.context.pages();
616
+ const nextIndex = Math.min(resolvedIndex, pagesAfter.length);
617
+ const page = pagesAfter[nextIndex - 1];
618
+ if (!page) {
619
+ throw new errors_1.AppError("INTERNAL_ERROR", "No tab remained after close", 500);
620
+ }
621
+ await page.bringToFront().catch(() => undefined);
622
+ return {
623
+ page,
624
+ closedTabIndex: resolvedIndex,
625
+ activeTabIndex: nextIndex
626
+ };
627
+ }
628
+ async function navigateHistory(page, direction, timeoutMs) {
629
+ try {
630
+ if (direction === "back") {
631
+ await page.goBack({ waitUntil: "domcontentloaded", timeout: timeoutMs }).catch(() => null);
632
+ }
633
+ else if (direction === "forward") {
634
+ await page.goForward({ waitUntil: "domcontentloaded", timeout: timeoutMs }).catch(() => null);
635
+ }
636
+ else {
637
+ await page.reload({ waitUntil: "domcontentloaded", timeout: timeoutMs });
638
+ }
639
+ await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
640
+ return page.url();
641
+ }
642
+ catch (error) {
643
+ throw new errors_1.AppError("NAVIGATION_TIMEOUT", error instanceof Error ? error.message : `Failed to navigate ${direction}`, 504, { direction });
644
+ }
645
+ }
646
+ async function readCookies(session) {
647
+ const cookies = await session.context.cookies();
648
+ return cookies
649
+ .map((cookie) => ({
650
+ name: cookie.name,
651
+ value: cookie.value,
652
+ domain: cookie.domain,
653
+ path: cookie.path,
654
+ expires: cookie.expires,
655
+ httpOnly: cookie.httpOnly,
656
+ secure: cookie.secure,
657
+ sameSite: cookie.sameSite
658
+ }))
659
+ .sort((left, right) => `${left.domain}|${left.path}|${left.name}`.localeCompare(`${right.domain}|${right.path}|${right.name}`));
660
+ }
661
+ async function importCookies(session, cookies) {
662
+ await session.context.addCookies(cookies);
663
+ return cookies.length;
664
+ }
665
+ async function clearCookies(session) {
666
+ const existing = await session.context.cookies();
667
+ await session.context.clearCookies();
668
+ return existing.length;
669
+ }
670
+ async function exportStorageState(page, scope) {
671
+ const resolvedScope = normalizeStorageScope(scope);
672
+ const origin = resolvePageOrigin(page);
673
+ const state = await page.evaluate((requestedScope) => {
674
+ const localEntries = [];
675
+ const sessionEntries = [];
676
+ if (requestedScope === "local" || requestedScope === "both") {
677
+ for (let index = 0; index < localStorage.length; index += 1) {
678
+ const key = localStorage.key(index);
679
+ if (key !== null) {
680
+ localEntries.push([key, localStorage.getItem(key) ?? ""]);
681
+ }
682
+ }
683
+ localEntries.sort(([left], [right]) => left.localeCompare(right));
684
+ }
685
+ if (requestedScope === "session" || requestedScope === "both") {
686
+ for (let index = 0; index < sessionStorage.length; index += 1) {
687
+ const key = sessionStorage.key(index);
688
+ if (key !== null) {
689
+ sessionEntries.push([key, sessionStorage.getItem(key) ?? ""]);
690
+ }
691
+ }
692
+ sessionEntries.sort(([left], [right]) => left.localeCompare(right));
693
+ }
694
+ return {
695
+ localStorage: Object.fromEntries(localEntries),
696
+ sessionStorage: Object.fromEntries(sessionEntries)
697
+ };
698
+ }, resolvedScope);
699
+ return {
700
+ origin,
701
+ localStorage: state.localStorage,
702
+ sessionStorage: state.sessionStorage
703
+ };
704
+ }
705
+ async function importStorageState(page, state, scope, clear = false) {
706
+ const resolvedScope = normalizeStorageScope(scope);
707
+ const origin = resolvePageOrigin(page);
708
+ if (origin !== state.origin) {
709
+ throw new errors_1.AppError("BAD_REQUEST", `Storage state origin ${state.origin} does not match current tab origin ${origin}`, 400, {
710
+ expectedOrigin: origin,
711
+ actualOrigin: state.origin
712
+ });
713
+ }
714
+ const localStorageState = hasLocalScope(resolvedScope) ? state.localStorage : {};
715
+ const sessionStorageState = hasSessionScope(resolvedScope) ? state.sessionStorage : {};
716
+ await page.evaluate(({ incoming, requestedScope, shouldClear }) => {
717
+ if (shouldClear) {
718
+ if (requestedScope === "local" || requestedScope === "both") {
719
+ localStorage.clear();
720
+ }
721
+ if (requestedScope === "session" || requestedScope === "both") {
722
+ sessionStorage.clear();
723
+ }
724
+ }
725
+ if (requestedScope === "local" || requestedScope === "both") {
726
+ for (const [key, value] of Object.entries(incoming.localStorage)) {
727
+ localStorage.setItem(key, value);
728
+ }
729
+ }
730
+ if (requestedScope === "session" || requestedScope === "both") {
731
+ for (const [key, value] of Object.entries(incoming.sessionStorage)) {
732
+ sessionStorage.setItem(key, value);
733
+ }
734
+ }
735
+ }, {
736
+ incoming: {
737
+ localStorage: localStorageState,
738
+ sessionStorage: sessionStorageState
739
+ },
740
+ requestedScope: resolvedScope,
741
+ shouldClear: clear
742
+ });
743
+ return {
744
+ origin,
745
+ localKeys: Object.keys(localStorageState).length,
746
+ sessionKeys: Object.keys(sessionStorageState).length
747
+ };
748
+ }
749
+ async function clearStorageState(page, scope) {
750
+ const resolvedScope = normalizeStorageScope(scope);
751
+ const origin = resolvePageOrigin(page);
752
+ await page.evaluate((requestedScope) => {
753
+ if (requestedScope === "local" || requestedScope === "both") {
754
+ localStorage.clear();
755
+ }
756
+ if (requestedScope === "session" || requestedScope === "both") {
757
+ sessionStorage.clear();
758
+ }
759
+ }, resolvedScope);
760
+ return {
761
+ origin,
762
+ scope: resolvedScope
763
+ };
764
+ }
765
+ async function evaluateExpression(page, expression) {
766
+ try {
767
+ return await page.evaluate((userExpression) => {
768
+ const fn = new Function(`return (${userExpression});`);
769
+ return fn();
770
+ }, expression);
771
+ }
772
+ catch (error) {
773
+ throw new errors_1.AppError("BAD_REQUEST", error instanceof Error ? error.message : String(error), 400, {
774
+ expression
775
+ });
776
+ }
777
+ }
778
+ async function annotatePageWithRefs(page, refs) {
779
+ await page.evaluate((labels) => {
780
+ document.getElementById("__gologin-agent-browser-annotations")?.remove();
781
+ const root = document.createElement("div");
782
+ root.id = "__gologin-agent-browser-annotations";
783
+ root.style.position = "absolute";
784
+ root.style.left = "0";
785
+ root.style.top = "0";
786
+ root.style.width = "100%";
787
+ root.style.height = "100%";
788
+ root.style.pointerEvents = "none";
789
+ root.style.zIndex = "2147483647";
790
+ for (const label of labels) {
791
+ const node = document.createElement("div");
792
+ node.textContent = label.ref;
793
+ node.style.position = "absolute";
794
+ node.style.left = `${Math.max(0, label.x)}px`;
795
+ node.style.top = `${Math.max(0, label.y)}px`;
796
+ node.style.padding = "2px 6px";
797
+ node.style.borderRadius = "999px";
798
+ node.style.background = "#ffcf33";
799
+ node.style.color = "#111";
800
+ node.style.font = "600 12px/1.2 Menlo, Monaco, monospace";
801
+ node.style.boxShadow = "0 1px 2px rgba(0,0,0,0.3)";
802
+ node.style.border = "1px solid rgba(0,0,0,0.35)";
803
+ root.appendChild(node);
804
+ }
805
+ document.body.appendChild(root);
806
+ }, refs);
807
+ }
808
+ async function clearPageAnnotations(page) {
809
+ await page.evaluate(() => {
810
+ document.getElementById("__gologin-agent-browser-annotations")?.remove();
811
+ });
812
+ }
813
+ async function closeSessionHandles(session) {
814
+ await session.browser.close().catch(async () => {
815
+ await session.page.close().catch(() => undefined);
816
+ await session.context.close().catch(() => undefined);
817
+ });
818
+ }