@ulpi/browse 0.10.0 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulpi/browse",
3
- "version": "0.10.0",
3
+ "version": "1.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ulpi-io/browse"
@@ -8,20 +8,20 @@
8
8
  "dependencies": {
9
9
  "@lightpanda/browser": "^1.2.0",
10
10
  "@ulpi/browse": "^0.3.0",
11
+ "better-sqlite3": "^11.0.0",
11
12
  "diff": "^7.0.0",
12
13
  "playwright": "^1.58.2",
13
14
  "playwright-core": "^1.58.2"
14
15
  },
15
16
  "bin": {
16
- "browse": "bin/browse.ts"
17
+ "browse": "dist/browse.mjs"
17
18
  },
18
19
  "description": "Fast headless browser CLI — persistent Chromium daemon via Playwright.",
19
20
  "engines": {
20
- "bun": ">=1.0.0"
21
+ "node": ">=18.0.0"
21
22
  },
22
23
  "files": [
23
- "bin/",
24
- "src/",
24
+ "dist/",
25
25
  "skill/",
26
26
  "LICENSE",
27
27
  "README.md",
@@ -41,19 +41,23 @@
41
41
  "access": "public"
42
42
  },
43
43
  "scripts": {
44
- "build": "bun build --compile --external electron --external chromium-bidi src/cli.ts --outfile dist/browse && rm -f .*.bun-build",
44
+ "build": "esbuild src/cli.ts --bundle --format=esm --platform=node --target=node18 --outfile=dist/browse.mjs --external:playwright --external:playwright-core --external:better-sqlite3 --external:electron --external:chromium-bidi --banner:js='#!/usr/bin/env node'",
45
45
  "build:all": "bash scripts/build-all.sh",
46
- "dev": "bun run src/cli.ts",
47
- "server": "bun run src/server.ts",
48
- "test": "bun test",
49
- "start": "bun run src/server.ts",
50
- "postinstall": "bunx playwright install chromium",
51
- "benchmark": "bun run benchmark.ts"
46
+ "dev": "tsx src/cli.ts",
47
+ "server": "tsx src/server.ts",
48
+ "test": "vitest run",
49
+ "start": "tsx src/server.ts",
50
+ "postinstall": "npx playwright install chromium",
51
+ "benchmark": "tsx benchmark.ts"
52
52
  },
53
53
  "type": "module",
54
54
  "devDependencies": {
55
+ "@types/better-sqlite3": "^7.0.0",
55
56
  "@types/node": "^25.5.0",
56
- "typescript": "^5.9.3"
57
+ "esbuild": "^0.25.0",
58
+ "tsx": "^4.0.0",
59
+ "typescript": "^5.9.3",
60
+ "vitest": "^3.0.0"
57
61
  },
58
62
  "optionalDependencies": {
59
63
  "rebrowser-playwright": "^1.52.0"
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: browse
3
- version: 2.7.0
3
+ version: 2.8.0
4
4
  description: |
5
5
  Fast web browsing for AI coding agents via persistent headless Chromium daemon. Navigate to any URL,
6
6
  read page content, click elements, fill forms, run JavaScript, take screenshots,
@@ -31,8 +31,7 @@ fi
31
31
 
32
32
  If `NEEDS_INSTALL`:
33
33
  1. Tell the user: "browse needs a one-time install via npm. OK to proceed?"
34
- 2. If they approve: `bun install -g @ulpi/browse`
35
- 3. If `bun` is not installed: `curl -fsSL https://bun.sh/install | bash`
34
+ 2. If they approve: `npm install -g @ulpi/browse`
36
35
 
37
36
  ### Permissions check
38
37
 
package/bin/browse.ts DELETED
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { main } from '../src/cli';
3
-
4
- if (process.env.__BROWSE_SERVER_MODE === '1') {
5
- import('../src/server');
6
- } else {
7
- main().catch((err) => {
8
- console.error(`[browse] ${err.message}`);
9
- process.exit(1);
10
- });
11
- }
package/src/auth-vault.ts DELETED
@@ -1,196 +0,0 @@
1
- /**
2
- * Credential vault — AES-256-GCM encrypted credential storage
3
- *
4
- * Encryption key: BROWSE_ENCRYPTION_KEY env var (64-char hex)
5
- * or auto-generated at .browse/.encryption-key
6
- *
7
- * Storage: .browse/auth/<name>.json (mode 0o600)
8
- * Password never returned in list/get — only hasPassword: true
9
- */
10
-
11
- import * as fs from 'fs';
12
- import * as path from 'path';
13
- import type { BrowserManager } from './browser-manager';
14
- import { DEFAULTS } from './constants';
15
- import { resolveEncryptionKey, encrypt, decrypt } from './encryption';
16
- import { sanitizeName } from './sanitize';
17
-
18
- interface StoredCredential {
19
- name: string;
20
- url: string;
21
- username: string;
22
- encrypted: true;
23
- iv: string; // base64
24
- authTag: string; // base64
25
- data: string; // base64 (encrypted password)
26
- usernameSelector?: string;
27
- passwordSelector?: string;
28
- submitSelector?: string;
29
- createdAt: string;
30
- updatedAt: string;
31
- }
32
-
33
- export interface CredentialInfo {
34
- name: string;
35
- url: string;
36
- username: string;
37
- hasPassword: boolean;
38
- createdAt: string;
39
- }
40
-
41
- export class AuthVault {
42
- private authDir: string;
43
- private encryptionKey: Buffer;
44
-
45
- constructor(localDir: string) {
46
- this.authDir = path.join(localDir, 'auth');
47
- this.encryptionKey = resolveEncryptionKey(localDir);
48
- }
49
-
50
- save(
51
- name: string,
52
- url: string,
53
- username: string,
54
- password: string,
55
- selectors?: { username?: string; password?: string; submit?: string },
56
- ): void {
57
- fs.mkdirSync(this.authDir, { recursive: true });
58
-
59
- const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
60
- const now = new Date().toISOString();
61
-
62
- const credential: StoredCredential = {
63
- name,
64
- url,
65
- username,
66
- encrypted: true,
67
- iv,
68
- authTag,
69
- data: ciphertext,
70
- usernameSelector: selectors?.username,
71
- passwordSelector: selectors?.password,
72
- submitSelector: selectors?.submit,
73
- createdAt: now,
74
- updatedAt: now,
75
- };
76
-
77
- const filePath = path.join(this.authDir, `${sanitizeName(name)}.json`);
78
- fs.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 0o600 });
79
- }
80
-
81
- private load(name: string): StoredCredential {
82
- const filePath = path.join(this.authDir, `${sanitizeName(name)}.json`);
83
- if (!fs.existsSync(filePath)) {
84
- throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
85
- }
86
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
87
- }
88
-
89
- async login(name: string, bm: BrowserManager): Promise<string> {
90
- const cred = this.load(name);
91
- const password = decrypt(cred.data, cred.iv, cred.authTag, this.encryptionKey);
92
- const page = bm.getPage();
93
-
94
- // Navigate to login URL
95
- await page.goto(cred.url, {
96
- waitUntil: 'domcontentloaded',
97
- timeout: DEFAULTS.COMMAND_TIMEOUT_MS,
98
- });
99
-
100
- // Resolve selectors: use stored or auto-detect
101
- const userSel = cred.usernameSelector || await autoDetectSelector(page, 'username');
102
- const passSel = cred.passwordSelector || await autoDetectSelector(page, 'password');
103
- const submitSel = cred.submitSelector || await autoDetectSelector(page, 'submit');
104
-
105
- // Fill credentials
106
- await page.fill(userSel, cred.username, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
107
- await page.fill(passSel, password, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
108
-
109
- // Submit
110
- await page.click(submitSel, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
111
- await page.waitForLoadState('domcontentloaded').catch(() => {});
112
-
113
- return `Logged in as ${cred.username} at ${page.url()}`;
114
- }
115
-
116
- list(): CredentialInfo[] {
117
- if (!fs.existsSync(this.authDir)) return [];
118
-
119
- const files = fs.readdirSync(this.authDir).filter(f => f.endsWith('.json'));
120
- return files.map(f => {
121
- try {
122
- const data = JSON.parse(fs.readFileSync(path.join(this.authDir, f), 'utf-8'));
123
- return {
124
- name: data.name,
125
- url: data.url,
126
- username: data.username,
127
- hasPassword: true,
128
- createdAt: data.createdAt,
129
- };
130
- } catch {
131
- return null;
132
- }
133
- }).filter(Boolean) as CredentialInfo[];
134
- }
135
-
136
- delete(name: string): void {
137
- const filePath = path.join(this.authDir, `${sanitizeName(name)}.json`);
138
- if (!fs.existsSync(filePath)) {
139
- throw new Error(`Credential "${name}" not found.`);
140
- }
141
- fs.unlinkSync(filePath);
142
- }
143
- }
144
-
145
- /**
146
- * Auto-detect login form selectors by common patterns.
147
- */
148
- async function autoDetectSelector(page: any, field: 'username' | 'password' | 'submit'): Promise<string> {
149
- if (field === 'username') {
150
- const candidates = [
151
- 'input[type="email"]',
152
- 'input[name="email"]',
153
- 'input[name="username"]',
154
- 'input[name="user"]',
155
- 'input[name="login"]',
156
- 'input[autocomplete="username"]',
157
- 'input[autocomplete="email"]',
158
- 'input[type="text"]:first-of-type',
159
- ];
160
- for (const sel of candidates) {
161
- const count = await page.locator(sel).count();
162
- if (count > 0) return sel;
163
- }
164
- throw new Error('Could not auto-detect username field. Save with explicit selectors: browse auth save <name> <url> <user> <pass> --user-sel <sel> --pass-sel <sel> --submit-sel <sel>');
165
- }
166
-
167
- if (field === 'password') {
168
- const candidates = [
169
- 'input[type="password"]',
170
- 'input[name="password"]',
171
- 'input[name="pass"]',
172
- 'input[autocomplete="current-password"]',
173
- ];
174
- for (const sel of candidates) {
175
- const count = await page.locator(sel).count();
176
- if (count > 0) return sel;
177
- }
178
- throw new Error('Could not auto-detect password field.');
179
- }
180
-
181
- // submit
182
- const candidates = [
183
- 'button[type="submit"]',
184
- 'input[type="submit"]',
185
- 'form button',
186
- 'button:has-text("Log in")',
187
- 'button:has-text("Sign in")',
188
- 'button:has-text("Login")',
189
- 'button:has-text("Submit")',
190
- ];
191
- for (const sel of candidates) {
192
- const count = await page.locator(sel).count();
193
- if (count > 0) return sel;
194
- }
195
- throw new Error('Could not auto-detect submit button.');
196
- }