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 +1 -1
- package/src/cli.ts +25 -41
- package/src/daemon.ts +117 -0
- package/src/index.ts +4 -0
- package/src/version.ts +19 -0
package/package.json
CHANGED
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 =
|
|
583
|
-
|
|
584
|
-
|
|
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 =
|
|
616
|
-
|
|
617
|
-
|
|
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 =
|
|
660
|
-
|
|
661
|
-
|
|
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 =
|
|
706
|
-
|
|
707
|
-
|
|
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 =
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
.
|
|
1384
|
-
|
|
1385
|
-
|
|
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();
|