hyper-agent-browser 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.
package/src/cli.ts ADDED
@@ -0,0 +1,628 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ import * as configCommands from "./commands/config";
4
+ import { DaemonClient } from "./daemon/client";
5
+ import type { CommandRequest } from "./daemon/server";
6
+ import { getExitCode } from "./utils/errors";
7
+
8
+ const program = new Command();
9
+ const daemonClient = new DaemonClient();
10
+
11
+ program
12
+ .name("hab")
13
+ .description("hyper-agent-browser - Browser automation CLI for AI Agents")
14
+ .version("0.2.0");
15
+
16
+ // Global options
17
+ program
18
+ .option("-s, --session <name>", "Session name", "default")
19
+ .option("-H, --headed", "Show browser window", false)
20
+ .option("-c, --channel <browser>", "Browser channel (chrome/msedge/chromium)", "chrome")
21
+ .option("-t, --timeout <ms>", "Timeout in milliseconds", "30000")
22
+ .option("-v, --verbose", "Verbose output", false);
23
+
24
+ // Helper to execute command via daemon
25
+ async function executeViaDaemon(command: string, args: Record<string, any>, parentCommand: any) {
26
+ const request: CommandRequest = {
27
+ command,
28
+ session: getSessionName(parentCommand),
29
+ args,
30
+ options: {
31
+ headed: parentCommand.opts().headed || false,
32
+ timeout: getTimeout(parentCommand),
33
+ channel: getChannel(parentCommand),
34
+ },
35
+ };
36
+
37
+ try {
38
+ const response = await daemonClient.execute(request);
39
+
40
+ if (!response.success) {
41
+ console.error("Error:", response.error);
42
+ process.exit(1);
43
+ }
44
+
45
+ return response.data;
46
+ } catch (error) {
47
+ console.error("Error:", error instanceof Error ? error.message : String(error));
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ // Navigation commands
53
+ program
54
+ .command("open <url>")
55
+ .description("Open a URL")
56
+ .option("--wait-until <state>", "Wait until state (load/domcontentloaded/networkidle)", "load")
57
+ .action(async (url, options, command) => {
58
+ const result = await executeViaDaemon(
59
+ "open",
60
+ { url, waitUntil: options.waitUntil },
61
+ command.parent,
62
+ );
63
+ console.log(result);
64
+ });
65
+
66
+ program
67
+ .command("reload")
68
+ .description("Reload current page")
69
+ .action(async (_options, command) => {
70
+ const result = await executeViaDaemon("reload", {}, command.parent);
71
+ console.log(result);
72
+ });
73
+
74
+ program
75
+ .command("back")
76
+ .description("Go back in history")
77
+ .action(async (_options, command) => {
78
+ const result = await executeViaDaemon("back", {}, command.parent);
79
+ console.log(result);
80
+ });
81
+
82
+ program
83
+ .command("forward")
84
+ .description("Go forward in history")
85
+ .action(async (_options, command) => {
86
+ const result = await executeViaDaemon("forward", {}, command.parent);
87
+ console.log(result);
88
+ });
89
+
90
+ // Action commands
91
+ program
92
+ .command("click <selector>")
93
+ .description("Click an element")
94
+ .action(async (selector, _options, command) => {
95
+ const result = await executeViaDaemon("click", { selector }, command.parent);
96
+ console.log(result);
97
+ });
98
+
99
+ program
100
+ .command("fill <selector> <value>")
101
+ .description("Fill an input field")
102
+ .action(async (selector, value, _options, command) => {
103
+ const result = await executeViaDaemon("fill", { selector, value }, command.parent);
104
+ console.log(result);
105
+ });
106
+
107
+ program
108
+ .command("type <selector> <text>")
109
+ .description("Type text into an element")
110
+ .option("--delay <ms>", "Delay between keystrokes", "0")
111
+ .action(async (selector, text, options, command) => {
112
+ const result = await executeViaDaemon(
113
+ "type",
114
+ { selector, text, delay: Number.parseInt(options.delay) },
115
+ command.parent,
116
+ );
117
+ console.log(result);
118
+ });
119
+
120
+ program
121
+ .command("press <key>")
122
+ .description("Press a key")
123
+ .action(async (key, _options, command) => {
124
+ const result = await executeViaDaemon("press", { key }, command.parent);
125
+ console.log(result);
126
+ });
127
+
128
+ program
129
+ .command("scroll <direction>")
130
+ .description("Scroll page (up/down/left/right)")
131
+ .option("--amount <pixels>", "Scroll amount in pixels", "500")
132
+ .option("--selector <selector>", "Scroll within element")
133
+ .action(async (direction, options, command) => {
134
+ const result = await executeViaDaemon(
135
+ "scroll",
136
+ {
137
+ direction,
138
+ amount: Number.parseInt(options.amount),
139
+ selector: options.selector,
140
+ },
141
+ command.parent,
142
+ );
143
+ console.log(result);
144
+ });
145
+
146
+ program
147
+ .command("hover <selector>")
148
+ .description("Hover over an element")
149
+ .action(async (selector, _options, command) => {
150
+ const result = await executeViaDaemon("hover", { selector }, command.parent);
151
+ console.log(result);
152
+ });
153
+
154
+ program
155
+ .command("select <selector> <value>")
156
+ .description("Select option from dropdown")
157
+ .action(async (selector, value, _options, command) => {
158
+ const result = await executeViaDaemon("select", { selector, value }, command.parent);
159
+ console.log(result);
160
+ });
161
+
162
+ program
163
+ .command("wait [condition]")
164
+ .description("Wait for condition (ms/selector=/hidden=/navigation)")
165
+ .option("--timeout <ms>", "Timeout in milliseconds")
166
+ .option("--text <text>", "Wait for text to appear")
167
+ .option("--url <pattern>", "Wait for URL pattern")
168
+ .option("--fn <function>", "Wait for JavaScript condition")
169
+ .option("--load <state>", "Wait for load state (load/domcontentloaded/networkidle)")
170
+ .action(async (condition, options, command) => {
171
+ const result = await executeViaDaemon(
172
+ "wait",
173
+ {
174
+ condition: condition || "",
175
+ timeout: options.timeout,
176
+ text: options.text,
177
+ url: options.url,
178
+ fn: options.fn,
179
+ load: options.load,
180
+ },
181
+ command.parent,
182
+ );
183
+ console.log(result);
184
+ });
185
+
186
+ // 第一批新增 Action 命令
187
+ program
188
+ .command("check <selector>")
189
+ .description("Check checkbox")
190
+ .action(async (selector, _options, command) => {
191
+ const result = await executeViaDaemon("check", { selector }, command.parent);
192
+ console.log(result);
193
+ });
194
+
195
+ program
196
+ .command("uncheck <selector>")
197
+ .description("Uncheck checkbox")
198
+ .action(async (selector, _options, command) => {
199
+ const result = await executeViaDaemon("uncheck", { selector }, command.parent);
200
+ console.log(result);
201
+ });
202
+
203
+ program
204
+ .command("dblclick <selector>")
205
+ .description("Double-click element")
206
+ .action(async (selector, _options, command) => {
207
+ const result = await executeViaDaemon("dblclick", { selector }, command.parent);
208
+ console.log(result);
209
+ });
210
+
211
+ program
212
+ .command("focus <selector>")
213
+ .description("Focus element")
214
+ .action(async (selector, _options, command) => {
215
+ const result = await executeViaDaemon("focus", { selector }, command.parent);
216
+ console.log(result);
217
+ });
218
+
219
+ program
220
+ .command("upload <selector> <files...>")
221
+ .description("Upload files")
222
+ .action(async (selector, files, _options, command) => {
223
+ const result = await executeViaDaemon("upload", { selector, files }, command.parent);
224
+ console.log(result);
225
+ });
226
+
227
+ program
228
+ .command("scrollintoview <selector>")
229
+ .description("Scroll element into view")
230
+ .action(async (selector, _options, command) => {
231
+ const result = await executeViaDaemon("scrollintoview", { selector }, command.parent);
232
+ console.log(result);
233
+ });
234
+
235
+ program
236
+ .command("drag <source> <target>")
237
+ .description("Drag and drop")
238
+ .action(async (source, target, _options, command) => {
239
+ const result = await executeViaDaemon("drag", { source, target }, command.parent);
240
+ console.log(result);
241
+ });
242
+
243
+ // Get 系列命令
244
+ program
245
+ .command("get <subcommand> <selector> [attribute]")
246
+ .description("Get element info (text/value/attr/html/count/box)")
247
+ .action(async (subcommand, selector, attribute, _options, command) => {
248
+ const result = await executeViaDaemon(
249
+ "get",
250
+ { subcommand, selector, attribute },
251
+ command.parent,
252
+ );
253
+ console.log(result);
254
+ });
255
+
256
+ // Is 系列命令
257
+ program
258
+ .command("is <subcommand> <selector>")
259
+ .description("Check element state (visible/enabled/checked/editable/hidden)")
260
+ .action(async (subcommand, selector, _options, command) => {
261
+ const result = await executeViaDaemon("is", { subcommand, selector }, command.parent);
262
+ console.log(result);
263
+ });
264
+
265
+ // 第二批新增高级命令
266
+
267
+ // Dialog 命令
268
+ program
269
+ .command("dialog <action> [text]")
270
+ .description("Handle dialogs (accept/dismiss)")
271
+ .action(async (action, text, _options, command) => {
272
+ const result = await executeViaDaemon("dialog", { action, text }, command.parent);
273
+ console.log(result);
274
+ });
275
+
276
+ // Cookie 命令
277
+ program
278
+ .command("cookies [action] [name] [value]")
279
+ .description("Manage cookies (get/set/clear)")
280
+ .action(async (action, name, value, _options, command) => {
281
+ const result = await executeViaDaemon("cookies", { action, name, value }, command.parent);
282
+ if (typeof result === "object") {
283
+ console.log(JSON.stringify(result, null, 2));
284
+ } else {
285
+ console.log(result);
286
+ }
287
+ });
288
+
289
+ // Storage 命令
290
+ program
291
+ .command("storage <type> [action] [key] [value]")
292
+ .description("Manage storage (local/session)")
293
+ .action(async (type, action, key, value, _options, command) => {
294
+ const result = await executeViaDaemon(
295
+ "storage",
296
+ {
297
+ storageType: type,
298
+ action,
299
+ key,
300
+ value,
301
+ },
302
+ command.parent,
303
+ );
304
+ if (typeof result === "object") {
305
+ console.log(JSON.stringify(result, null, 2));
306
+ } else {
307
+ console.log(result);
308
+ }
309
+ });
310
+
311
+ // PDF 导出
312
+ program
313
+ .command("pdf <path>")
314
+ .description("Save page as PDF")
315
+ .option("--format <format>", "Paper format (A4/Letter/etc)", "A4")
316
+ .option("--landscape", "Landscape orientation")
317
+ .option("--print-background", "Print background graphics", true)
318
+ .action(async (path, options, command) => {
319
+ const result = await executeViaDaemon(
320
+ "pdf",
321
+ {
322
+ path,
323
+ format: options.format,
324
+ landscape: options.landscape,
325
+ printBackground: options.printBackground,
326
+ },
327
+ command.parent,
328
+ );
329
+ console.log(result);
330
+ });
331
+
332
+ // Set 系列命令
333
+ program
334
+ .command("set <subcommand> [args...]")
335
+ .description("Set browser settings (viewport/geo/offline/headers/media)")
336
+ .action(async (subcommand, args, _options, command) => {
337
+ const params: any = { subcommand };
338
+
339
+ switch (subcommand) {
340
+ case "viewport":
341
+ params.width = Number.parseInt(args[0]);
342
+ params.height = Number.parseInt(args[1]);
343
+ break;
344
+ case "geo":
345
+ params.latitude = Number.parseFloat(args[0]);
346
+ params.longitude = Number.parseFloat(args[1]);
347
+ break;
348
+ case "offline":
349
+ params.enabled = args[0] === "on" || args[0] === "true";
350
+ break;
351
+ case "headers":
352
+ params.headers = JSON.parse(args[0]);
353
+ break;
354
+ case "media":
355
+ params.scheme = args[0];
356
+ break;
357
+ }
358
+
359
+ const result = await executeViaDaemon("set", params, command.parent);
360
+ console.log(result);
361
+ });
362
+
363
+ // Mouse 命令
364
+ program
365
+ .command("mouse <action> [args...]")
366
+ .description("Mouse control (move/down/up/wheel)")
367
+ .action(async (action, args, _options, command) => {
368
+ const params: any = { action };
369
+
370
+ switch (action) {
371
+ case "move":
372
+ params.x = Number.parseInt(args[0]);
373
+ params.y = Number.parseInt(args[1]);
374
+ break;
375
+ case "down":
376
+ case "up":
377
+ params.button = args[0] || "left";
378
+ break;
379
+ case "wheel":
380
+ params.deltaY = Number.parseInt(args[0]);
381
+ params.deltaX = args[1] ? Number.parseInt(args[1]) : 0;
382
+ break;
383
+ }
384
+
385
+ const result = await executeViaDaemon("mouse", params, command.parent);
386
+ console.log(result);
387
+ });
388
+
389
+ // Keyboard 命令
390
+ program
391
+ .command("keydown <key>")
392
+ .description("Press and hold key")
393
+ .action(async (key, _options, command) => {
394
+ const result = await executeViaDaemon("keydown", { key }, command.parent);
395
+ console.log(result);
396
+ });
397
+
398
+ program
399
+ .command("keyup <key>")
400
+ .description("Release key")
401
+ .action(async (key, _options, command) => {
402
+ const result = await executeViaDaemon("keyup", { key }, command.parent);
403
+ console.log(result);
404
+ });
405
+
406
+ // Debug 命令
407
+ program
408
+ .command("console [action]")
409
+ .description("View/clear console logs")
410
+ .action(async (action, _options, command) => {
411
+ const result = await executeViaDaemon("console", { action }, command.parent);
412
+ if (Array.isArray(result)) {
413
+ console.log(result.join("\n"));
414
+ } else {
415
+ console.log(result);
416
+ }
417
+ });
418
+
419
+ program
420
+ .command("errors [action]")
421
+ .description("View/clear page errors")
422
+ .action(async (action, _options, command) => {
423
+ const result = await executeViaDaemon("errors", { action }, command.parent);
424
+ if (Array.isArray(result)) {
425
+ console.log(result.join("\n"));
426
+ } else {
427
+ console.log(result);
428
+ }
429
+ });
430
+
431
+ program
432
+ .command("highlight <selector>")
433
+ .description("Highlight element on page")
434
+ .action(async (selector, _options, command) => {
435
+ const result = await executeViaDaemon("highlight", { selector }, command.parent);
436
+ console.log(result);
437
+ });
438
+
439
+ // Info commands
440
+ program
441
+ .command("snapshot")
442
+ .description("Get page snapshot")
443
+ .option("-i, --interactive", "Show only interactive elements")
444
+ .option("-f, --full", "Show all elements")
445
+ .option("-r, --raw", "Output raw JSON")
446
+ .option("-o, --output <file>", "Output to file")
447
+ .action(async (options, command) => {
448
+ const result = await executeViaDaemon(
449
+ "snapshot",
450
+ {
451
+ interactive: options.interactive,
452
+ full: options.full,
453
+ raw: options.raw,
454
+ },
455
+ command.parent,
456
+ );
457
+
458
+ if (options.output) {
459
+ await Bun.write(options.output, result);
460
+ console.log(`Snapshot saved to: ${options.output}`);
461
+ } else {
462
+ console.log(result);
463
+ }
464
+ });
465
+
466
+ program
467
+ .command("screenshot")
468
+ .description("Take a screenshot")
469
+ .option("-o, --output <file>", "Output file", "screenshot.png")
470
+ .option("--full-page", "Capture full page")
471
+ .option("--selector <selector>", "Capture specific element")
472
+ .action(async (options, command) => {
473
+ const result = await executeViaDaemon(
474
+ "screenshot",
475
+ {
476
+ output: options.output,
477
+ fullPage: options.fullPage,
478
+ selector: options.selector,
479
+ },
480
+ command.parent,
481
+ );
482
+ console.log(result);
483
+ });
484
+
485
+ program
486
+ .command("evaluate <script>")
487
+ .description("Execute JavaScript in page context")
488
+ .action(async (script, _options, command) => {
489
+ const result = await executeViaDaemon("evaluate", { script }, command.parent);
490
+ console.log(result);
491
+ });
492
+
493
+ program
494
+ .command("url")
495
+ .description("Get current URL")
496
+ .action(async (_options, command) => {
497
+ const result = await executeViaDaemon("url", {}, command.parent);
498
+ console.log(result);
499
+ });
500
+
501
+ program
502
+ .command("title")
503
+ .description("Get page title")
504
+ .action(async (_options, command) => {
505
+ const result = await executeViaDaemon("title", {}, command.parent);
506
+ console.log(result);
507
+ });
508
+
509
+ // Session commands
510
+ program
511
+ .command("sessions")
512
+ .description("List all sessions")
513
+ .action(async () => {
514
+ try {
515
+ const sessions = await daemonClient.listSessions();
516
+ console.log("Active Sessions:");
517
+ if (sessions.length === 0) {
518
+ console.log(" No active sessions");
519
+ } else {
520
+ for (const session of sessions) {
521
+ console.log(` - ${session.name} (${session.status})`);
522
+ }
523
+ }
524
+ } catch (error) {
525
+ handleError(error);
526
+ }
527
+ });
528
+
529
+ program
530
+ .command("close")
531
+ .description("Close current session")
532
+ .action(async (_options, command) => {
533
+ try {
534
+ const sessionName = getSessionName(command.parent);
535
+ const closed = await daemonClient.closeSession(sessionName);
536
+
537
+ if (closed) {
538
+ console.log(`Session closed: ${sessionName}`);
539
+ } else {
540
+ console.log(`Session not found: ${sessionName}`);
541
+ }
542
+ } catch (error) {
543
+ handleError(error);
544
+ }
545
+ });
546
+
547
+ // Daemon management commands
548
+ program
549
+ .command("daemon <action>")
550
+ .description("Manage daemon (start/stop/status/restart)")
551
+ .action(async (action) => {
552
+ try {
553
+ const { spawnSync } = await import("node:child_process");
554
+ const { join } = await import("node:path");
555
+
556
+ // Get the project root directory
557
+ const projectRoot = join(import.meta.dir, "..");
558
+ const daemonScript = join(projectRoot, "src/daemon/main.ts");
559
+
560
+ const result = spawnSync("bun", [daemonScript, action], {
561
+ stdio: "inherit",
562
+ cwd: projectRoot,
563
+ });
564
+
565
+ process.exit(result.status || 0);
566
+ } catch (error) {
567
+ console.error("Failed to execute daemon command:", error);
568
+ process.exit(1);
569
+ }
570
+ });
571
+
572
+ // Config commands
573
+ program
574
+ .command("config <action> [key] [value]")
575
+ .description("Manage configuration (list/get/set)")
576
+ .action(async (action, key, value) => {
577
+ try {
578
+ if (action === "list") {
579
+ const result = await configCommands.listConfig();
580
+ console.log(result);
581
+ } else if (action === "get" && key) {
582
+ const result = await configCommands.getConfig(key);
583
+ console.log(result);
584
+ } else if (action === "set" && key && value) {
585
+ const result = await configCommands.setConfig(key, value);
586
+ console.log(result);
587
+ } else {
588
+ throw new Error(`Unknown config action: ${action}`);
589
+ }
590
+ } catch (error) {
591
+ handleError(error);
592
+ }
593
+ });
594
+
595
+ // Version command
596
+ program
597
+ .command("version")
598
+ .description("Show version information")
599
+ .action(() => {
600
+ console.log("hyper-agent-browser v0.2.0 (with daemon architecture)");
601
+ console.log(`Bun v${Bun.version}`);
602
+ console.log("Patchright v1.55.1");
603
+ });
604
+
605
+ // Helper functions
606
+ function getSessionName(parentCommand: any): string {
607
+ return parentCommand.opts().session || "default";
608
+ }
609
+
610
+ function getChannel(parentCommand: any): "chrome" | "msedge" | "chromium" {
611
+ return parentCommand.opts().channel || "chrome";
612
+ }
613
+
614
+ function getTimeout(parentCommand: any): number {
615
+ return Number.parseInt(parentCommand.opts().timeout || "30000");
616
+ }
617
+
618
+ function handleError(error: any) {
619
+ if (error instanceof Error) {
620
+ console.error("Error:", error.message);
621
+ process.exit(getExitCode(error));
622
+ } else {
623
+ console.error("Error:", error);
624
+ process.exit(1);
625
+ }
626
+ }
627
+
628
+ program.parse();