agent-browser-loop 0.2.2 → 0.3.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.
package/src/cli.ts CHANGED
@@ -24,8 +24,17 @@ import {
24
24
  isDaemonRunning,
25
25
  } from "./daemon";
26
26
  import { log, withLog } from "./log";
27
+ import {
28
+ deleteProfile,
29
+ importProfile,
30
+ listProfiles,
31
+ loadProfile,
32
+ resolveProfilePath,
33
+ resolveStorageStateOption,
34
+ saveProfile,
35
+ } from "./profiles";
27
36
  import { startBrowserServer } from "./server";
28
- import type { BrowserCliConfig } from "./types";
37
+ import type { BrowserCliConfig, StorageState } from "./types";
29
38
 
30
39
  // ============================================================================
31
40
  // Config Loading
@@ -119,16 +128,62 @@ const jsonFlag = flag({
119
128
  description: "Output as JSON instead of text",
120
129
  });
121
130
 
131
+ const profileOption = option({
132
+ long: "profile",
133
+ short: "p",
134
+ type: optional(string),
135
+ description:
136
+ "Load profile and save back on close (use --no-save for read-only)",
137
+ });
138
+
139
+ const noSaveFlag = flag({
140
+ long: "no-save",
141
+ description: "Don't save profile changes on close (read-only)",
142
+ });
143
+
144
+ const globalFlag = flag({
145
+ long: "global",
146
+ description: "Save to user-level global profiles (~/.config/agent-browser/)",
147
+ });
148
+
149
+ const privateFlag = flag({
150
+ long: "private",
151
+ description: "Save to project .private/ (gitignored, for secrets)",
152
+ });
153
+
154
+ const widthOption = option({
155
+ long: "width",
156
+ short: "W",
157
+ type: optional(number),
158
+ description: "Viewport width in pixels (default: 1280)",
159
+ });
160
+
161
+ const heightOption = option({
162
+ long: "height",
163
+ short: "H",
164
+ type: optional(number),
165
+ description: "Viewport height in pixels (default: 720)",
166
+ });
167
+
122
168
  // ============================================================================
123
169
  // Browser Options Resolution
124
170
  // ============================================================================
125
171
 
172
+ type SessionBrowserOptions = AgentBrowserOptions & {
173
+ profile?: string;
174
+ noSave?: boolean;
175
+ };
176
+
126
177
  async function resolveBrowserOptions(args: {
127
178
  configPath?: string;
128
179
  headless?: boolean;
129
180
  headed?: boolean;
130
181
  bundled?: boolean;
131
- }): Promise<AgentBrowserOptions> {
182
+ profile?: string;
183
+ noSave?: boolean;
184
+ width?: number;
185
+ height?: number;
186
+ }): Promise<SessionBrowserOptions> {
132
187
  const configPath = await findConfigPath(args.configPath);
133
188
  const config = configPath ? await loadConfig(configPath) : undefined;
134
189
 
@@ -143,18 +198,30 @@ async function resolveBrowserOptions(args: {
143
198
 
144
199
  const useSystemChrome = args.bundled ? false : config?.useSystemChrome;
145
200
 
201
+ // Resolve storage state from profile or config
202
+ const storageState = resolveStorageStateOption(
203
+ args.profile,
204
+ config?.storageStatePath,
205
+ );
206
+
146
207
  return {
147
208
  headless,
148
209
  executablePath: config?.executablePath,
149
210
  useSystemChrome,
150
211
  allowSystemChromeHeadless: config?.allowSystemChromeHeadless,
151
- viewportWidth: config?.viewportWidth,
152
- viewportHeight: config?.viewportHeight,
212
+ viewportWidth: args.width ?? config?.viewportWidth,
213
+ viewportHeight: args.height ?? config?.viewportHeight,
153
214
  userDataDir: config?.userDataDir,
154
215
  timeout: config?.timeout,
155
216
  captureNetwork: config?.captureNetwork,
156
217
  networkLogLimit: config?.networkLogLimit,
157
- storageStatePath: config?.storageStatePath,
218
+ // Use resolved storage state (object or path)
219
+ storageState: typeof storageState === "object" ? storageState : undefined,
220
+ storageStatePath:
221
+ typeof storageState === "string" ? storageState : undefined,
222
+ // Track profile for save-on-close
223
+ profile: args.profile,
224
+ noSave: args.noSave,
158
225
  };
159
226
  }
160
227
 
@@ -171,6 +238,7 @@ async function resolveBrowserOptions(args: {
171
238
  * press:Enter
172
239
  * scroll:down
173
240
  * scroll:down:500
241
+ * resize:1920:1080
174
242
  */
175
243
  function parseAction(actionStr: string): StepAction {
176
244
  const parts = actionStr.split(":");
@@ -207,6 +275,15 @@ function parseAction(actionStr: string): StepAction {
207
275
  return { type: "select", ref, value };
208
276
  }
209
277
 
278
+ case "resize": {
279
+ const width = Number.parseInt(parts[1], 10);
280
+ const height = Number.parseInt(parts[2], 10);
281
+ if (Number.isNaN(width) || Number.isNaN(height)) {
282
+ throw new Error("resize requires width:height (e.g. resize:1920:1080)");
283
+ }
284
+ return { type: "resize", width, height };
285
+ }
286
+
210
287
  default:
211
288
  throw new Error(`Unknown action type: ${type}`);
212
289
  }
@@ -320,6 +397,10 @@ const openCommand = command({
320
397
  headed: headedFlag,
321
398
  config: configOption,
322
399
  json: jsonFlag,
400
+ profile: profileOption,
401
+ noSave: noSaveFlag,
402
+ width: widthOption,
403
+ height: heightOption,
323
404
  },
324
405
  handler: async (args) => {
325
406
  const browserOptions = await resolveBrowserOptions({
@@ -351,13 +432,16 @@ const openCommand = command({
351
432
  if (args.json) {
352
433
  const jsonData =
353
434
  typeof response.data === "object" && response.data !== null
354
- ? { ...(response.data as object), sessionId }
355
- : { data: response.data, sessionId };
435
+ ? { ...(response.data as object), sessionId, profile: args.profile }
436
+ : { data: response.data, sessionId, profile: args.profile };
356
437
  console.log(JSON.stringify(jsonData, null, 2));
357
438
  } else {
358
439
  if (args.new && sessionId) {
359
440
  console.log(`Session: ${sessionId}`);
360
441
  }
442
+ if (args.profile) {
443
+ console.log(`Profile: ${args.profile}`);
444
+ }
361
445
  console.log(data.text ?? "Navigated successfully");
362
446
  }
363
447
  },
@@ -380,6 +464,7 @@ const actCommand = command({
380
464
  long: "no-state",
381
465
  description: "Don't return state after actions",
382
466
  }),
467
+ profile: profileOption,
383
468
  },
384
469
  handler: async (args) => {
385
470
  if (args.actions.length === 0) {
@@ -606,6 +691,44 @@ const screenshotCommand = command({
606
691
  },
607
692
  });
608
693
 
694
+ // --- resize ---
695
+ const resizeCommand = command({
696
+ name: "resize",
697
+ description: "Resize browser viewport",
698
+ args: {
699
+ width: positional({ type: number, displayName: "width" }),
700
+ height: positional({ type: number, displayName: "height" }),
701
+ session: sessionOption,
702
+ json: jsonFlag,
703
+ },
704
+ 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
+ }
712
+
713
+ const response = await client.command({
714
+ type: "resize",
715
+ width: args.width,
716
+ height: args.height,
717
+ });
718
+
719
+ if (!response.success) {
720
+ console.error("Error:", response.error);
721
+ process.exit(1);
722
+ }
723
+
724
+ if (args.json) {
725
+ console.log(JSON.stringify({ width: args.width, height: args.height }));
726
+ } else {
727
+ console.log(`Viewport resized to ${args.width}x${args.height}`);
728
+ }
729
+ },
730
+ });
731
+
609
732
  // --- close ---
610
733
  const closeCommand = command({
611
734
  name: "close",
@@ -906,6 +1029,323 @@ const installSkillCommand = command({
906
1029
  },
907
1030
  });
908
1031
 
1032
+ // ============================================================================
1033
+ // Profile Commands
1034
+ // ============================================================================
1035
+
1036
+ const profileListCommand = command({
1037
+ name: "list",
1038
+ description: "List all available profiles",
1039
+ args: {
1040
+ json: jsonFlag,
1041
+ },
1042
+ handler: async (args) => {
1043
+ const profiles = listProfiles();
1044
+
1045
+ if (args.json) {
1046
+ console.log(JSON.stringify(profiles, null, 2));
1047
+ } else {
1048
+ if (profiles.length === 0) {
1049
+ console.log("No profiles found.");
1050
+ console.log("\nCreate a profile with:");
1051
+ console.log(" agent-browser profile login <name> --url <login-url>");
1052
+ console.log(" agent-browser profile save <name>");
1053
+ return;
1054
+ }
1055
+
1056
+ console.log(`Profiles (${profiles.length}):\n`);
1057
+ for (const p of profiles) {
1058
+ const scopeLabel =
1059
+ p.scope === "global"
1060
+ ? "[global]"
1061
+ : p.scope === "local-private"
1062
+ ? "[private]"
1063
+ : "[local]";
1064
+ console.log(` ${p.name} ${scopeLabel}`);
1065
+ if (p.meta?.description) {
1066
+ console.log(` ${p.meta.description}`);
1067
+ }
1068
+ if (p.meta?.origins?.length) {
1069
+ console.log(` Origins: ${p.meta.origins.join(", ")}`);
1070
+ }
1071
+ if (p.meta?.lastUsedAt) {
1072
+ console.log(` Last used: ${p.meta.lastUsedAt}`);
1073
+ }
1074
+ console.log();
1075
+ }
1076
+ }
1077
+ },
1078
+ });
1079
+
1080
+ const profileShowCommand = command({
1081
+ name: "show",
1082
+ description: "Show profile contents",
1083
+ args: {
1084
+ name: positional({ type: string, displayName: "name" }),
1085
+ json: jsonFlag,
1086
+ },
1087
+ handler: async (args) => {
1088
+ const profile = loadProfile(args.name);
1089
+ if (!profile) {
1090
+ console.error(`Profile not found: ${args.name}`);
1091
+ process.exit(1);
1092
+ }
1093
+
1094
+ if (args.json) {
1095
+ console.log(JSON.stringify(profile, null, 2));
1096
+ } else {
1097
+ const resolved = resolveProfilePath(args.name);
1098
+ console.log(`Profile: ${args.name}`);
1099
+ console.log(`Path: ${resolved?.path}`);
1100
+ console.log(`Scope: ${resolved?.scope}`);
1101
+ console.log();
1102
+ if (profile._meta?.description) {
1103
+ console.log(`Description: ${profile._meta.description}`);
1104
+ }
1105
+ if (profile._meta?.origins?.length) {
1106
+ console.log(`Origins: ${profile._meta.origins.join(", ")}`);
1107
+ }
1108
+ if (profile._meta?.createdAt) {
1109
+ console.log(`Created: ${profile._meta.createdAt}`);
1110
+ }
1111
+ if (profile._meta?.lastUsedAt) {
1112
+ console.log(`Last used: ${profile._meta.lastUsedAt}`);
1113
+ }
1114
+ console.log();
1115
+ console.log(`Cookies: ${profile.cookies.length}`);
1116
+ for (const cookie of profile.cookies) {
1117
+ console.log(` - ${cookie.name} (${cookie.domain})`);
1118
+ }
1119
+ console.log();
1120
+ console.log(`LocalStorage origins: ${profile.origins.length}`);
1121
+ for (const origin of profile.origins) {
1122
+ console.log(
1123
+ ` - ${origin.origin}: ${origin.localStorage.length} items`,
1124
+ );
1125
+ }
1126
+ }
1127
+ },
1128
+ });
1129
+
1130
+ const profileSaveCommand = command({
1131
+ name: "save",
1132
+ description: "Save current session storage to a profile",
1133
+ args: {
1134
+ name: positional({ type: string, displayName: "name" }),
1135
+ session: sessionOption,
1136
+ global: globalFlag,
1137
+ private: privateFlag,
1138
+ description: option({
1139
+ long: "description",
1140
+ short: "d",
1141
+ type: optional(string),
1142
+ description: "Profile description",
1143
+ }),
1144
+ },
1145
+ 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
+ }
1154
+
1155
+ // Get storage state from session via command
1156
+ const response = await client.command({
1157
+ type: "saveStorageState",
1158
+ });
1159
+
1160
+ if (!response.success) {
1161
+ console.error("Error:", response.error);
1162
+ process.exit(1);
1163
+ }
1164
+
1165
+ const storageState = response.data as StorageState;
1166
+
1167
+ // Extract origins from storage state
1168
+ const origins = [
1169
+ ...new Set([
1170
+ ...storageState.cookies.map((c) => c.domain),
1171
+ ...storageState.origins.map((o) => o.origin),
1172
+ ]),
1173
+ ].filter(Boolean);
1174
+
1175
+ const savedPath = saveProfile(args.name, storageState, {
1176
+ global: args.global,
1177
+ private: args.private,
1178
+ description: args.description,
1179
+ origins,
1180
+ });
1181
+
1182
+ console.log(`Profile saved: ${args.name}`);
1183
+ console.log(`Path: ${savedPath}`);
1184
+ console.log(`Cookies: ${storageState.cookies.length}`);
1185
+ console.log(`LocalStorage origins: ${storageState.origins.length}`);
1186
+ },
1187
+ });
1188
+
1189
+ const profileDeleteCommand = command({
1190
+ name: "delete",
1191
+ description: "Delete a profile",
1192
+ args: {
1193
+ name: positional({ type: string, displayName: "name" }),
1194
+ },
1195
+ handler: async (args) => {
1196
+ const resolved = resolveProfilePath(args.name);
1197
+ if (!resolved) {
1198
+ console.error(`Profile not found: ${args.name}`);
1199
+ process.exit(1);
1200
+ }
1201
+
1202
+ const deleted = deleteProfile(args.name);
1203
+ if (deleted) {
1204
+ console.log(`Deleted profile: ${args.name}`);
1205
+ console.log(`Path: ${resolved.path}`);
1206
+ } else {
1207
+ console.error(`Failed to delete profile: ${args.name}`);
1208
+ process.exit(1);
1209
+ }
1210
+ },
1211
+ });
1212
+
1213
+ const profileImportCommand = command({
1214
+ name: "import",
1215
+ description: "Import a profile from a storage state JSON file",
1216
+ args: {
1217
+ name: positional({ type: string, displayName: "name" }),
1218
+ path: positional({ type: string, displayName: "path" }),
1219
+ global: globalFlag,
1220
+ private: privateFlag,
1221
+ },
1222
+ handler: async (args) => {
1223
+ try {
1224
+ const savedPath = importProfile(args.name, args.path, {
1225
+ global: args.global,
1226
+ private: args.private,
1227
+ });
1228
+ console.log(`Imported profile: ${args.name}`);
1229
+ console.log(`Path: ${savedPath}`);
1230
+ } catch (err) {
1231
+ console.error("Error:", err instanceof Error ? err.message : String(err));
1232
+ process.exit(1);
1233
+ }
1234
+ },
1235
+ });
1236
+
1237
+ const profileCaptureCommand = command({
1238
+ name: "capture",
1239
+ description: "Open browser, interact manually, then save session to profile",
1240
+ args: {
1241
+ name: positional({ type: string, displayName: "name" }),
1242
+ url: option({
1243
+ long: "url",
1244
+ type: string,
1245
+ description: "URL to navigate to",
1246
+ }),
1247
+ headed: headedFlag,
1248
+ headless: headlessFlag,
1249
+ config: configOption,
1250
+ global: globalFlag,
1251
+ private: privateFlag,
1252
+ description: option({
1253
+ long: "description",
1254
+ short: "d",
1255
+ type: optional(string),
1256
+ description: "Profile description",
1257
+ }),
1258
+ },
1259
+ handler: async (args) => {
1260
+ console.log(`Capturing session for profile: ${args.name}`);
1261
+ console.log(`URL: ${args.url}`);
1262
+ console.log();
1263
+ console.log("Browser will open. Log in or do whatever you need.");
1264
+ console.log("Press Enter here when done to save and close.");
1265
+ console.log();
1266
+
1267
+ // Force headed mode for interactive login
1268
+ const browserOptions = await resolveBrowserOptions({
1269
+ ...args,
1270
+ configPath: args.config,
1271
+ headed: true,
1272
+ headless: false,
1273
+ });
1274
+
1275
+ // Create a new session for login
1276
+ const client = await ensureDaemonNewSession(browserOptions);
1277
+ const sessionId = client.getSessionId();
1278
+
1279
+ // Navigate to login URL
1280
+ const navResponse = await client.act([{ type: "navigate", url: args.url }]);
1281
+ if (!navResponse.success) {
1282
+ console.error("Error navigating:", navResponse.error);
1283
+ await client.closeSession(sessionId!);
1284
+ process.exit(1);
1285
+ }
1286
+
1287
+ // Wait for user to press Enter
1288
+ process.stdout.write("Press Enter when login is complete...");
1289
+ await new Promise<void>((resolve) => {
1290
+ process.stdin.setRawMode?.(false);
1291
+ process.stdin.resume();
1292
+ process.stdin.once("data", () => {
1293
+ resolve();
1294
+ });
1295
+ });
1296
+ console.log();
1297
+
1298
+ // Save storage state
1299
+ const saveResponse = await client.command({
1300
+ type: "saveStorageState",
1301
+ });
1302
+
1303
+ if (!saveResponse.success) {
1304
+ console.error("Error saving storage state:", saveResponse.error);
1305
+ await client.closeSession(sessionId!);
1306
+ process.exit(1);
1307
+ }
1308
+
1309
+ const storageState = saveResponse.data as StorageState;
1310
+
1311
+ // Extract origins from storage state
1312
+ const origins = [
1313
+ ...new Set([
1314
+ ...storageState.cookies.map((c) => c.domain),
1315
+ ...storageState.origins.map((o) => o.origin),
1316
+ ]),
1317
+ ].filter(Boolean);
1318
+
1319
+ const savedPath = saveProfile(args.name, storageState, {
1320
+ global: args.global,
1321
+ private: args.private,
1322
+ description: args.description,
1323
+ origins,
1324
+ });
1325
+
1326
+ // Close the session
1327
+ await client.closeSession(sessionId!);
1328
+
1329
+ console.log();
1330
+ console.log(`Profile saved: ${args.name}`);
1331
+ console.log(`Path: ${savedPath}`);
1332
+ console.log(`Cookies: ${storageState.cookies.length}`);
1333
+ console.log(`LocalStorage origins: ${storageState.origins.length}`);
1334
+ },
1335
+ });
1336
+
1337
+ const profileCommand = subcommands({
1338
+ name: "profile",
1339
+ cmds: {
1340
+ list: profileListCommand,
1341
+ show: profileShowCommand,
1342
+ save: profileSaveCommand,
1343
+ delete: profileDeleteCommand,
1344
+ import: profileImportCommand,
1345
+ capture: profileCaptureCommand,
1346
+ },
1347
+ });
1348
+
909
1349
  // ============================================================================
910
1350
  // Main CLI
911
1351
  // ============================================================================
@@ -919,10 +1359,14 @@ const cli = subcommands({
919
1359
  wait: waitCommand,
920
1360
  state: stateCommand,
921
1361
  screenshot: screenshotCommand,
1362
+ resize: resizeCommand,
922
1363
  close: closeCommand,
923
1364
  sessions: sessionsCommand,
924
1365
  status: statusCommand,
925
1366
 
1367
+ // Profile management
1368
+ profile: profileCommand,
1369
+
926
1370
  // Setup & configuration
927
1371
  setup: setupCommand,
928
1372
  "install-skill": installSkillCommand,
package/src/commands.ts CHANGED
@@ -121,6 +121,12 @@ const screenshotCommandSchema = z.object({
121
121
  path: z.string().optional(),
122
122
  });
123
123
 
124
+ const resizeCommandSchema = z.object({
125
+ type: z.literal("resize"),
126
+ width: z.number().int().positive(),
127
+ height: z.number().int().positive(),
128
+ });
129
+
124
130
  const saveStorageStateCommandSchema = z.object({
125
131
  type: z.literal("saveStorageState"),
126
132
  path: z.string().optional(),
@@ -178,6 +184,7 @@ export const stepActionSchema = z.discriminatedUnion("type", [
178
184
  waitForElementCommandSchema,
179
185
  screenshotCommandSchema,
180
186
  saveStorageStateCommandSchema,
187
+ resizeCommandSchema,
181
188
  ]);
182
189
 
183
190
  // All commands
@@ -202,6 +209,7 @@ export const commandSchema = z.discriminatedUnion("type", [
202
209
  clearNetworkLogsCommandSchema,
203
210
  enableNetworkCaptureCommandSchema,
204
211
  saveStorageStateCommandSchema,
212
+ resizeCommandSchema,
205
213
  closeCommandSchema,
206
214
  ]);
207
215
 
@@ -280,6 +288,9 @@ export async function executeCommand(
280
288
  case "close":
281
289
  await browser.stop();
282
290
  return;
291
+ case "resize":
292
+ await browser.resize(command.width, command.height);
293
+ return;
283
294
  // Commands that return data
284
295
  case "getState":
285
296
  return browser.getState(command.options);