agent-browser-loop 0.2.2 → 0.3.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/src/daemon.ts CHANGED
@@ -3,6 +3,7 @@ import * as net from "node:net";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { z } from "zod";
6
+ import { VERSION } from "./version";
6
7
  import { type AgentBrowserOptions, createBrowser } from "./browser";
7
8
  import {
8
9
  type Command,
@@ -19,7 +20,9 @@ import {
19
20
  waitConditionSchema,
20
21
  } from "./commands";
21
22
  import { createIdGenerator } from "./id";
23
+ import { saveProfile } from "./profiles";
22
24
  import { formatStateText } from "./state";
25
+ import type { StorageState } from "./types";
23
26
 
24
27
  // ============================================================================
25
28
  // Daemon Protocol
@@ -37,6 +40,10 @@ const browserOptionsSchema = z
37
40
  captureNetwork: z.boolean().optional(),
38
41
  networkLogLimit: z.number().optional(),
39
42
  storageStatePath: z.string().optional(),
43
+ // Profile to load and save back on close
44
+ profile: z.string().optional(),
45
+ // If true, don't save profile on close (read-only)
46
+ noSave: z.boolean().optional(),
40
47
  })
41
48
  .optional();
42
49
 
@@ -120,6 +127,10 @@ type DaemonSession = {
120
127
  lastUsed: number;
121
128
  busy: boolean;
122
129
  options: AgentBrowserOptions;
130
+ // Profile name to save back on close (if set)
131
+ profile?: string;
132
+ // If true, don't save profile on close
133
+ noSave?: boolean;
123
134
  };
124
135
 
125
136
  // ============================================================================
@@ -147,6 +158,25 @@ export function getConfigPath(): string {
147
158
  return path.join(DAEMON_DIR, "daemon.config.json");
148
159
  }
149
160
 
161
+ export function getVersionPath(): string {
162
+ return path.join(DAEMON_DIR, "daemon.version");
163
+ }
164
+
165
+ /**
166
+ * Get the version of the currently running daemon (if any)
167
+ */
168
+ export function getDaemonVersion(): string | null {
169
+ const versionPath = getVersionPath();
170
+ if (!fs.existsSync(versionPath)) {
171
+ return null;
172
+ }
173
+ try {
174
+ return fs.readFileSync(versionPath, "utf-8").trim();
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
150
180
  // ============================================================================
151
181
  // Daemon Status
152
182
  // ============================================================================
@@ -173,6 +203,7 @@ export function cleanupDaemonFiles(): void {
173
203
  const socketPath = getSocketPath();
174
204
  const pidPath = getPidPath();
175
205
  const configPath = getConfigPath();
206
+ const versionPath = getVersionPath();
176
207
 
177
208
  try {
178
209
  if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
@@ -183,6 +214,9 @@ export function cleanupDaemonFiles(): void {
183
214
  try {
184
215
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
185
216
  } catch {}
217
+ try {
218
+ if (fs.existsSync(versionPath)) fs.unlinkSync(versionPath);
219
+ } catch {}
186
220
  }
187
221
 
188
222
  // ============================================================================
@@ -200,10 +234,14 @@ export async function startDaemon(options: DaemonOptions = {}): Promise<void> {
200
234
  const socketPath = getSocketPath();
201
235
  const pidPath = getPidPath();
202
236
  const configPath = getConfigPath();
237
+ const versionPath = getVersionPath();
203
238
 
204
239
  ensureDaemonDir();
205
240
  cleanupDaemonFiles();
206
241
 
242
+ // Write version file so CLI can detect version mismatch
243
+ fs.writeFileSync(versionPath, VERSION);
244
+
207
245
  // Multi-session state
208
246
  const sessions = new Map<string, DaemonSession>();
209
247
  const idGenerator = createIdGenerator();
@@ -219,13 +257,17 @@ export async function startDaemon(options: DaemonOptions = {}): Promise<void> {
219
257
  // Session helpers
220
258
  async function createSession(
221
259
  sessionId?: string,
222
- browserOptions?: AgentBrowserOptions,
260
+ browserOptions?: AgentBrowserOptions & {
261
+ profile?: string;
262
+ noSave?: boolean;
263
+ },
223
264
  ): Promise<DaemonSession> {
224
265
  const id = sessionId ?? idGenerator.next();
225
266
  if (sessions.has(id)) {
226
267
  throw new Error(`Session already exists: ${id}`);
227
268
  }
228
- const mergedOptions = { ...defaultOptions, ...browserOptions };
269
+ const { profile, noSave, ...restOptions } = browserOptions ?? {};
270
+ const mergedOptions = { ...defaultOptions, ...restOptions };
229
271
  const browser = createBrowser(mergedOptions);
230
272
  await browser.start();
231
273
  const session: DaemonSession = {
@@ -234,6 +276,8 @@ export async function startDaemon(options: DaemonOptions = {}): Promise<void> {
234
276
  lastUsed: Date.now(),
235
277
  busy: false,
236
278
  options: mergedOptions,
279
+ profile,
280
+ noSave,
237
281
  };
238
282
  sessions.set(id, session);
239
283
  return session;
@@ -250,14 +294,34 @@ export async function startDaemon(options: DaemonOptions = {}): Promise<void> {
250
294
  return session;
251
295
  }
252
296
 
253
- async function closeSession(sessionId: string): Promise<void> {
297
+ async function closeSession(
298
+ sessionId: string,
299
+ ): Promise<{ profileSaved?: string }> {
254
300
  const session = sessions.get(sessionId);
255
301
  if (!session) {
256
302
  throw new Error(`Session not found: ${sessionId}`);
257
303
  }
304
+
305
+ let profileSaved: string | undefined;
306
+
307
+ // Save profile if one was loaded and noSave is not set
308
+ if (session.profile && !session.noSave) {
309
+ try {
310
+ const storageState =
311
+ (await session.browser.saveStorageState()) as StorageState;
312
+ saveProfile(session.profile, storageState);
313
+ profileSaved = session.profile;
314
+ } catch (err) {
315
+ // Log but don't fail the close
316
+ console.error(`Failed to save profile ${session.profile}:`, err);
317
+ }
318
+ }
319
+
258
320
  await session.browser.stop();
259
321
  sessions.delete(sessionId);
260
322
  idGenerator.release(sessionId);
323
+
324
+ return { profileSaved };
261
325
  }
262
326
 
263
327
  const shutdown = async () => {
@@ -398,10 +462,10 @@ async function handleRequest(
398
462
  sessions: Map<string, DaemonSession>,
399
463
  createSession: (
400
464
  sessionId?: string,
401
- options?: AgentBrowserOptions,
465
+ options?: AgentBrowserOptions & { profile?: string; noSave?: boolean },
402
466
  ) => Promise<DaemonSession>,
403
467
  getOrDefaultSession: (sessionId?: string) => DaemonSession,
404
- closeSession: (sessionId: string) => Promise<void>,
468
+ closeSession: (sessionId: string) => Promise<{ profileSaved?: string }>,
405
469
  ): Promise<DaemonResponse> {
406
470
  const { id } = request;
407
471
 
@@ -439,8 +503,12 @@ async function handleRequest(
439
503
  }
440
504
 
441
505
  case "close": {
442
- await closeSession(request.sessionId);
443
- return { id, success: true, data: { closed: request.sessionId } };
506
+ const { profileSaved } = await closeSession(request.sessionId);
507
+ return {
508
+ id,
509
+ success: true,
510
+ data: { closed: request.sessionId, profileSaved },
511
+ };
444
512
  }
445
513
 
446
514
  case "command": {
@@ -451,7 +519,12 @@ async function handleRequest(
451
519
  const result = await executeCommand(session.browser, request.command);
452
520
  // Handle close command - close the session
453
521
  if (request.command.type === "close") {
454
- await closeSession(session.id);
522
+ const { profileSaved } = await closeSession(session.id);
523
+ return {
524
+ id,
525
+ success: true,
526
+ data: { ...((result as object) ?? {}), profileSaved },
527
+ };
455
528
  }
456
529
  return { id, success: true, data: result };
457
530
  } finally {
@@ -769,6 +842,34 @@ export class DaemonClient {
769
842
  // Daemon Spawner
770
843
  // ============================================================================
771
844
 
845
+ /**
846
+ * Force restart the daemon by shutting down any existing one
847
+ */
848
+ async function forceRestartDaemon(
849
+ browserOptions?: AgentBrowserOptions,
850
+ ): Promise<void> {
851
+ const client = new DaemonClient();
852
+
853
+ // Try to gracefully shutdown existing daemon
854
+ if (isDaemonRunning()) {
855
+ try {
856
+ if (await client.ping()) {
857
+ await client.shutdown();
858
+ // Wait a bit for shutdown
859
+ await new Promise((r) => setTimeout(r, 200));
860
+ }
861
+ } catch {
862
+ // Ignore errors during shutdown
863
+ }
864
+ }
865
+
866
+ // Clean up any stale files
867
+ cleanupDaemonFiles();
868
+
869
+ // Spawn fresh daemon
870
+ await spawnDaemon(browserOptions);
871
+ }
872
+
772
873
  /**
773
874
  * Ensure daemon is running and return a client.
774
875
  * If sessionId is provided, set the client to use that session.
@@ -784,6 +885,41 @@ export async function ensureDaemon(
784
885
 
785
886
  // Check if daemon is already running
786
887
  if (isDaemonRunning()) {
888
+ // Check for version mismatch - force restart if versions don't match
889
+ const daemonVersion = getDaemonVersion();
890
+ if (daemonVersion && daemonVersion !== VERSION) {
891
+ // If we're not allowed to create sessions, tell user to re-open
892
+ if (!createIfMissing) {
893
+ throw new Error(
894
+ `Daemon was upgraded (${daemonVersion} -> ${VERSION}). Please run 'agent-browser open <url>' to start a new session.`,
895
+ );
896
+ }
897
+
898
+ console.log(
899
+ `Daemon version mismatch (daemon: ${daemonVersion}, cli: ${VERSION}), restarting...`,
900
+ );
901
+ await forceRestartDaemon(browserOptions);
902
+
903
+ // Wait for new daemon to be ready
904
+ const maxAttempts = 50;
905
+ for (let i = 0; i < maxAttempts; i++) {
906
+ await new Promise((r) => setTimeout(r, 100));
907
+ if (await client.ping()) {
908
+ if (sessionId === "default") {
909
+ const createResp = await client.create({
910
+ sessionId: "default",
911
+ browserOptions,
912
+ });
913
+ if (!createResp.success) {
914
+ throw new Error(`Failed to create session: ${createResp.error}`);
915
+ }
916
+ }
917
+ return client;
918
+ }
919
+ }
920
+ throw new Error("Failed to restart daemon after version mismatch");
921
+ }
922
+
787
923
  // Verify it's responsive
788
924
  if (await client.ping()) {
789
925
  // Daemon is running, check if session exists or create default
@@ -848,6 +984,32 @@ export async function ensureDaemonNewSession(
848
984
 
849
985
  // Check if daemon is already running
850
986
  if (isDaemonRunning()) {
987
+ // Check for version mismatch - force restart if versions don't match
988
+ const daemonVersion = getDaemonVersion();
989
+ if (daemonVersion && daemonVersion !== VERSION) {
990
+ console.log(
991
+ `Daemon version mismatch (daemon: ${daemonVersion}, cli: ${VERSION}), restarting...`,
992
+ );
993
+ await forceRestartDaemon(browserOptions);
994
+
995
+ // Wait for new daemon to be ready and create session
996
+ const maxAttempts = 50;
997
+ for (let i = 0; i < maxAttempts; i++) {
998
+ await new Promise((r) => setTimeout(r, 100));
999
+ if (await client.ping()) {
1000
+ const createResp = await client.create({ browserOptions });
1001
+ if (!createResp.success) {
1002
+ throw new Error(`Failed to create session: ${createResp.error}`);
1003
+ }
1004
+ const newSessionId = (createResp.data as { sessionId: string })
1005
+ .sessionId;
1006
+ client.setSession(newSessionId);
1007
+ return client;
1008
+ }
1009
+ }
1010
+ throw new Error("Failed to restart daemon after version mismatch");
1011
+ }
1012
+
851
1013
  if (await client.ping()) {
852
1014
  // Create new session with auto-generated ID
853
1015
  const createResp = await client.create({ browserOptions });
package/src/index.ts CHANGED
@@ -30,11 +30,30 @@ export {
30
30
  DaemonClient,
31
31
  type DaemonOptions,
32
32
  ensureDaemon,
33
+ getDaemonVersion,
33
34
  getPidPath,
34
35
  getSocketPath,
36
+ getVersionPath,
35
37
  isDaemonRunning,
36
38
  startDaemon,
37
39
  } from "./daemon";
40
+ // Version
41
+ export { VERSION } from "./version";
42
+ // Profiles
43
+ export {
44
+ deleteProfile,
45
+ importProfile,
46
+ listProfiles,
47
+ loadProfile,
48
+ loadStorageState,
49
+ type Profile,
50
+ type ProfileInfo,
51
+ type ProfileMeta,
52
+ resolveProfilePath,
53
+ resolveStorageStateOption,
54
+ saveProfile,
55
+ touchProfile,
56
+ } from "./profiles";
38
57
  export type { ElementSelectors, StoredElementRef } from "./ref-store";
39
58
  // Ref store for server-side element reference management
40
59
  export { ElementRefStore } from "./ref-store";