@ulpi/browse 0.3.0 → 0.4.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Ciprian Hacman
3
+ Copyright (c) 2026 Open Growth Group INC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -116,6 +116,7 @@ Every detected element gets a ref. `browse click @e3` just works.
116
116
  | State persistence | Not available | `state save\|load` |
117
117
  | Credential vault | Not available | `auth save\|login\|list` |
118
118
  | HAR recording | Not available | `har start\|stop` |
119
+ | Video recording | Not available | `video start [dir]\|stop\|status` |
119
120
  | Clipboard access | Not available | `clipboard [write <text>]` |
120
121
  | Element finding | Not available | `find role\|text\|label\|placeholder\|testid` |
121
122
  | DevTools inspect | Not available | `inspect` |
@@ -291,7 +292,7 @@ echo '[["goto","https://example.com"],["text"]]' | browse chain
291
292
  `state save [name]` | `state load [name]` | `state list` | `state show [name]` | `auth save <name> <url> <user> <pass>` | `auth login <name>` | `auth list` | `auth delete <name>`
292
293
 
293
294
  ### Recording
294
- `har start` | `har stop [path]`
295
+ `har start` | `har stop [path]` | `video start [dir]` | `video stop` | `video status`
295
296
 
296
297
  ### Debug
297
298
  `inspect` — open DevTools debugger (requires `BROWSE_DEBUG_PORT`).
@@ -384,6 +385,7 @@ Inspired by and originally derived from the `/browse` skill in [gstack](https://
384
385
  - `offline on/off` — offline mode toggle
385
386
  - `state save/load` — persist and restore cookies + localStorage (all origins)
386
387
  - `har start/stop` — HAR recording and export
388
+ - `video start/stop/status` — video recording (WebM, compositor-level, works with remote CDP)
387
389
  - `screenshot-diff` — pixel-level visual regression testing
388
390
  - `find role/text/label/placeholder/testid` — semantic element locators
389
391
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@ulpi/browse",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ulpi-io/browse"
7
7
  },
8
8
  "dependencies": {
9
+ "@ulpi/browse": "^0.3.0",
9
10
  "diff": "^7.0.0",
10
11
  "playwright": "^1.58.2"
11
12
  },
@@ -38,7 +39,7 @@
38
39
  "access": "public"
39
40
  },
40
41
  "scripts": {
41
- "build": "bun build --compile --external electron --external chromium-bidi src/cli.ts --outfile dist/browse",
42
+ "build": "bun build --compile --external electron --external chromium-bidi src/cli.ts --outfile dist/browse && rm -f .*.bun-build",
42
43
  "build:all": "bash scripts/build-all.sh",
43
44
  "dev": "bun run src/cli.ts",
44
45
  "server": "bun run src/server.ts",
@@ -167,6 +167,9 @@ export class BrowserManager {
167
167
  // ─── HAR Recording ────────────────────────────────────────
168
168
  private harRecording: HarRecording | null = null;
169
169
 
170
+ // ─── Video Recording ────────────────────────────────────────
171
+ private videoRecording: { dir: string; startedAt: number } | null = null;
172
+
170
173
  // ─── Init Script (domain filter JS injection) ─────────────
171
174
  private initScript: string | null = null;
172
175
 
@@ -553,6 +556,17 @@ export class BrowserManager {
553
556
  private async recreateContext(contextOptions: Record<string, any>): Promise<void> {
554
557
  if (!this.browser) return;
555
558
 
559
+ // Auto-inject recordVideo when video recording is active (so emulateDevice/applyUserAgent pass it through)
560
+ if (this.videoRecording && !contextOptions.recordVideo) {
561
+ contextOptions = {
562
+ ...contextOptions,
563
+ recordVideo: {
564
+ dir: this.videoRecording.dir,
565
+ size: contextOptions.viewport || this.currentDevice?.viewport || { width: 1920, height: 1080 },
566
+ },
567
+ };
568
+ }
569
+
556
570
  // Save all tab URLs and which tab was active
557
571
  const tabUrls: Array<{ id: number; url: string; active: boolean }> = [];
558
572
  for (const [id, page] of this.pages) {
@@ -766,6 +780,67 @@ export class BrowserManager {
766
780
  return this.harRecording;
767
781
  }
768
782
 
783
+ // ─── Video Recording ──────────────────────────────────────
784
+
785
+ async startVideoRecording(dir: string): Promise<void> {
786
+ if (this.videoRecording) throw new Error('Video recording already active');
787
+ const fs = await import('fs');
788
+ fs.mkdirSync(dir, { recursive: true });
789
+
790
+ this.videoRecording = { dir, startedAt: Date.now() };
791
+ const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
792
+ await this.recreateContext({
793
+ viewport,
794
+ ...(this.customUserAgent ? { userAgent: this.customUserAgent } : {}),
795
+ ...(this.currentDevice ? {
796
+ deviceScaleFactor: this.currentDevice.deviceScaleFactor,
797
+ isMobile: this.currentDevice.isMobile,
798
+ hasTouch: this.currentDevice.hasTouch,
799
+ } : {}),
800
+ recordVideo: { dir, size: viewport },
801
+ });
802
+ }
803
+
804
+ async stopVideoRecording(): Promise<{ dir: string; startedAt: number; paths: string[] } | null> {
805
+ if (!this.videoRecording) return null;
806
+
807
+ const recording = this.videoRecording;
808
+ // Collect video objects before pages are closed by recreateContext
809
+ const videos: Array<{ video: any; tabId: number }> = [];
810
+ for (const [id, page] of this.pages) {
811
+ const video = page.video();
812
+ if (video) videos.push({ video, tabId: id });
813
+ }
814
+
815
+ // Clear state BEFORE recreateContext so auto-injection doesn't add recordVideo
816
+ this.videoRecording = null;
817
+
818
+ const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
819
+ await this.recreateContext({
820
+ viewport,
821
+ ...(this.customUserAgent ? { userAgent: this.customUserAgent } : {}),
822
+ ...(this.currentDevice ? {
823
+ deviceScaleFactor: this.currentDevice.deviceScaleFactor,
824
+ isMobile: this.currentDevice.isMobile,
825
+ hasTouch: this.currentDevice.hasTouch,
826
+ } : {}),
827
+ });
828
+
829
+ // Save videos with predictable names (saveAs works for both local and remote CDP)
830
+ const paths: string[] = [];
831
+ for (const { video, tabId } of videos) {
832
+ const target = `${recording.dir}/tab-${tabId}.webm`;
833
+ await video.saveAs(target);
834
+ paths.push(target);
835
+ }
836
+
837
+ return { dir: recording.dir, startedAt: recording.startedAt, paths };
838
+ }
839
+
840
+ getVideoRecording(): { dir: string; startedAt: number } | null {
841
+ return this.videoRecording;
842
+ }
843
+
769
844
  // ─── Init Script ───────────────────────────────────────────
770
845
  setInitScript(script: string): void {
771
846
  this.initScript = script;
package/src/cli.ts CHANGED
@@ -612,6 +612,7 @@ Compare: diff <url1> <url2> | screenshot-diff <baseline> [current]
612
612
  Multi-step: chain (reads JSON from stdin)
613
613
  Network: offline [on|off] | route <pattern> block|fulfill
614
614
  Recording: har start | har stop [path]
615
+ video start [dir] | video stop | video status
615
616
  Tabs: tabs | tab <id> | newtab [url] | closetab [id]
616
617
  Frames: frame <sel> | frame main
617
618
  Sessions: sessions | session-close <id>
@@ -619,6 +619,36 @@ export async function handleMetaCommand(
619
619
  throw new Error('Usage: browse har start | browse har stop [path]');
620
620
  }
621
621
 
622
+ // ─── Video Recording ─────────────────────────────────
623
+ case 'video': {
624
+ const subcommand = args[0];
625
+ if (!subcommand) throw new Error('Usage: browse video start [dir] | browse video stop | browse video status');
626
+
627
+ if (subcommand === 'start') {
628
+ const dir = args[1] || (currentSession
629
+ ? `${currentSession.outputDir}`
630
+ : `${LOCAL_DIR}`);
631
+ await bm.startVideoRecording(dir);
632
+ return `Video recording started — output dir: ${dir}`;
633
+ }
634
+
635
+ if (subcommand === 'stop') {
636
+ const result = await bm.stopVideoRecording();
637
+ if (!result) throw new Error('No active video recording. Run "browse video start" first.');
638
+ const duration = ((Date.now() - result.startedAt) / 1000).toFixed(1);
639
+ return `Video saved: ${result.paths.join(', ')} (${duration}s)`;
640
+ }
641
+
642
+ if (subcommand === 'status') {
643
+ const recording = bm.getVideoRecording();
644
+ if (!recording) return 'No active video recording';
645
+ const duration = ((Date.now() - recording.startedAt) / 1000).toFixed(1);
646
+ return `Video recording active — dir: ${recording.dir}, duration: ${duration}s`;
647
+ }
648
+
649
+ throw new Error('Usage: browse video start [dir] | browse video stop | browse video status');
650
+ }
651
+
622
652
  // ─── Semantic Locator ──────────────────────────────
623
653
  case 'find': {
624
654
  const root = bm.getLocatorRoot();
package/src/server.ts CHANGED
@@ -129,7 +129,7 @@ const META_COMMANDS = new Set([
129
129
  'url', 'snapshot', 'snapshot-diff', 'screenshot-diff',
130
130
  'sessions', 'session-close',
131
131
  'frame', 'state', 'find',
132
- 'auth', 'har', 'inspect',
132
+ 'auth', 'har', 'video', 'inspect',
133
133
  ]);
134
134
 
135
135
  // Probe if a port is free using net.createServer (not Bun.serve which fatally crashes on EADDRINUSE)