agent-browser-loop 0.1.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.
@@ -0,0 +1,455 @@
1
+ import { z } from "zod";
2
+ import type { AgentBrowser } from "./browser";
3
+ import { formatStateText } from "./state";
4
+ import type { BrowserState, GetStateOptions } from "./types";
5
+
6
+ // ============================================================================
7
+ // Command Schemas
8
+ // ============================================================================
9
+
10
+ // Base option schemas
11
+ const navigateOptionsSchema = z.object({
12
+ waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).optional(),
13
+ });
14
+
15
+ const clickOptionsSchema = z.object({
16
+ ref: z.string().optional(),
17
+ index: z.number().int().optional(),
18
+ double: z.boolean().optional(),
19
+ button: z.enum(["left", "right", "middle"]).optional(),
20
+ modifiers: z.array(z.enum(["Alt", "Control", "Meta", "Shift"])).optional(),
21
+ });
22
+
23
+ const typeOptionsSchema = z.object({
24
+ ref: z.string().optional(),
25
+ index: z.number().int().optional(),
26
+ text: z.string(),
27
+ submit: z.boolean().optional(),
28
+ clear: z.boolean().optional(),
29
+ delay: z.number().int().optional(),
30
+ });
31
+
32
+ const waitForNavigationOptionsSchema = z.object({
33
+ timeoutMs: z.number().int().optional(),
34
+ });
35
+
36
+ const waitForElementOptionsSchema = z.object({
37
+ timeoutMs: z.number().int().optional(),
38
+ state: z.enum(["attached", "visible"]).optional(),
39
+ });
40
+
41
+ export const getStateOptionsSchema = z.object({
42
+ includeScreenshot: z.boolean().optional(),
43
+ viewportOnly: z.boolean().optional(),
44
+ includeElements: z.boolean().optional(),
45
+ includeTree: z.boolean().optional(),
46
+ elementsLimit: z.number().int().optional(),
47
+ elementsHead: z.number().int().optional(),
48
+ elementsTail: z.number().int().optional(),
49
+ treeLimit: z.number().int().optional(),
50
+ treeHead: z.number().int().optional(),
51
+ treeTail: z.number().int().optional(),
52
+ });
53
+
54
+ const dumpStateOptionsSchema = z.object({
55
+ path: z.string(),
56
+ pretty: z.boolean().optional(),
57
+ state: getStateOptionsSchema.optional(),
58
+ });
59
+
60
+ const dumpStateTextOptionsSchema = z.object({
61
+ path: z.string(),
62
+ state: getStateOptionsSchema.optional(),
63
+ });
64
+
65
+ const dumpNetworkOptionsSchema = z.object({
66
+ path: z.string(),
67
+ pretty: z.boolean().optional(),
68
+ });
69
+
70
+ // Command schemas
71
+ const navigateCommandSchema = z
72
+ .object({ type: z.literal("navigate"), url: z.string() })
73
+ .extend({ options: navigateOptionsSchema.optional() });
74
+
75
+ const clickCommandSchema = z
76
+ .object({ type: z.literal("click") })
77
+ .extend(clickOptionsSchema.shape);
78
+
79
+ const typeCommandSchema = z
80
+ .object({ type: z.literal("type") })
81
+ .extend(typeOptionsSchema.shape);
82
+
83
+ const pressCommandSchema = z.object({
84
+ type: z.literal("press"),
85
+ key: z.string(),
86
+ });
87
+
88
+ const scrollCommandSchema = z.object({
89
+ type: z.literal("scroll"),
90
+ direction: z.enum(["up", "down"]),
91
+ amount: z.number().int().optional(),
92
+ });
93
+
94
+ const hoverCommandSchema = z.object({
95
+ type: z.literal("hover"),
96
+ ref: z.string().optional(),
97
+ index: z.number().int().optional(),
98
+ });
99
+
100
+ const selectCommandSchema = z.object({
101
+ type: z.literal("select"),
102
+ ref: z.string().optional(),
103
+ index: z.number().int().optional(),
104
+ value: z.union([z.string(), z.array(z.string())]),
105
+ });
106
+
107
+ const waitForNavigationCommandSchema = z.object({
108
+ type: z.literal("waitForNavigation"),
109
+ options: waitForNavigationOptionsSchema.optional(),
110
+ });
111
+
112
+ const waitForElementCommandSchema = z.object({
113
+ type: z.literal("waitForElement"),
114
+ selector: z.string(),
115
+ options: waitForElementOptionsSchema.optional(),
116
+ });
117
+
118
+ const screenshotCommandSchema = z.object({
119
+ type: z.literal("screenshot"),
120
+ fullPage: z.boolean().optional(),
121
+ path: z.string().optional(),
122
+ });
123
+
124
+ const saveStorageStateCommandSchema = z.object({
125
+ type: z.literal("saveStorageState"),
126
+ path: z.string().optional(),
127
+ });
128
+
129
+ const getStateCommandSchema = z.object({
130
+ type: z.literal("getState"),
131
+ options: getStateOptionsSchema.optional(),
132
+ });
133
+
134
+ const dumpStateCommandSchema = z
135
+ .object({ type: z.literal("dumpState") })
136
+ .extend(dumpStateOptionsSchema.shape);
137
+
138
+ const dumpStateTextCommandSchema = z
139
+ .object({ type: z.literal("dumpStateText") })
140
+ .extend(dumpStateTextOptionsSchema.shape);
141
+
142
+ const dumpNetworkLogsCommandSchema = z
143
+ .object({ type: z.literal("dumpNetworkLogs") })
144
+ .extend(dumpNetworkOptionsSchema.shape);
145
+
146
+ const getConsoleLogsCommandSchema = z.object({
147
+ type: z.literal("getConsoleLogs"),
148
+ });
149
+
150
+ const clearConsoleLogsCommandSchema = z.object({
151
+ type: z.literal("clearConsoleLogs"),
152
+ });
153
+
154
+ const getNetworkLogsCommandSchema = z.object({
155
+ type: z.literal("getNetworkLogs"),
156
+ });
157
+
158
+ const clearNetworkLogsCommandSchema = z.object({
159
+ type: z.literal("clearNetworkLogs"),
160
+ });
161
+
162
+ const enableNetworkCaptureCommandSchema = z.object({
163
+ type: z.literal("enableNetworkCapture"),
164
+ });
165
+
166
+ const closeCommandSchema = z.object({ type: z.literal("close") });
167
+
168
+ // Step actions - subset that can be batched
169
+ export const stepActionSchema = z.discriminatedUnion("type", [
170
+ navigateCommandSchema,
171
+ clickCommandSchema,
172
+ typeCommandSchema,
173
+ pressCommandSchema,
174
+ scrollCommandSchema,
175
+ hoverCommandSchema,
176
+ selectCommandSchema,
177
+ waitForNavigationCommandSchema,
178
+ waitForElementCommandSchema,
179
+ screenshotCommandSchema,
180
+ saveStorageStateCommandSchema,
181
+ ]);
182
+
183
+ // All commands
184
+ export const commandSchema = z.discriminatedUnion("type", [
185
+ navigateCommandSchema,
186
+ clickCommandSchema,
187
+ typeCommandSchema,
188
+ pressCommandSchema,
189
+ scrollCommandSchema,
190
+ hoverCommandSchema,
191
+ selectCommandSchema,
192
+ waitForNavigationCommandSchema,
193
+ waitForElementCommandSchema,
194
+ getStateCommandSchema,
195
+ dumpStateCommandSchema,
196
+ dumpStateTextCommandSchema,
197
+ dumpNetworkLogsCommandSchema,
198
+ screenshotCommandSchema,
199
+ getConsoleLogsCommandSchema,
200
+ clearConsoleLogsCommandSchema,
201
+ getNetworkLogsCommandSchema,
202
+ clearNetworkLogsCommandSchema,
203
+ enableNetworkCaptureCommandSchema,
204
+ saveStorageStateCommandSchema,
205
+ closeCommandSchema,
206
+ ]);
207
+
208
+ // Wait condition schema
209
+ export const waitConditionSchema = z.object({
210
+ selector: z.string().min(1).optional(),
211
+ text: z.string().min(1).optional(),
212
+ url: z.string().min(1).optional(),
213
+ notSelector: z.string().min(1).optional(),
214
+ notText: z.string().min(1).optional(),
215
+ });
216
+
217
+ // Derive types
218
+ export type StepAction = z.infer<typeof stepActionSchema>;
219
+ export type Command = z.infer<typeof commandSchema>;
220
+ export type WaitCondition = z.infer<typeof waitConditionSchema>;
221
+
222
+ // ============================================================================
223
+ // Command Execution
224
+ // ============================================================================
225
+
226
+ /**
227
+ * Execute a single command against an AgentBrowser.
228
+ * Returns data for query commands, undefined for action commands.
229
+ */
230
+ export async function executeCommand(
231
+ browser: AgentBrowser,
232
+ command: Command,
233
+ ): Promise<unknown | undefined> {
234
+ switch (command.type) {
235
+ case "navigate":
236
+ await browser.navigate(command.url, command.options);
237
+ return;
238
+ case "click":
239
+ await browser.click(command);
240
+ return;
241
+ case "type":
242
+ await browser.type(command);
243
+ return;
244
+ case "press":
245
+ await browser.press(command.key);
246
+ return;
247
+ case "scroll":
248
+ await browser.scroll(command.direction, command.amount);
249
+ return;
250
+ case "hover":
251
+ await browser.hover(command);
252
+ return;
253
+ case "select":
254
+ await browser.select(command);
255
+ return;
256
+ case "waitForNavigation":
257
+ await browser.waitForNavigation(command.options);
258
+ return;
259
+ case "waitForElement":
260
+ await browser.waitForElement(command.selector, command.options);
261
+ return;
262
+ case "dumpState":
263
+ await browser.dumpState(command);
264
+ return;
265
+ case "dumpStateText":
266
+ await browser.dumpStateText(command);
267
+ return;
268
+ case "dumpNetworkLogs":
269
+ await browser.dumpNetworkLogs(command);
270
+ return;
271
+ case "clearConsoleLogs":
272
+ browser.clearConsoleLogs();
273
+ return;
274
+ case "clearNetworkLogs":
275
+ browser.clearNetworkLogs();
276
+ return;
277
+ case "enableNetworkCapture":
278
+ browser.enableNetworkCapture();
279
+ return;
280
+ case "close":
281
+ await browser.stop();
282
+ return;
283
+ // Commands that return data
284
+ case "getState":
285
+ return browser.getState(command.options);
286
+ case "screenshot":
287
+ return browser.screenshot(command);
288
+ case "getConsoleLogs":
289
+ return browser.getConsoleLogs();
290
+ case "getNetworkLogs":
291
+ return browser.getNetworkLogs();
292
+ case "saveStorageState":
293
+ return browser.saveStorageState(command.path);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Result from executing a single action
299
+ */
300
+ export interface ActionResult {
301
+ action: StepAction;
302
+ result?: unknown;
303
+ error?: string;
304
+ }
305
+
306
+ /**
307
+ * Execute multiple actions sequentially
308
+ */
309
+ export async function executeActions(
310
+ browser: AgentBrowser,
311
+ actions: StepAction[],
312
+ options: { haltOnError?: boolean } = {},
313
+ ): Promise<ActionResult[]> {
314
+ const { haltOnError = true } = options;
315
+ const results: ActionResult[] = [];
316
+
317
+ for (const action of actions) {
318
+ try {
319
+ const result = await executeCommand(browser, action);
320
+ results.push({ action, result: result ?? undefined });
321
+ } catch (error) {
322
+ const message = error instanceof Error ? error.message : String(error);
323
+ results.push({ action, error: message });
324
+ if (haltOnError) {
325
+ break;
326
+ }
327
+ }
328
+ }
329
+
330
+ return results;
331
+ }
332
+
333
+ /**
334
+ * Execute a wait condition
335
+ */
336
+ export async function executeWait(
337
+ browser: AgentBrowser,
338
+ condition: WaitCondition,
339
+ options: { timeoutMs?: number; signal?: AbortSignal } = {},
340
+ ): Promise<void> {
341
+ const { selector, text, url, notSelector, notText } = condition;
342
+
343
+ if (!selector && !text && !url && !notSelector && !notText) {
344
+ throw new Error("Wait condition required");
345
+ }
346
+
347
+ await browser.waitFor({
348
+ selector,
349
+ text,
350
+ url,
351
+ notSelector,
352
+ notText,
353
+ timeoutMs: options.timeoutMs,
354
+ signal: options.signal,
355
+ });
356
+ }
357
+
358
+ // ============================================================================
359
+ // Response Formatting
360
+ // ============================================================================
361
+
362
+ /**
363
+ * Format step results as human-readable text
364
+ */
365
+ export function formatStepText(params: {
366
+ results: ActionResult[];
367
+ stateText?: string;
368
+ }): string {
369
+ const lines: string[] = [];
370
+ lines.push("Step results:");
371
+ for (const result of params.results) {
372
+ const hasError = result.error != null;
373
+ const status = hasError ? "error" : "ok";
374
+ const action = JSON.stringify(result.action);
375
+ lines.push(`- ${status} ${action}`);
376
+ if (hasError) {
377
+ lines.push(` ${result.error}`);
378
+ }
379
+ }
380
+
381
+ if (params.stateText) {
382
+ lines.push("");
383
+ lines.push("State:");
384
+ lines.push(params.stateText);
385
+ }
386
+
387
+ return lines.join("\n");
388
+ }
389
+
390
+ /**
391
+ * Format wait result as human-readable text
392
+ */
393
+ export function formatWaitText(params: {
394
+ condition: WaitCondition;
395
+ stateText?: string;
396
+ }): string {
397
+ const lines: string[] = [];
398
+ const conditions: string[] = [];
399
+ if (params.condition.selector) {
400
+ conditions.push(`selector=${params.condition.selector}`);
401
+ }
402
+ if (params.condition.text) {
403
+ conditions.push(`text=${params.condition.text}`);
404
+ }
405
+ if (params.condition.url) {
406
+ conditions.push(`url=${params.condition.url}`);
407
+ }
408
+ if (params.condition.notSelector) {
409
+ conditions.push(`notSelector=${params.condition.notSelector}`);
410
+ }
411
+ if (params.condition.notText) {
412
+ conditions.push(`notText=${params.condition.notText}`);
413
+ }
414
+
415
+ lines.push(
416
+ `Wait: ok${conditions.length > 0 ? ` (${conditions.join(", ")})` : ""}`,
417
+ );
418
+
419
+ if (params.stateText) {
420
+ lines.push("");
421
+ lines.push("State:");
422
+ lines.push(params.stateText);
423
+ }
424
+
425
+ return lines.join("\n");
426
+ }
427
+
428
+ /**
429
+ * Get state and optionally format as text
430
+ */
431
+ export async function getStateWithFormat(
432
+ browser: AgentBrowser,
433
+ options: {
434
+ stateOptions?: GetStateOptions;
435
+ includeState?: boolean;
436
+ includeStateText?: boolean;
437
+ } = {},
438
+ ): Promise<{ state?: BrowserState; stateText?: string }> {
439
+ const {
440
+ stateOptions,
441
+ includeState = false,
442
+ includeStateText = true,
443
+ } = options;
444
+
445
+ if (!includeState && !includeStateText) {
446
+ return {};
447
+ }
448
+
449
+ const state = await browser.getState(stateOptions);
450
+
451
+ return {
452
+ state: includeState ? state : undefined,
453
+ stateText: includeStateText ? formatStateText(state) : undefined,
454
+ };
455
+ }
package/src/config.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import type { BrowserCliConfig } from "./types";
3
+
4
+ const storageStateSchema = z.object({
5
+ cookies: z
6
+ .array(
7
+ z.object({
8
+ name: z.string(),
9
+ value: z.string(),
10
+ domain: z.string(),
11
+ path: z.string(),
12
+ expires: z.number(),
13
+ httpOnly: z.boolean(),
14
+ secure: z.boolean(),
15
+ sameSite: z.enum(["Strict", "Lax", "None"]),
16
+ }),
17
+ )
18
+ .default([]),
19
+ origins: z
20
+ .array(
21
+ z.object({
22
+ origin: z.string(),
23
+ localStorage: z
24
+ .array(z.object({ name: z.string(), value: z.string() }))
25
+ .default([]),
26
+ }),
27
+ )
28
+ .default([]),
29
+ });
30
+
31
+ export const browserCliConfigSchema = z.looseObject({
32
+ headless: z.boolean().optional(),
33
+ executablePath: z.string().optional(),
34
+ useSystemChrome: z.boolean().optional(),
35
+ viewportWidth: z.number().int().optional(),
36
+ viewportHeight: z.number().int().optional(),
37
+ userDataDir: z.string().optional(),
38
+ timeout: z.number().int().optional(),
39
+ captureNetwork: z.boolean().optional(),
40
+ networkLogLimit: z.number().int().optional(),
41
+ storageState: z.union([z.string(), storageStateSchema]).optional(),
42
+ storageStatePath: z.string().optional(),
43
+ saveStorageStatePath: z.string().optional(),
44
+ serverHost: z.string().optional(),
45
+ serverPort: z.number().int().optional(),
46
+ serverSessionTtlMs: z.number().int().optional(),
47
+ });
48
+
49
+ export function defineBrowserConfig<T extends BrowserCliConfig>(config: T): T {
50
+ return config;
51
+ }
52
+
53
+ export function parseBrowserConfig(input: unknown): BrowserCliConfig {
54
+ const parsed = browserCliConfigSchema.safeParse(input);
55
+ if (!parsed.success) {
56
+ throw new Error(`Invalid browser config: ${parsed.error.message}`);
57
+ }
58
+ return parsed.data;
59
+ }
package/src/context.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ export function createContext<T>(name: string) {
4
+ const storage = new AsyncLocalStorage<T>();
5
+ return {
6
+ use() {
7
+ const result = storage.getStore();
8
+ if (!result) {
9
+ throw new Error(`No context available for ${name}`);
10
+ }
11
+ return result;
12
+ },
13
+ useSafe() {
14
+ return storage.getStore();
15
+ },
16
+ with<R>(value: T, fn: () => R) {
17
+ return storage.run<R>(value, fn);
18
+ },
19
+ };
20
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ // Daemon entry point - this file is spawned as a detached process
3
+ process.env.AGENT_BROWSER_DAEMON = "1";
4
+ import "./daemon";