agent-browser-loop 0.3.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-browser-loop",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Let your AI coding agent drive a browser to verify its own work",
5
5
  "license": "MIT",
6
6
  "author": "Jason Silberman",
package/src/cli.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  string,
14
14
  subcommands,
15
15
  } from "cmd-ts";
16
+ import { VERSION } from "./version";
16
17
  import type { AgentBrowserOptions } from "./browser";
17
18
  import type { StepAction } from "./commands";
18
19
  import { parseBrowserConfig } from "./config";
@@ -89,6 +90,8 @@ async function loadConfig(configPath: string): Promise<BrowserCliConfig> {
89
90
  return parseBrowserConfig(exported);
90
91
  }
91
92
 
93
+ // ============================================================================
94
+ // Shared CLI Options
92
95
  // ============================================================================
93
96
  // Shared CLI Options
94
97
  // ============================================================================
@@ -579,13 +582,9 @@ const waitCommand = command({
579
582
  process.exit(1);
580
583
  }
581
584
 
582
- const client = new DaemonClient(args.session);
583
- if (!(await client.ping())) {
584
- console.error(
585
- "Daemon not running. Use 'agent-browser open <url>' first.",
586
- );
587
- process.exit(1);
588
- }
585
+ const client = await ensureDaemon(args.session ?? "default", undefined, {
586
+ createIfMissing: false,
587
+ });
589
588
 
590
589
  const response = await client.wait(condition, { timeoutMs: args.timeout });
591
590
 
@@ -612,13 +611,9 @@ const stateCommand = command({
612
611
  json: jsonFlag,
613
612
  },
614
613
  handler: async (args) => {
615
- const client = new DaemonClient(args.session);
616
- if (!(await client.ping())) {
617
- console.error(
618
- "Daemon not running. Use 'agent-browser open <url>' first.",
619
- );
620
- process.exit(1);
621
- }
614
+ const client = await ensureDaemon(args.session ?? "default", undefined, {
615
+ createIfMissing: false,
616
+ });
622
617
 
623
618
  const response = await client.state({
624
619
  format: args.json ? "json" : "text",
@@ -656,13 +651,9 @@ const screenshotCommand = command({
656
651
  }),
657
652
  },
658
653
  handler: async (args) => {
659
- const client = new DaemonClient(args.session);
660
- if (!(await client.ping())) {
661
- console.error(
662
- "Daemon not running. Use 'agent-browser open <url>' first.",
663
- );
664
- process.exit(1);
665
- }
654
+ const client = await ensureDaemon(args.session ?? "default", undefined, {
655
+ createIfMissing: false,
656
+ });
666
657
 
667
658
  const response = await client.screenshot({
668
659
  fullPage: args.fullPage,
@@ -702,13 +693,9 @@ const resizeCommand = command({
702
693
  json: jsonFlag,
703
694
  },
704
695
  handler: async (args) => {
705
- const client = new DaemonClient(args.session);
706
- if (!(await client.ping())) {
707
- console.error(
708
- "Daemon not running. Use 'agent-browser open <url>' first.",
709
- );
710
- process.exit(1);
711
- }
696
+ const client = await ensureDaemon(args.session ?? "default", undefined, {
697
+ createIfMissing: false,
698
+ });
712
699
 
713
700
  const response = await client.command({
714
701
  type: "resize",
@@ -1143,14 +1130,9 @@ const profileSaveCommand = command({
1143
1130
  }),
1144
1131
  },
1145
1132
  handler: async (args) => {
1146
- const client = new DaemonClient(args.session);
1147
-
1148
- if (!(await client.ping())) {
1149
- console.error(
1150
- "Daemon not running. Use 'agent-browser open <url>' first to start a session.",
1151
- );
1152
- process.exit(1);
1153
- }
1133
+ const client = await ensureDaemon(args.session ?? "default", undefined, {
1134
+ createIfMissing: false,
1135
+ });
1154
1136
 
1155
1137
  // Get storage state from session via command
1156
1138
  const response = await client.command({
@@ -1352,6 +1334,7 @@ const profileCommand = subcommands({
1352
1334
 
1353
1335
  const cli = subcommands({
1354
1336
  name: "agent-browser",
1337
+ version: VERSION,
1355
1338
  cmds: {
1356
1339
  // Primary CLI commands (daemon-based)
1357
1340
  open: openCommand,
@@ -1378,10 +1361,11 @@ const cli = subcommands({
1378
1361
  });
1379
1362
 
1380
1363
  run(cli, process.argv.slice(2)).catch((error) => {
1381
- log
1382
- .withError(error)
1383
- .withMetadata({ argv: process.argv.slice(2) })
1384
- .error("CLI failed");
1385
- console.error(error);
1364
+ // Print clean error message for user-facing errors
1365
+ if (error instanceof Error) {
1366
+ console.error(`Error: ${error.message}`);
1367
+ } else {
1368
+ console.error(error);
1369
+ }
1386
1370
  process.exit(1);
1387
1371
  });
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,
@@ -157,6 +158,25 @@ export function getConfigPath(): string {
157
158
  return path.join(DAEMON_DIR, "daemon.config.json");
158
159
  }
159
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
+
160
180
  // ============================================================================
161
181
  // Daemon Status
162
182
  // ============================================================================
@@ -183,6 +203,7 @@ export function cleanupDaemonFiles(): void {
183
203
  const socketPath = getSocketPath();
184
204
  const pidPath = getPidPath();
185
205
  const configPath = getConfigPath();
206
+ const versionPath = getVersionPath();
186
207
 
187
208
  try {
188
209
  if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
@@ -193,6 +214,9 @@ export function cleanupDaemonFiles(): void {
193
214
  try {
194
215
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
195
216
  } catch {}
217
+ try {
218
+ if (fs.existsSync(versionPath)) fs.unlinkSync(versionPath);
219
+ } catch {}
196
220
  }
197
221
 
198
222
  // ============================================================================
@@ -210,10 +234,14 @@ export async function startDaemon(options: DaemonOptions = {}): Promise<void> {
210
234
  const socketPath = getSocketPath();
211
235
  const pidPath = getPidPath();
212
236
  const configPath = getConfigPath();
237
+ const versionPath = getVersionPath();
213
238
 
214
239
  ensureDaemonDir();
215
240
  cleanupDaemonFiles();
216
241
 
242
+ // Write version file so CLI can detect version mismatch
243
+ fs.writeFileSync(versionPath, VERSION);
244
+
217
245
  // Multi-session state
218
246
  const sessions = new Map<string, DaemonSession>();
219
247
  const idGenerator = createIdGenerator();
@@ -814,6 +842,34 @@ export class DaemonClient {
814
842
  // Daemon Spawner
815
843
  // ============================================================================
816
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
+
817
873
  /**
818
874
  * Ensure daemon is running and return a client.
819
875
  * If sessionId is provided, set the client to use that session.
@@ -829,6 +885,41 @@ export async function ensureDaemon(
829
885
 
830
886
  // Check if daemon is already running
831
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
+
832
923
  // Verify it's responsive
833
924
  if (await client.ping()) {
834
925
  // Daemon is running, check if session exists or create default
@@ -893,6 +984,32 @@ export async function ensureDaemonNewSession(
893
984
 
894
985
  // Check if daemon is already running
895
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
+
896
1013
  if (await client.ping()) {
897
1014
  // Create new session with auto-generated ID
898
1015
  const createResp = await client.create({ browserOptions });
package/src/index.ts CHANGED
@@ -30,11 +30,15 @@ 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";
38
42
  // Profiles
39
43
  export {
40
44
  deleteProfile,
package/src/version.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Version is read from package.json at build/runtime
2
+ // This provides a single source of truth for the package version
3
+
4
+ import { readFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+
7
+ function loadVersion(): string {
8
+ try {
9
+ // Try to read from package.json relative to this file
10
+ const packagePath = join(dirname(import.meta.dirname), "package.json");
11
+ const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
12
+ return pkg.version;
13
+ } catch {
14
+ // Fallback if package.json can't be read
15
+ return "0.0.0";
16
+ }
17
+ }
18
+
19
+ export const VERSION = loadVersion();