agent-browser-loop 0.2.1 → 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,17 +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
- viewportWidth: config?.viewportWidth,
151
- viewportHeight: config?.viewportHeight,
211
+ allowSystemChromeHeadless: config?.allowSystemChromeHeadless,
212
+ viewportWidth: args.width ?? config?.viewportWidth,
213
+ viewportHeight: args.height ?? config?.viewportHeight,
152
214
  userDataDir: config?.userDataDir,
153
215
  timeout: config?.timeout,
154
216
  captureNetwork: config?.captureNetwork,
155
217
  networkLogLimit: config?.networkLogLimit,
156
- 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,
157
225
  };
158
226
  }
159
227
 
@@ -170,6 +238,7 @@ async function resolveBrowserOptions(args: {
170
238
  * press:Enter
171
239
  * scroll:down
172
240
  * scroll:down:500
241
+ * resize:1920:1080
173
242
  */
174
243
  function parseAction(actionStr: string): StepAction {
175
244
  const parts = actionStr.split(":");
@@ -206,6 +275,15 @@ function parseAction(actionStr: string): StepAction {
206
275
  return { type: "select", ref, value };
207
276
  }
208
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
+
209
287
  default:
210
288
  throw new Error(`Unknown action type: ${type}`);
211
289
  }
@@ -319,9 +397,16 @@ const openCommand = command({
319
397
  headed: headedFlag,
320
398
  config: configOption,
321
399
  json: jsonFlag,
400
+ profile: profileOption,
401
+ noSave: noSaveFlag,
402
+ width: widthOption,
403
+ height: heightOption,
322
404
  },
323
405
  handler: async (args) => {
324
- const browserOptions = await resolveBrowserOptions(args);
406
+ const browserOptions = await resolveBrowserOptions({
407
+ ...args,
408
+ configPath: args.config,
409
+ });
325
410
 
326
411
  let client: DaemonClient;
327
412
  if (args.new) {
@@ -347,13 +432,16 @@ const openCommand = command({
347
432
  if (args.json) {
348
433
  const jsonData =
349
434
  typeof response.data === "object" && response.data !== null
350
- ? { ...(response.data as object), sessionId }
351
- : { data: response.data, sessionId };
435
+ ? { ...(response.data as object), sessionId, profile: args.profile }
436
+ : { data: response.data, sessionId, profile: args.profile };
352
437
  console.log(JSON.stringify(jsonData, null, 2));
353
438
  } else {
354
439
  if (args.new && sessionId) {
355
440
  console.log(`Session: ${sessionId}`);
356
441
  }
442
+ if (args.profile) {
443
+ console.log(`Profile: ${args.profile}`);
444
+ }
357
445
  console.log(data.text ?? "Navigated successfully");
358
446
  }
359
447
  },
@@ -376,6 +464,7 @@ const actCommand = command({
376
464
  long: "no-state",
377
465
  description: "Don't return state after actions",
378
466
  }),
467
+ profile: profileOption,
379
468
  },
380
469
  handler: async (args) => {
381
470
  if (args.actions.length === 0) {
@@ -386,7 +475,10 @@ const actCommand = command({
386
475
  process.exit(1);
387
476
  }
388
477
 
389
- const browserOptions = await resolveBrowserOptions(args);
478
+ const browserOptions = await resolveBrowserOptions({
479
+ ...args,
480
+ configPath: args.config,
481
+ });
390
482
 
391
483
  let client: DaemonClient;
392
484
  if (args.new) {
@@ -599,6 +691,44 @@ const screenshotCommand = command({
599
691
  },
600
692
  });
601
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
+
602
732
  // --- close ---
603
733
  const closeCommand = command({
604
734
  name: "close",
@@ -833,6 +963,7 @@ const serverCommand = command({
833
963
  headless,
834
964
  executablePath: args.executablePath ?? config?.executablePath,
835
965
  useSystemChrome,
966
+ allowSystemChromeHeadless: config?.allowSystemChromeHeadless,
836
967
  viewportWidth: args.viewportWidth || config?.viewportWidth,
837
968
  viewportHeight: args.viewportHeight || config?.viewportHeight,
838
969
  userDataDir: args.userDataDir ?? config?.userDataDir,
@@ -898,6 +1029,323 @@ const installSkillCommand = command({
898
1029
  },
899
1030
  });
900
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
+
901
1349
  // ============================================================================
902
1350
  // Main CLI
903
1351
  // ============================================================================
@@ -911,10 +1359,14 @@ const cli = subcommands({
911
1359
  wait: waitCommand,
912
1360
  state: stateCommand,
913
1361
  screenshot: screenshotCommand,
1362
+ resize: resizeCommand,
914
1363
  close: closeCommand,
915
1364
  sessions: sessionsCommand,
916
1365
  status: statusCommand,
917
1366
 
1367
+ // Profile management
1368
+ profile: profileCommand,
1369
+
918
1370
  // Setup & configuration
919
1371
  setup: setupCommand,
920
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);
package/src/config.ts CHANGED
@@ -32,6 +32,7 @@ export const browserCliConfigSchema = z.looseObject({
32
32
  headless: z.boolean().optional(),
33
33
  executablePath: z.string().optional(),
34
34
  useSystemChrome: z.boolean().optional(),
35
+ allowSystemChromeHeadless: z.boolean().optional(),
35
36
  viewportWidth: z.number().int().optional(),
36
37
  viewportHeight: z.number().int().optional(),
37
38
  userDataDir: z.string().optional(),