gologin-agent-browser-cli 0.2.0 → 0.2.1

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/dist/cli.js CHANGED
@@ -80,8 +80,8 @@ const commandUsage = {
80
80
  upload: "upload <target> <file...> [--session <sessionId>]",
81
81
  pdf: "pdf <path> [--session <sessionId>]",
82
82
  screenshot: "screenshot <path> [--annotate] [--press-escape] [--session <sessionId>]",
83
- close: "close [--session <sessionId>] (aliases: quit, exit)",
84
- sessions: "sessions",
83
+ close: "close [--session <sessionId>] [--all] (aliases: quit, exit)",
84
+ sessions: "sessions [--prune] [--older-than-ms <ms>]",
85
85
  current: "current"
86
86
  };
87
87
  function printUsage() {
@@ -1,10 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runCloseCommand = runCloseCommand;
4
+ const errors_1 = require("../lib/errors");
4
5
  const utils_1 = require("../lib/utils");
5
6
  async function runCloseCommand(context, argv) {
6
7
  const parsed = (0, utils_1.parseArgs)(argv);
8
+ const closeAll = (0, utils_1.getFlagBoolean)(parsed, "all");
7
9
  const sessionId = (0, utils_1.getFlagString)(parsed, "session");
10
+ if (closeAll) {
11
+ if (sessionId) {
12
+ throw new errors_1.AppError("BAD_REQUEST", "--all cannot be combined with --session", 400);
13
+ }
14
+ const response = await context.client.request("POST", "/sessions/close-all");
15
+ context.stdout.write(`closed ${response.closed} session(s)\n`);
16
+ return;
17
+ }
8
18
  const resolvedSessionId = sessionId ??
9
19
  (await context.client.request("GET", "/sessions/current")).sessionId;
10
20
  const response = await context.client.request("POST", `/sessions/${resolvedSessionId}/close`);
@@ -1,9 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runSessionsCommand = runSessionsCommand;
4
+ const errors_1 = require("../lib/errors");
4
5
  const utils_1 = require("../lib/utils");
6
+ function parseOlderThanMs(value) {
7
+ if (!value) {
8
+ return undefined;
9
+ }
10
+ const parsed = Number(value);
11
+ if (!Number.isInteger(parsed) || parsed < 0) {
12
+ throw new errors_1.AppError("BAD_REQUEST", "--older-than-ms must be a non-negative integer", 400);
13
+ }
14
+ return parsed;
15
+ }
5
16
  async function runSessionsCommand(context, argv) {
6
- (0, utils_1.parseArgs)(argv);
17
+ const parsed = (0, utils_1.parseArgs)(argv);
18
+ if ((0, utils_1.getFlagBoolean)(parsed, "prune")) {
19
+ const olderThanMs = parseOlderThanMs((0, utils_1.getFlagString)(parsed, "older-than-ms"));
20
+ const prune = await context.client.request("POST", "/sessions/prune", {
21
+ maxIdleMs: olderThanMs,
22
+ });
23
+ context.stderr.write(`pruned ${prune.closed} session(s) idle for at least ${prune.maxIdleMs}ms\n`);
24
+ }
7
25
  const response = await context.client.request("GET", "/sessions");
8
26
  if (response.sessions.length === 0) {
9
27
  context.stdout.write("no sessions\n");
@@ -46,6 +46,15 @@ async function handleRequest(request, response) {
46
46
  (0, utils_1.writeJsonResponse)(response, 200, await sessionManager.currentSession());
47
47
  return;
48
48
  }
49
+ if (method === "POST" && pathname === "/sessions/close-all") {
50
+ (0, utils_1.writeJsonResponse)(response, 200, await sessionManager.closeAll());
51
+ return;
52
+ }
53
+ if (method === "POST" && pathname === "/sessions/prune") {
54
+ const body = (await (0, utils_1.readJsonBody)(request));
55
+ (0, utils_1.writeJsonResponse)(response, 200, await sessionManager.pruneSessions(body?.maxIdleMs));
56
+ return;
57
+ }
49
58
  if (method === "POST" && pathname === "/sessions/open") {
50
59
  const body = (await (0, utils_1.readJsonBody)(request));
51
60
  (0, utils_1.writeJsonResponse)(response, 200, await sessionManager.open(body));
@@ -1,6 +1,8 @@
1
- import type { ActionResponse, AgentConfig, BrowserCookie, CheckResponse, ClickResponse, CloseSessionResponse, CookiesClearResponse, CookiesImportResponse, CookiesResponse, DoubleClickResponse, EvalResponse, FillResponse, FindRequest, FindResponse, FocusResponse, GetKind, GetResponse, HoverResponse, OpenSessionRequest, OpenSessionResponse, PdfResponse, PressResponse, ScrollDirection, ScrollIntoViewResponse, ScrollResponse, ScreenshotResponse, SelectResponse, SessionSummary, SessionsResponse, SnapshotResponse, StorageClearResponse, StorageExportResponse, StorageImportResponse, StorageScope, StorageState, TabCloseResponse, TabFocusResponse, TabOpenResponse, TabsResponse, TypeResponse, UncheckResponse, UploadResponse, WaitResponse } from "../lib/types";
1
+ import type { ActionResponse, AgentConfig, BrowserCookie, CheckResponse, ClickResponse, CloseSessionResponse, CloseAllSessionsResponse, CookiesClearResponse, CookiesImportResponse, CookiesResponse, DoubleClickResponse, EvalResponse, FillResponse, FindRequest, FindResponse, FocusResponse, GetKind, GetResponse, HoverResponse, OpenSessionRequest, OpenSessionResponse, PdfResponse, PressResponse, PruneSessionsResponse, ScrollDirection, ScrollIntoViewResponse, ScrollResponse, ScreenshotResponse, SelectResponse, SessionSummary, SessionsResponse, SnapshotResponse, StorageClearResponse, StorageExportResponse, StorageImportResponse, StorageScope, StorageState, TabCloseResponse, TabFocusResponse, TabOpenResponse, TabsResponse, TypeResponse, UncheckResponse, UploadResponse, WaitResponse } from "../lib/types";
2
2
  export declare class SessionManager {
3
3
  private readonly config;
4
+ private static readonly DEFAULT_PRUNE_IDLE_MS;
5
+ private static readonly CLOUD_SLOT_RELEASE_WAIT_MS;
4
6
  private readonly sessions;
5
7
  private activeSessionId?;
6
8
  private readonly refStore;
@@ -8,6 +10,10 @@ export declare class SessionManager {
8
10
  private nowIso;
9
11
  private requireToken;
10
12
  private sessionExpired;
13
+ private sessionIdleMs;
14
+ private isCloudSlotLimitError;
15
+ private pruneInactiveSessions;
16
+ private waitForCloudSlotRelease;
11
17
  private destroySession;
12
18
  private getSessionOrThrow;
13
19
  private evictExpiredSessions;
@@ -17,6 +23,7 @@ export declare class SessionManager {
17
23
  private resetSnapshotState;
18
24
  private activatePage;
19
25
  private validateIdleTimeout;
26
+ private createSessionRecord;
20
27
  private resolveTargetLocator;
21
28
  private runTargetAction;
22
29
  open(request: OpenSessionRequest): Promise<OpenSessionResponse>;
@@ -62,5 +69,6 @@ export declare class SessionManager {
62
69
  close(sessionId?: string): Promise<CloseSessionResponse>;
63
70
  listSessions(): Promise<SessionsResponse>;
64
71
  currentSession(): Promise<SessionSummary>;
65
- closeAll(): Promise<void>;
72
+ pruneSessions(maxIdleMs?: number): Promise<PruneSessionsResponse>;
73
+ closeAll(): Promise<CloseAllSessionsResponse>;
66
74
  }
@@ -13,6 +13,8 @@ const refStore_1 = require("./refStore");
13
13
  const snapshot_1 = require("./snapshot");
14
14
  class SessionManager {
15
15
  config;
16
+ static DEFAULT_PRUNE_IDLE_MS = 10 * 60 * 1000;
17
+ static CLOUD_SLOT_RELEASE_WAIT_MS = 3_000;
16
18
  sessions = new Map();
17
19
  activeSessionId;
18
20
  refStore = new refStore_1.RefStore();
@@ -38,6 +40,32 @@ class SessionManager {
38
40
  }
39
41
  return Date.now() - lastActivityAt > session.idleTimeoutMs;
40
42
  }
43
+ sessionIdleMs(session) {
44
+ const lastActivityAt = Date.parse(session.lastActivityAt);
45
+ if (Number.isNaN(lastActivityAt)) {
46
+ return 0;
47
+ }
48
+ return Math.max(0, Date.now() - lastActivityAt);
49
+ }
50
+ isCloudSlotLimitError(error) {
51
+ return (error instanceof errors_1.AppError &&
52
+ error.code === "BROWSER_CONNECTION_FAILED" &&
53
+ /max parallel cloud launches limit/i.test(error.message));
54
+ }
55
+ async pruneInactiveSessions(maxIdleMs = SessionManager.DEFAULT_PRUNE_IDLE_MS) {
56
+ const closedSessionIds = [];
57
+ for (const session of Array.from(this.sessions.values())) {
58
+ if (this.sessionIdleMs(session) < maxIdleMs) {
59
+ continue;
60
+ }
61
+ closedSessionIds.push(session.sessionId);
62
+ await this.destroySession(session);
63
+ }
64
+ return closedSessionIds;
65
+ }
66
+ async waitForCloudSlotRelease() {
67
+ await new Promise((resolve) => setTimeout(resolve, SessionManager.CLOUD_SLOT_RELEASE_WAIT_MS));
68
+ }
41
69
  async destroySession(session) {
42
70
  await (0, browser_1.closeSessionHandles)(session).catch(() => undefined);
43
71
  this.sessions.delete(session.sessionId);
@@ -116,6 +144,30 @@ class SessionManager {
116
144
  throw new errors_1.AppError("BAD_REQUEST", "--idle-timeout-ms must be a positive integer", 400);
117
145
  }
118
146
  }
147
+ async createSessionRecord(token, sessionId, profileId, request, createdAt, resolvedProxy, autoCreatedProfile) {
148
+ const connection = await (0, browser_1.connectToBrowser)(this.config, token, profileId);
149
+ const currentUrl = await (0, browser_1.navigatePage)(connection.page, request.url, this.config.navigationTimeoutMs);
150
+ const lastActivityAt = this.nowIso();
151
+ if (!resolvedProxy && profileId) {
152
+ resolvedProxy = await (0, browser_1.getCloudProfileProxy)(token, profileId).catch(() => undefined);
153
+ }
154
+ return {
155
+ sessionId,
156
+ profileId,
157
+ autoCreatedProfile,
158
+ connectUrl: connection.connectUrl,
159
+ browser: connection.browser,
160
+ context: connection.context,
161
+ page: connection.page,
162
+ currentUrl,
163
+ hasSnapshot: false,
164
+ staleSnapshot: false,
165
+ proxy: resolvedProxy,
166
+ createdAt,
167
+ lastActivityAt,
168
+ idleTimeoutMs: request.idleTimeoutMs
169
+ };
170
+ }
119
171
  async resolveTargetLocator(session, target) {
120
172
  if ((0, utils_1.isRefTarget)(target)) {
121
173
  const descriptor = this.refStore.get(session.sessionId, target);
@@ -142,6 +194,7 @@ class SessionManager {
142
194
  async open(request) {
143
195
  const token = this.requireToken();
144
196
  this.validateIdleTimeout(request.idleTimeoutMs);
197
+ await this.pruneInactiveSessions();
145
198
  if (request.profileId && request.proxy) {
146
199
  throw new errors_1.AppError("BAD_REQUEST", "proxy flags cannot be combined with --profile", 400);
147
200
  }
@@ -180,35 +233,36 @@ class SessionManager {
180
233
  autoCreatedProfile = true;
181
234
  }
182
235
  try {
183
- const connection = await (0, browser_1.connectToBrowser)(this.config, token, profileId);
184
- const currentUrl = await (0, browser_1.navigatePage)(connection.page, request.url, this.config.navigationTimeoutMs);
185
- const lastActivityAt = this.nowIso();
186
- if (!resolvedProxy && profileId) {
187
- resolvedProxy = await (0, browser_1.getCloudProfileProxy)(token, profileId).catch(() => undefined);
236
+ let session;
237
+ try {
238
+ session = await this.createSessionRecord(token, sessionId, profileId, request, createdAt, resolvedProxy, autoCreatedProfile);
239
+ }
240
+ catch (error) {
241
+ if (!this.isCloudSlotLimitError(error)) {
242
+ throw error;
243
+ }
244
+ if (this.sessions.size === 0) {
245
+ throw new errors_1.AppError("BROWSER_CONNECTION_FAILED", `${error.message}. No tracked local sessions were available to close. Wait for cloud slots to free up or close stale sessions from another daemon, then retry.`, error.status, error.details);
246
+ }
247
+ const closedSessionIds = (await this.closeAll()).closedSessionIds;
248
+ await this.waitForCloudSlotRelease();
249
+ try {
250
+ session = await this.createSessionRecord(token, sessionId, profileId, request, createdAt, resolvedProxy, autoCreatedProfile);
251
+ }
252
+ catch (retryError) {
253
+ if (retryError instanceof errors_1.AppError && retryError.code === "BROWSER_CONNECTION_FAILED") {
254
+ throw new errors_1.AppError(retryError.code, `${retryError.message}. Closed tracked sessions (${closedSessionIds.join(", ")}) and retried once, but the cloud slot was still unavailable.`, retryError.status, retryError.details);
255
+ }
256
+ throw retryError;
257
+ }
188
258
  }
189
- const session = {
190
- sessionId,
191
- profileId,
192
- autoCreatedProfile,
193
- connectUrl: connection.connectUrl,
194
- browser: connection.browser,
195
- context: connection.context,
196
- page: connection.page,
197
- currentUrl,
198
- hasSnapshot: false,
199
- staleSnapshot: false,
200
- proxy: resolvedProxy,
201
- createdAt,
202
- lastActivityAt,
203
- idleTimeoutMs: request.idleTimeoutMs
204
- };
205
259
  this.sessions.set(sessionId, session);
206
260
  this.activeSessionId = sessionId;
207
261
  this.refStore.clear(sessionId);
208
262
  return {
209
263
  sessionId,
210
264
  profileId,
211
- url: currentUrl,
265
+ url: session.currentUrl,
212
266
  proxy: session.proxy,
213
267
  idleTimeoutMs: session.idleTimeoutMs
214
268
  };
@@ -436,10 +490,11 @@ class SessionManager {
436
490
  value
437
491
  };
438
492
  }
439
- if (!target) {
493
+ const resolvedTarget = target ?? (kind === "text" || kind === "html" ? "body" : undefined);
494
+ if (!resolvedTarget) {
440
495
  throw new errors_1.AppError("BAD_REQUEST", `get ${kind} requires a target`, 400);
441
496
  }
442
- const locator = await this.resolveTargetLocator(session, target);
497
+ const locator = await this.resolveTargetLocator(session, resolvedTarget);
443
498
  const value = await (0, browser_1.readLocatorValue)(locator, kind, this.config.actionTimeoutMs);
444
499
  this.markSessionState(session);
445
500
  return {
@@ -673,12 +728,26 @@ class SessionManager {
673
728
  async currentSession() {
674
729
  return this.toSummary(await this.getSessionOrThrow());
675
730
  }
731
+ async pruneSessions(maxIdleMs = SessionManager.DEFAULT_PRUNE_IDLE_MS) {
732
+ const closedSessionIds = await this.pruneInactiveSessions(maxIdleMs);
733
+ return {
734
+ closed: closedSessionIds.length,
735
+ closedSessionIds,
736
+ maxIdleMs,
737
+ };
738
+ }
676
739
  async closeAll() {
740
+ const closedSessionIds = [];
677
741
  for (const session of Array.from(this.sessions.values())) {
742
+ closedSessionIds.push(session.sessionId);
678
743
  await this.destroySession(session);
679
744
  }
680
745
  this.sessions.clear();
681
746
  this.activeSessionId = undefined;
747
+ return {
748
+ closed: closedSessionIds.length,
749
+ closedSessionIds,
750
+ };
682
751
  }
683
752
  }
684
753
  exports.SessionManager = SessionManager;
@@ -206,10 +206,22 @@ export interface CloseSessionResponse {
206
206
  sessionId: string;
207
207
  closed: true;
208
208
  }
209
+ export interface CloseAllSessionsResponse {
210
+ closed: number;
211
+ closedSessionIds: string[];
212
+ }
209
213
  export interface SessionsResponse {
210
214
  activeSessionId?: string;
211
215
  sessions: SessionSummary[];
212
216
  }
217
+ export interface PruneSessionsRequest {
218
+ maxIdleMs?: number;
219
+ }
220
+ export interface PruneSessionsResponse {
221
+ closed: number;
222
+ closedSessionIds: string[];
223
+ maxIdleMs: number;
224
+ }
213
225
  export interface TabSummary {
214
226
  index: number;
215
227
  url: string;
package/dist/lib/utils.js CHANGED
@@ -26,7 +26,7 @@ const errors_1 = require("./errors");
26
26
  function parseArgs(argv) {
27
27
  const positional = [];
28
28
  const flags = {};
29
- const booleanFlags = new Set(["interactive", "exact", "annotate", "press-escape", "json", "clear"]);
29
+ const booleanFlags = new Set(["interactive", "exact", "annotate", "press-escape", "json", "clear", "all", "prune"]);
30
30
  for (let index = 0; index < argv.length; index += 1) {
31
31
  const token = argv[index];
32
32
  if (token === "-i") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gologin-agent-browser-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Agent-native cloud browser automation CLI for Gologin",
5
5
  "main": "dist/cli.js",
6
6
  "types": "dist/lib/types.d.ts",