@ulpi/browse 0.2.4 → 0.2.5

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.2.4",
3
+ "version": "0.2.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ulpi-io/browse"
package/skill/SKILL.md CHANGED
@@ -361,6 +361,7 @@ browse har stop [path] Stop and save HAR file
361
361
  ### Server management
362
362
  ```
363
363
  browse status Server health, uptime, session count
364
+ browse instances List all running browse servers (instance, PID, port, status)
364
365
  browse stop Shutdown server
365
366
  browse restart Kill + restart server
366
367
  ```
@@ -425,12 +426,14 @@ browse restart Kill + restart server
425
426
 
426
427
  - Persistent Chromium daemon on localhost (port 9400-10400)
427
428
  - Bearer token auth per session
428
- - Auto-instance: each parent process (Claude Code) gets its own server
429
+ - One server per project directory `--session` handles agent isolation
429
430
  - Session multiplexing: multiple agents share one Chromium via isolated BrowserContexts
431
+ - For separate servers: set `BROWSE_INSTANCE` env var (e.g., fault isolation between teams)
432
+ - `browse instances` — discover all running servers (PID, port, status, session count)
430
433
  - Project-local state: `.browse/` directory at project root (auto-created, self-gitignored)
431
434
  - `sessions/{id}/` — per-session screenshots, logs, PDFs
432
435
  - `states/{name}.json` — saved browser state (cookies + localStorage)
433
- - `browse-server-{instance}.json` — server PID, port, auth token
436
+ - `browse-server.json` — server PID, port, auth token
434
437
  - Auto-shutdown when all sessions idle past 30 min
435
438
  - Chromium crash → server exits → auto-restarts on next command
436
439
  - AI-friendly error messages: Playwright errors rewritten to actionable hints
package/src/cli.ts CHANGED
@@ -22,11 +22,9 @@ const cliFlags = {
22
22
  };
23
23
 
24
24
  const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10);
25
- // Instance isolation: each parent process (e.g., Claude Code) gets its own server.
26
- // BROWSE_PORT takes precedence (explicit), then BROWSE_INSTANCE (env override), then PPID (auto).
27
- // In compiled mode ($bunfs), PPID is unstable (shell forks per invocation) — skip it.
28
- const IS_COMPILED = import.meta.dir.includes('$bunfs');
29
- const BROWSE_INSTANCE = process.env.BROWSE_INSTANCE || (BROWSE_PORT || IS_COMPILED ? '' : String(process.ppid));
25
+ // One server per project directory by default. Sessions handle agent isolation.
26
+ // For multiple servers on the same project: set BROWSE_INSTANCE or BROWSE_PORT.
27
+ const BROWSE_INSTANCE = process.env.BROWSE_INSTANCE || '';
30
28
  const INSTANCE_SUFFIX = BROWSE_PORT ? `-${BROWSE_PORT}` : (BROWSE_INSTANCE ? `-${BROWSE_INSTANCE}` : '');
31
29
 
32
30
  /**
@@ -124,6 +122,50 @@ function isProcessAlive(pid: number): boolean {
124
122
  }
125
123
  }
126
124
 
125
+ async function listInstances(): Promise<void> {
126
+ try {
127
+ const files = fs.readdirSync(LOCAL_DIR).filter(
128
+ f => f.startsWith('browse-server') && f.endsWith('.json') && !f.endsWith('.lock')
129
+ );
130
+ if (files.length === 0) { console.log('(no running instances)'); return; }
131
+
132
+ let found = false;
133
+ for (const file of files) {
134
+ try {
135
+ const data = JSON.parse(fs.readFileSync(path.join(LOCAL_DIR, file), 'utf-8'));
136
+ if (!data.pid || !data.port) continue;
137
+
138
+ const alive = isProcessAlive(data.pid);
139
+ let status = 'dead';
140
+ let sessions = 0;
141
+ if (alive) {
142
+ try {
143
+ const resp = await fetch(`http://127.0.0.1:${data.port}/health`, { signal: AbortSignal.timeout(1000) });
144
+ if (resp.ok) {
145
+ const health = await resp.json() as any;
146
+ status = health.status === 'healthy' ? 'healthy' : 'unhealthy';
147
+ sessions = health.sessions || 0;
148
+ }
149
+ } catch { status = 'unreachable'; }
150
+ }
151
+
152
+ // Derive instance name from filename
153
+ const match = file.match(/^browse-server-?(.*)\.json$/);
154
+ const instance = match?.[1] || 'default';
155
+
156
+ console.log(` ${instance.padEnd(15)} PID ${String(data.pid).padEnd(8)} port ${data.port} ${status}${sessions ? ` ${sessions} session(s)` : ''}`);
157
+ found = true;
158
+
159
+ // Clean up dead entries
160
+ if (!alive) {
161
+ try { fs.unlinkSync(path.join(LOCAL_DIR, file)); } catch {}
162
+ }
163
+ } catch {}
164
+ }
165
+ if (!found) console.log('(no running instances)');
166
+ } catch { console.log('(no running instances)'); }
167
+ }
168
+
127
169
  function isBrowseProcess(pid: number): boolean {
128
170
  try {
129
171
  const { execSync } = require('child_process');
@@ -291,9 +333,10 @@ async function ensureServer(): Promise<ServerState> {
291
333
  }
292
334
 
293
335
  /**
294
- * Clean up orphaned browse servers:
295
- * 1. Remove state files with dead PIDs
296
- * 2. Kill live servers from other instances (old PPID-suffixed state files)
336
+ * Clean up orphaned browse server state files.
337
+ * Removes any browse-server*.json whose PID is dead.
338
+ * Kills live orphans (legacy PPID-suffixed files from pre-v0.2.4) if they're browse processes.
339
+ * Preserves intentional BROWSE_PORT instances (suffix matches port inside the file).
297
340
  */
298
341
  function cleanOrphanedServers(): void {
299
342
  try {
@@ -301,27 +344,20 @@ function cleanOrphanedServers(): void {
301
344
  for (const file of files) {
302
345
  if (!file.startsWith('browse-server') || !file.endsWith('.json') || file.endsWith('.lock')) continue;
303
346
  const filePath = path.join(LOCAL_DIR, file);
304
- if (filePath === STATE_FILE) continue; // Don't touch our own state file
305
- // Only clean files with PID-based suffixes. Skip port-based and non-numeric.
306
- // Port-based files have a port number from a BROWSE_PORT env var.
307
- // PID-based files have a process ID (typically >10000, never <1000).
308
- // To distinguish: read the state file and check if the suffix matches the PID inside.
309
- const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
310
- if (!suffixMatch) continue;
311
- const suffix = parseInt(suffixMatch[1], 10);
347
+ if (filePath === STATE_FILE) continue;
312
348
  try {
313
349
  const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
314
- if (!data.pid) continue;
315
- // Port-based file: suffix matches the port inside (intentional BROWSE_PORT instance)
316
- if (data.port === suffix) continue;
317
- // PID-based file: suffix was a PPID from the spawning CLI
318
- if (isProcessAlive(data.pid) && isBrowseProcess(data.pid)) {
350
+ if (!data.pid) { fs.unlinkSync(filePath); continue; }
351
+ // Preserve intentional BROWSE_PORT instances (suffix = port number)
352
+ const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
353
+ if (suffixMatch && data.port === parseInt(suffixMatch[1], 10) && isProcessAlive(data.pid)) continue;
354
+ // Dead process remove state file
355
+ if (!isProcessAlive(data.pid)) { fs.unlinkSync(filePath); continue; }
356
+ // Live orphan (legacy PPID file) → kill if it's a browse process
357
+ if (isBrowseProcess(data.pid)) {
319
358
  try { process.kill(data.pid, 'SIGTERM'); } catch {}
320
359
  }
321
- if (!isProcessAlive(data.pid)) {
322
- fs.unlinkSync(filePath);
323
- }
324
- } catch {}
360
+ } catch { try { fs.unlinkSync(filePath); } catch {} }
325
361
  }
326
362
  } catch {}
327
363
  }
@@ -529,6 +565,11 @@ export async function main() {
529
565
  cliFlags.allowedDomains = allowedDomains || '';
530
566
 
531
567
  // ─── Local commands (no server needed) ─────────────────────
568
+ if (args[0] === 'instances') {
569
+ await listInstances();
570
+ return;
571
+ }
572
+
532
573
  if (args[0] === 'install-skill') {
533
574
  const { installSkill } = await import('./install-skill');
534
575
  installSkill(args[1]);
@@ -566,7 +607,7 @@ Sessions: sessions | session-close <id>
566
607
  Auth: auth save <name> <url> <user> <pass|--password-stdin>
567
608
  auth login <name> | auth list | auth delete <name>
568
609
  State: state save|load|list|show [name]
569
- Server: status | cookie <n>=<v> | header <n>:<v>
610
+ Server: status | instances | cookie <n>=<v> | header <n>:<v>
570
611
  useragent <str> | stop | restart
571
612
  Setup: install-skill [path]
572
613