@phi-code-admin/browser 1.0.0 → 1.0.2

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/dist/index.d.ts CHANGED
@@ -30,11 +30,21 @@ export declare function closeAll(): Promise<void>;
30
30
  export interface CreateTabResult {
31
31
  tabId: string;
32
32
  userId: string;
33
+ sessionKey: string;
33
34
  url?: string;
34
35
  }
35
- /** Open a new browser tab. Returns the tab id used by the other tools. */
36
- export declare function createTab(options: {
36
+ /**
37
+ * Open a new browser tab. Returns the tab id used by the other tools.
38
+ *
39
+ * The camofox-browser REST contract requires every tab to be associated
40
+ * with a `userId` (logical user) AND a `sessionKey` (logical session
41
+ * inside that user — used to group tabs that should share cookies /
42
+ * fingerprints / proxies). Phi-code's chat agents only need one of each,
43
+ * so both default to a constant sentinel when omitted.
44
+ */
45
+ export declare function createTab(options?: {
37
46
  userId?: string;
47
+ sessionKey?: string;
38
48
  url?: string;
39
49
  viewport?: {
40
50
  width: number;
@@ -56,6 +66,7 @@ export declare function navigate(options: {
56
66
  url: string;
57
67
  tabId?: string;
58
68
  userId?: string;
69
+ sessionKey?: string;
59
70
  waitUntil?: "load" | "domcontentloaded" | "networkidle";
60
71
  timeoutMs?: number;
61
72
  }): Promise<NavigateResult>;
@@ -65,6 +76,7 @@ export declare function navigate(options: {
65
76
  */
66
77
  export declare function snapshot(options: {
67
78
  tabId: string;
79
+ userId?: string;
68
80
  }): Promise<unknown>;
69
81
  export interface ExtractResult {
70
82
  url?: string;
@@ -82,6 +94,7 @@ export interface ExtractResult {
82
94
  export declare function extract(options: {
83
95
  tabId?: string;
84
96
  userId?: string;
97
+ sessionKey?: string;
85
98
  url?: string;
86
99
  mode?: "readability" | "html" | "text";
87
100
  }): Promise<ExtractResult>;
@@ -93,6 +106,7 @@ export interface ScreenshotResult {
93
106
  /** Capture a screenshot of the given tab as a base64-encoded PNG. */
94
107
  export declare function screenshot(options: {
95
108
  tabId: string;
109
+ userId?: string;
96
110
  fullPage?: boolean;
97
111
  clip?: {
98
112
  x: number;
@@ -110,10 +124,12 @@ export declare function search(options: {
110
124
  query: string;
111
125
  engine?: "google" | "duckduckgo" | "bing";
112
126
  userId?: string;
127
+ sessionKey?: string;
113
128
  }): Promise<ExtractResult>;
114
129
  /** Click an element by ref (from `snapshot`) or CSS selector. */
115
130
  export declare function click(options: {
116
131
  tabId: string;
132
+ userId?: string;
117
133
  ref?: string;
118
134
  selector?: string;
119
135
  button?: "left" | "right" | "middle";
@@ -123,6 +139,7 @@ export declare function click(options: {
123
139
  /** Type text into a focused element (or one targeted via ref/selector). */
124
140
  export declare function type(options: {
125
141
  tabId: string;
142
+ userId?: string;
126
143
  text: string;
127
144
  ref?: string;
128
145
  selector?: string;
@@ -134,6 +151,7 @@ export declare function type(options: {
134
151
  /** Scroll the page or a specific element by ref. */
135
152
  export declare function scroll(options: {
136
153
  tabId: string;
154
+ userId?: string;
137
155
  direction: "up" | "down" | "left" | "right";
138
156
  ref?: string;
139
157
  pixels?: number;
@@ -143,6 +161,7 @@ export declare function scroll(options: {
143
161
  /** Close a single tab. The underlying browser context is kept warm. */
144
162
  export declare function closeTab(options: {
145
163
  tabId: string;
164
+ userId?: string;
146
165
  }): Promise<{
147
166
  tabId: string;
148
167
  }>;
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ let serverProcess = null;
26
26
  let serverPort = null;
27
27
  let bootPromise = null;
28
28
  const DEFAULT_USER_ID = "phi-default";
29
+ const DEFAULT_SESSION_KEY = "phi-default-session";
29
30
  const HEALTH_TIMEOUT_MS = 30_000;
30
31
  const HEALTH_POLL_INTERVAL_MS = 250;
31
32
  async function findAvailablePort() {
@@ -92,24 +93,58 @@ export async function ensureServer() {
92
93
  stdio: ["ignore", "pipe", "pipe"],
93
94
  detached: false,
94
95
  });
95
- // Surface crashes during boot, but never propagate to the consumer.
96
+ // Surface child stderr so the user can see crash reasons. Once the
97
+ // server has become healthy we go quiet again unless
98
+ // PHI_BROWSER_VERBOSE=1 is set. Boot-time crashes ALWAYS print —
99
+ // otherwise a silent E22-style "failed to become healthy" exception
100
+ // is unsurmountable from the consumer side.
101
+ const stderrTail = [];
102
+ let healthy = false;
96
103
  child.stderr?.on("data", (chunk) => {
97
- if (process.env.PHI_BROWSER_VERBOSE) {
98
- process.stderr.write(`[camofox] ${chunk}`);
104
+ const text = chunk.toString();
105
+ if (!healthy || process.env.PHI_BROWSER_VERBOSE) {
106
+ process.stderr.write(`[camofox] ${text}`);
99
107
  }
108
+ stderrTail.push(text);
109
+ while (stderrTail.length > 200)
110
+ stderrTail.shift();
100
111
  });
101
112
  child.on("exit", (code) => {
102
113
  serverProcess = null;
103
114
  serverPort = null;
104
115
  bootPromise = null;
105
- if (process.env.PHI_BROWSER_VERBOSE) {
116
+ if (!healthy || process.env.PHI_BROWSER_VERBOSE) {
106
117
  process.stderr.write(`[camofox] server exited with code ${code}\n`);
107
118
  }
108
119
  });
120
+ // Expose stderr tail through a wrapper that promotes the listener
121
+ // flip — needed below when waitForHealth resolves.
122
+ child.__markHealthy = () => {
123
+ healthy = true;
124
+ };
125
+ child.__stderrTail = stderrTail;
109
126
  serverProcess = child;
110
127
  serverPort = port;
111
128
  const baseUrl = `http://127.0.0.1:${port}`;
112
- await waitForHealth(baseUrl);
129
+ try {
130
+ await waitForHealth(baseUrl);
131
+ child.__markHealthy?.();
132
+ }
133
+ catch (err) {
134
+ // Augment the health-check error with whatever the child wrote to
135
+ // stderr so the consumer has at least one breadcrumb to follow.
136
+ const tail = (child.__stderrTail ?? [])
137
+ .join("")
138
+ .split(/\r?\n/)
139
+ .filter(Boolean)
140
+ .slice(-20)
141
+ .join("\n");
142
+ const original = err instanceof Error ? err.message : String(err);
143
+ const augmented = new Error(tail
144
+ ? `${original}\nLast stderr lines from camofox-browser child:\n${tail}`
145
+ : `${original}\n(no stderr captured — set PHI_BROWSER_VERBOSE=1 for more)`);
146
+ throw augmented;
147
+ }
113
148
  return { baseUrl };
114
149
  })();
115
150
  try {
@@ -209,16 +244,25 @@ async function request(pathname, options = {}) {
209
244
  clearTimeout(timeout);
210
245
  }
211
246
  }
212
- /** Open a new browser tab. Returns the tab id used by the other tools. */
213
- export async function createTab(options) {
247
+ /**
248
+ * Open a new browser tab. Returns the tab id used by the other tools.
249
+ *
250
+ * The camofox-browser REST contract requires every tab to be associated
251
+ * with a `userId` (logical user) AND a `sessionKey` (logical session
252
+ * inside that user — used to group tabs that should share cookies /
253
+ * fingerprints / proxies). Phi-code's chat agents only need one of each,
254
+ * so both default to a constant sentinel when omitted.
255
+ */
256
+ export async function createTab(options = {}) {
214
257
  const userId = options.userId ?? DEFAULT_USER_ID;
215
- const body = { userId };
258
+ const sessionKey = options.sessionKey ?? DEFAULT_SESSION_KEY;
259
+ const body = { userId, sessionKey };
216
260
  if (options.url)
217
261
  body.url = options.url;
218
262
  if (options.viewport)
219
263
  body.viewport = options.viewport;
220
264
  const res = await request("/tabs", { method: "POST", body });
221
- return { tabId: res.tabId, userId, url: options.url };
265
+ return { tabId: res.tabId, userId, sessionKey, url: options.url };
222
266
  }
223
267
  /**
224
268
  * Navigate the given tab (or a freshly opened one) to a URL.
@@ -228,11 +272,19 @@ export async function createTab(options) {
228
272
  export async function navigate(options) {
229
273
  let tabId = options.tabId;
230
274
  if (!tabId) {
231
- const tab = await createTab({ userId: options.userId, url: options.url });
275
+ const tab = await createTab({
276
+ userId: options.userId,
277
+ sessionKey: options.sessionKey,
278
+ url: options.url,
279
+ });
232
280
  tabId = tab.tabId;
233
281
  return { tabId, url: options.url };
234
282
  }
235
- const body = { url: options.url };
283
+ const body = {
284
+ userId: options.userId ?? DEFAULT_USER_ID,
285
+ sessionKey: options.sessionKey ?? DEFAULT_SESSION_KEY,
286
+ url: options.url,
287
+ };
236
288
  if (options.waitUntil)
237
289
  body.waitUntil = options.waitUntil;
238
290
  if (options.timeoutMs)
@@ -245,7 +297,9 @@ export async function navigate(options) {
245
297
  * Refs returned here can be used with `click`/`type`/`scroll`.
246
298
  */
247
299
  export async function snapshot(options) {
248
- return await request(`/tabs/${encodeURIComponent(options.tabId)}/snapshot`);
300
+ const userId = options.userId ?? DEFAULT_USER_ID;
301
+ const qs = `?userId=${encodeURIComponent(userId)}`;
302
+ return await request(`/tabs/${encodeURIComponent(options.tabId)}/snapshot${qs}`);
249
303
  }
250
304
  /**
251
305
  * Extract the readable content of the current page (Readability-style).
@@ -258,32 +312,45 @@ export async function extract(options) {
258
312
  if (!options.url) {
259
313
  throw new Error("extract() requires either tabId or url");
260
314
  }
261
- const tab = await createTab({ userId: options.userId, url: options.url });
315
+ const tab = await createTab({
316
+ userId: options.userId,
317
+ sessionKey: options.sessionKey,
318
+ url: options.url,
319
+ });
262
320
  tabId = tab.tabId;
263
321
  // Wait for the navigation to settle before extracting.
264
322
  await request(`/tabs/${encodeURIComponent(tabId)}/wait`, {
265
323
  method: "POST",
266
- body: { event: "load" },
324
+ body: { userId: options.userId ?? DEFAULT_USER_ID, event: "load" },
267
325
  }).catch(() => { });
268
326
  }
269
327
  else if (options.url) {
270
- await navigate({ tabId, url: options.url });
328
+ await navigate({
329
+ tabId,
330
+ url: options.url,
331
+ userId: options.userId,
332
+ sessionKey: options.sessionKey,
333
+ });
271
334
  }
272
335
  const res = await request(`/tabs/${encodeURIComponent(tabId)}/extract`, {
273
336
  method: "POST",
274
- body: { mode: options.mode ?? "readability" },
337
+ body: {
338
+ userId: options.userId ?? DEFAULT_USER_ID,
339
+ sessionKey: options.sessionKey ?? DEFAULT_SESSION_KEY,
340
+ mode: options.mode ?? "readability",
341
+ },
275
342
  });
276
343
  return res;
277
344
  }
278
345
  /** Capture a screenshot of the given tab as a base64-encoded PNG. */
279
346
  export async function screenshot(options) {
280
347
  const query = new URLSearchParams();
348
+ query.set("userId", options.userId ?? DEFAULT_USER_ID);
281
349
  if (options.fullPage)
282
350
  query.set("fullPage", "1");
283
351
  if (options.clip)
284
352
  query.set("clip", JSON.stringify(options.clip));
285
- const qs = query.toString();
286
- const res = await request(`/tabs/${encodeURIComponent(options.tabId)}/screenshot${qs ? `?${qs}` : ""}`);
353
+ const res = await request(`/tabs/${encodeURIComponent(options.tabId)}/screenshot?${query.toString()}`);
287
354
  return {
288
355
  tabId: options.tabId,
289
356
  mimeType: res.mimeType ?? "image/png",
@@ -302,14 +369,14 @@ export async function search(options) {
302
369
  : engine === "bing"
303
370
  ? `https://www.bing.com/search?q=${encodeURIComponent(options.query)}`
304
371
  : `https://duckduckgo.com/?q=${encodeURIComponent(options.query)}`;
305
- return await extract({ url, userId: options.userId });
372
+ return await extract({ url, userId: options.userId, sessionKey: options.sessionKey });
306
373
  }
307
374
  /** Click an element by ref (from `snapshot`) or CSS selector. */
308
375
  export async function click(options) {
309
376
  if (!options.ref && !options.selector) {
310
377
  throw new Error("click() requires `ref` or `selector`");
311
378
  }
312
- const body = {};
379
+ const body = { userId: options.userId ?? DEFAULT_USER_ID };
313
380
  if (options.ref)
314
381
  body.ref = options.ref;
315
382
  if (options.selector)
@@ -324,7 +391,10 @@ export async function click(options) {
324
391
  }
325
392
  /** Type text into a focused element (or one targeted via ref/selector). */
326
393
  export async function type(options) {
327
- const body = { text: options.text };
394
+ const body = {
395
+ userId: options.userId ?? DEFAULT_USER_ID,
396
+ text: options.text,
397
+ };
328
398
  if (options.ref)
329
399
  body.ref = options.ref;
330
400
  if (options.selector)
@@ -341,7 +411,10 @@ export async function type(options) {
341
411
  }
342
412
  /** Scroll the page or a specific element by ref. */
343
413
  export async function scroll(options) {
344
- const body = { direction: options.direction };
414
+ const body = {
415
+ userId: options.userId ?? DEFAULT_USER_ID,
416
+ direction: options.direction,
417
+ };
345
418
  if (options.ref)
346
419
  body.ref = options.ref;
347
420
  if (options.pixels)
@@ -354,7 +427,9 @@ export async function scroll(options) {
354
427
  }
355
428
  /** Close a single tab. The underlying browser context is kept warm. */
356
429
  export async function closeTab(options) {
357
- await request(`/tabs/${encodeURIComponent(options.tabId)}`, { method: "DELETE" });
430
+ const userId = options.userId ?? DEFAULT_USER_ID;
431
+ const qs = `?userId=${encodeURIComponent(userId)}`;
432
+ await request(`/tabs/${encodeURIComponent(options.tabId)}${qs}`, { method: "DELETE" });
358
433
  return { tabId: options.tabId };
359
434
  }
360
435
  /** List all open tabs for a user. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/browser",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Phi-code browser automation API: lazy-start the bundled Camoufox + camofox-browser server and expose 10 high-level tools (navigate, extract, screenshot, click, type, scroll, snapshot, search, close_tab, list_tabs) as plain ES module functions. Zero external dependencies at runtime.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "clean": "rimraf dist"
39
39
  },
40
40
  "dependencies": {
41
- "@phi-code-admin/camofox-browser": "1.0.0",
41
+ "@phi-code-admin/camofox-browser": "1.0.1",
42
42
  "@phi-code-admin/camoufox-js": "1.0.0"
43
43
  },
44
44
  "devDependencies": {