@mindstudio-ai/local-model-tunnel 0.5.54 → 0.5.56

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.
@@ -25,7 +25,118 @@ import {
25
25
  syncSchema,
26
26
  watchConfigFile,
27
27
  watchTableFiles
28
- } from "./chunk-7XPEELY3.js";
28
+ } from "./chunk-ERAJTIOM.js";
29
+
30
+ // src/dev/browser/launcher.ts
31
+ import puppeteer from "puppeteer-core";
32
+
33
+ // src/dev/browser/chrome-path.ts
34
+ import { existsSync } from "fs";
35
+ import { execSync } from "child_process";
36
+ var CANDIDATES = [
37
+ "/usr/bin/google-chrome-stable",
38
+ "/usr/bin/google-chrome",
39
+ "/usr/bin/chromium",
40
+ "/usr/bin/chromium-browser",
41
+ "/opt/google/chrome/google-chrome",
42
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
43
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
44
+ ];
45
+ var PATH_COMMANDS = ["google-chrome-stable", "google-chrome", "chromium"];
46
+ function resolveChromePath() {
47
+ for (const candidate of CANDIDATES) {
48
+ if (existsSync(candidate)) return candidate;
49
+ }
50
+ for (const cmd of PATH_COMMANDS) {
51
+ try {
52
+ const resolved = execSync(`command -v ${cmd}`, {
53
+ encoding: "utf-8",
54
+ stdio: ["ignore", "pipe", "ignore"]
55
+ }).trim();
56
+ if (resolved && existsSync(resolved)) return resolved;
57
+ } catch {
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ // src/dev/browser/launcher.ts
64
+ var LAUNCH_ARGS = [
65
+ "--no-sandbox",
66
+ "--disable-dev-shm-usage",
67
+ "--disable-gpu",
68
+ "--hide-scrollbars",
69
+ "--force-color-profile=srgb",
70
+ "--font-render-hinting=none",
71
+ "--disable-blink-features=AutomationControlled",
72
+ "--lang=en-US"
73
+ ];
74
+ var DESKTOP_VIEWPORT = {
75
+ width: 1440,
76
+ height: 900,
77
+ deviceScaleFactor: 1,
78
+ isMobile: false,
79
+ hasTouch: false
80
+ };
81
+ var MOBILE_VIEWPORT = {
82
+ width: 390,
83
+ height: 844,
84
+ deviceScaleFactor: 2,
85
+ isMobile: true,
86
+ hasTouch: true
87
+ };
88
+ function viewportFor(mode) {
89
+ return mode === "mobile" ? MOBILE_VIEWPORT : DESKTOP_VIEWPORT;
90
+ }
91
+ async function launchSandboxBrowser(opts) {
92
+ const executablePath = resolveChromePath();
93
+ if (!executablePath) {
94
+ log.warn(
95
+ "browser",
96
+ "No Chrome executable found \u2014 sandbox-browser mode disabled for this session"
97
+ );
98
+ return null;
99
+ }
100
+ const previewMode = opts.previewMode === "mobile" ? "mobile" : "desktop";
101
+ const viewport = viewportFor(previewMode);
102
+ const browser = await puppeteer.launch({
103
+ executablePath,
104
+ headless: true,
105
+ args: LAUNCH_ARGS,
106
+ defaultViewport: viewport
107
+ });
108
+ const proc = browser.process();
109
+ proc?.stderr?.on("data", (buf) => {
110
+ const line = buf.toString().trim();
111
+ if (line) log.debug("browser-chrome", line);
112
+ });
113
+ const pages = await browser.pages();
114
+ const page = pages[0] ?? await browser.newPage();
115
+ const target = `http://127.0.0.1:${opts.proxyPort}/?ms_sandbox=1`;
116
+ try {
117
+ await page.goto(target, { waitUntil: "networkidle0", timeout: 15e3 });
118
+ } catch (err) {
119
+ await browser.close().catch(() => {
120
+ });
121
+ throw err;
122
+ }
123
+ const viewportStr = `${viewport.width}x${viewport.height}@${viewport.deviceScaleFactor}x`;
124
+ log.info("browser", "Sandbox browser launched", {
125
+ executablePath,
126
+ target,
127
+ previewMode,
128
+ viewport: viewportStr,
129
+ pid: proc?.pid ?? null
130
+ });
131
+ return {
132
+ browser,
133
+ page,
134
+ executablePath,
135
+ pid: proc?.pid ?? null,
136
+ previewMode,
137
+ viewport: viewportStr
138
+ };
139
+ }
29
140
 
30
141
  // src/dev/ipc/ipc.ts
31
142
  function emitEvent(event, data) {
@@ -37,6 +148,350 @@ function emitResponse(action, requestId, status, data) {
37
148
  );
38
149
  }
39
150
 
151
+ // src/dev/browser/supervisor.ts
152
+ var BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
153
+ var MAX_FAILURES = 5;
154
+ var CLOSE_TIMEOUT_MS = 5e3;
155
+ var BrowserSupervisor = class {
156
+ constructor(proxyPort, previewMode = "desktop") {
157
+ this.proxyPort = proxyPort;
158
+ this.previewMode = previewMode;
159
+ }
160
+ proxyPort;
161
+ previewMode;
162
+ browser = null;
163
+ page = null;
164
+ stopping = false;
165
+ degraded = false;
166
+ consecutiveFailures = 0;
167
+ restartTimer = null;
168
+ runningSince = null;
169
+ lastExitInfo = null;
170
+ async start() {
171
+ if (this.browser) return;
172
+ await this.launchOnce();
173
+ }
174
+ async stop() {
175
+ if (this.stopping) return;
176
+ this.stopping = true;
177
+ if (this.restartTimer) {
178
+ clearTimeout(this.restartTimer);
179
+ this.restartTimer = null;
180
+ }
181
+ const browser = this.browser;
182
+ this.browser = null;
183
+ this.page = null;
184
+ if (browser) {
185
+ await this.closeBrowser(browser);
186
+ }
187
+ this.runningSince = null;
188
+ this.lastExitInfo = null;
189
+ emitEvent("sandbox-browser-state", { state: "stopped" });
190
+ }
191
+ isRunning() {
192
+ return !!this.browser && !this.degraded;
193
+ }
194
+ isDegraded() {
195
+ return this.degraded;
196
+ }
197
+ /**
198
+ * Returns the active puppeteer Page when the sandbox browser is running
199
+ * and not degraded; null otherwise. Callers use this to decide whether
200
+ * a CDP-side fast path is available for a given command.
201
+ */
202
+ getActivePage() {
203
+ if (this.stopping || this.degraded) return null;
204
+ if (!this.browser || !this.page) return null;
205
+ return this.page;
206
+ }
207
+ async launchOnce() {
208
+ if (this.stopping) return;
209
+ const attempt = this.consecutiveFailures + 1;
210
+ log.info("browser", "Sandbox browser launch starting", {
211
+ proxyPort: this.proxyPort,
212
+ attempt
213
+ });
214
+ emitEvent("sandbox-browser-state", {
215
+ state: "starting",
216
+ attempt,
217
+ previewMode: this.previewMode
218
+ });
219
+ try {
220
+ const launched = await launchSandboxBrowser({
221
+ proxyPort: this.proxyPort,
222
+ previewMode: this.previewMode
223
+ });
224
+ if (!launched) {
225
+ this.degraded = true;
226
+ emitEvent("sandbox-browser-state", {
227
+ state: "degraded",
228
+ reason: "no-executable"
229
+ });
230
+ return;
231
+ }
232
+ if (this.stopping) {
233
+ await this.closeBrowser(launched.browser).catch(() => {
234
+ });
235
+ return;
236
+ }
237
+ this.browser = launched.browser;
238
+ this.page = launched.page;
239
+ this.consecutiveFailures = 0;
240
+ this.degraded = false;
241
+ this.runningSince = Date.now();
242
+ this.lastExitInfo = null;
243
+ const proc = launched.browser.process();
244
+ proc?.once("exit", (code, signal) => {
245
+ this.lastExitInfo = { exitCode: code, signal: signal ?? null };
246
+ });
247
+ launched.browser.on("disconnected", () => this.onDisconnect());
248
+ emitEvent("sandbox-browser-state", {
249
+ state: "running",
250
+ pid: launched.pid,
251
+ previewMode: launched.previewMode,
252
+ viewport: launched.viewport,
253
+ executablePath: launched.executablePath
254
+ });
255
+ } catch (err) {
256
+ if (this.stopping) return;
257
+ this.consecutiveFailures++;
258
+ const message = err instanceof Error ? err.message : String(err);
259
+ log.warn("browser", "Failed to launch sandbox browser", {
260
+ attempt: this.consecutiveFailures,
261
+ error: message
262
+ });
263
+ emitEvent("sandbox-browser-state", {
264
+ state: "crashed",
265
+ exitCode: null,
266
+ signal: null,
267
+ durationMs: 0,
268
+ consecutiveFailures: this.consecutiveFailures,
269
+ error: message
270
+ });
271
+ this.scheduleRestart();
272
+ }
273
+ }
274
+ async onDisconnect() {
275
+ if (this.stopping) return;
276
+ const hadBrowser = !!this.browser;
277
+ this.browser = null;
278
+ this.page = null;
279
+ if (!hadBrowser) return;
280
+ this.consecutiveFailures++;
281
+ const durationMs = this.runningSince ? Date.now() - this.runningSince : 0;
282
+ this.runningSince = null;
283
+ log.warn("browser", "Sandbox browser disconnected", {
284
+ attempt: this.consecutiveFailures
285
+ });
286
+ await this.waitForExitInfo();
287
+ emitEvent("sandbox-browser-state", {
288
+ state: "crashed",
289
+ exitCode: this.lastExitInfo?.exitCode ?? null,
290
+ signal: this.lastExitInfo?.signal ?? null,
291
+ durationMs,
292
+ consecutiveFailures: this.consecutiveFailures
293
+ });
294
+ this.lastExitInfo = null;
295
+ this.scheduleRestart();
296
+ }
297
+ async waitForExitInfo(timeoutMs = 200) {
298
+ if (this.lastExitInfo) return;
299
+ const deadline = Date.now() + timeoutMs;
300
+ while (!this.lastExitInfo && Date.now() < deadline) {
301
+ await new Promise((r) => setTimeout(r, 20));
302
+ }
303
+ }
304
+ scheduleRestart() {
305
+ if (this.stopping) return;
306
+ if (this.consecutiveFailures >= MAX_FAILURES) {
307
+ this.degraded = true;
308
+ log.warn(
309
+ "browser",
310
+ "Sandbox browser entering degraded mode after repeated failures \u2014 automation will fall back to user browsers",
311
+ { failures: this.consecutiveFailures }
312
+ );
313
+ emitEvent("sandbox-browser-state", {
314
+ state: "degraded",
315
+ reason: "repeated-crashes",
316
+ consecutiveFailures: this.consecutiveFailures
317
+ });
318
+ return;
319
+ }
320
+ const delay = BACKOFF_MS[Math.min(this.consecutiveFailures, BACKOFF_MS.length - 1)];
321
+ log.info("browser", "Scheduling sandbox browser restart", {
322
+ delayMs: delay,
323
+ attempt: this.consecutiveFailures
324
+ });
325
+ emitEvent("sandbox-browser-state", {
326
+ state: "restarting",
327
+ delayMs: delay,
328
+ nextAttempt: this.consecutiveFailures + 1
329
+ });
330
+ this.restartTimer = setTimeout(() => {
331
+ this.restartTimer = null;
332
+ void this.launchOnce();
333
+ }, delay);
334
+ }
335
+ async closeBrowser(browser) {
336
+ let resolved = false;
337
+ await new Promise((resolve) => {
338
+ const done = () => {
339
+ if (resolved) return;
340
+ resolved = true;
341
+ resolve();
342
+ };
343
+ const timeout = setTimeout(() => {
344
+ try {
345
+ browser.process()?.kill("SIGKILL");
346
+ } catch {
347
+ }
348
+ done();
349
+ }, CLOSE_TIMEOUT_MS);
350
+ browser.close().then(() => {
351
+ clearTimeout(timeout);
352
+ done();
353
+ }).catch(() => {
354
+ clearTimeout(timeout);
355
+ try {
356
+ browser.process()?.kill("SIGKILL");
357
+ } catch {
358
+ }
359
+ done();
360
+ });
361
+ });
362
+ }
363
+ };
364
+
365
+ // src/dev/browser/screenshot.ts
366
+ var GOTO_TIMEOUT_MS = 15e3;
367
+ var SETTLE_TIMEOUT_MS = 3e3;
368
+ var SETTLE_IDLE_MS = 200;
369
+ var JPEG_QUALITY = 85;
370
+ var PREROLL_BOTTOM_DWELL_MS = 300;
371
+ var PREROLL_NETWORK_IDLE_MS = 1500;
372
+ var PREROLL_TOP_DWELL_MS = 100;
373
+ async function captureViaCdp(page, opts) {
374
+ if (opts.path) {
375
+ const absolute = new URL(opts.path, page.url()).toString();
376
+ await page.goto(absolute, {
377
+ waitUntil: "networkidle0",
378
+ timeout: GOTO_TIMEOUT_MS
379
+ });
380
+ }
381
+ await page.waitForNetworkIdle({ timeout: SETTLE_TIMEOUT_MS, idleTime: SETTLE_IDLE_MS }).catch(() => {
382
+ });
383
+ if (opts.fullPage) {
384
+ await preRollScroll(page);
385
+ }
386
+ let width;
387
+ let height;
388
+ if (opts.fullPage) {
389
+ const dims = await page.evaluate(() => ({
390
+ width: document.documentElement.scrollWidth,
391
+ height: document.documentElement.scrollHeight
392
+ }));
393
+ width = dims.width;
394
+ height = dims.height;
395
+ } else {
396
+ const vp = page.viewport();
397
+ width = vp?.width ?? 0;
398
+ height = vp?.height ?? 0;
399
+ }
400
+ let styleMap;
401
+ try {
402
+ const result = await page.evaluate(() => {
403
+ const api = window.__MINDSTUDIO_BROWSER_AGENT__;
404
+ return api?.computeStyleMap?.() ?? null;
405
+ });
406
+ if (typeof result === "string" && result.length > 0) styleMap = result;
407
+ } catch {
408
+ }
409
+ const buf = await page.screenshot({
410
+ type: "jpeg",
411
+ quality: JPEG_QUALITY,
412
+ fullPage: opts.fullPage
413
+ });
414
+ await uploadToPresigned(opts.uploadUrl, opts.uploadFields, buf);
415
+ return {
416
+ uploaded: true,
417
+ width,
418
+ height,
419
+ ...styleMap ? { styleMap } : {}
420
+ };
421
+ }
422
+ async function preRollScroll(page) {
423
+ try {
424
+ const scrolled = await page.evaluate(() => {
425
+ const el = document.scrollingElement || document.documentElement;
426
+ const max = Math.max(
427
+ document.documentElement.scrollHeight,
428
+ document.body.scrollHeight
429
+ );
430
+ if (max <= window.innerHeight + 10) return false;
431
+ el.scrollTo({ top: max, left: 0, behavior: "instant" });
432
+ return true;
433
+ });
434
+ if (!scrolled) return;
435
+ await new Promise((r) => setTimeout(r, PREROLL_BOTTOM_DWELL_MS));
436
+ await page.waitForNetworkIdle({
437
+ timeout: PREROLL_NETWORK_IDLE_MS,
438
+ idleTime: SETTLE_IDLE_MS
439
+ }).catch(() => {
440
+ });
441
+ await page.evaluate(() => {
442
+ const el = document.scrollingElement || document.documentElement;
443
+ el.scrollTo({ top: 0, left: 0, behavior: "instant" });
444
+ });
445
+ await new Promise((r) => setTimeout(r, PREROLL_TOP_DWELL_MS));
446
+ } catch {
447
+ }
448
+ }
449
+ async function uploadToPresigned(uploadUrl, uploadFields, buf) {
450
+ const form = new FormData();
451
+ for (const [k, v] of Object.entries(uploadFields)) form.append(k, v);
452
+ form.append(
453
+ "file",
454
+ new Blob([buf], { type: "image/jpeg" }),
455
+ "screenshot.jpg"
456
+ );
457
+ const res = await fetch(uploadUrl, { method: "POST", body: form });
458
+ if (!res.ok) {
459
+ throw new Error(`Screenshot upload failed: ${res.status}`);
460
+ }
461
+ }
462
+
463
+ // src/dev/browser/cookies.ts
464
+ var AUTH_COOKIE_NAME = "__ms_auth";
465
+ function cookieHost(page) {
466
+ try {
467
+ return new URL(page.url()).hostname || "127.0.0.1";
468
+ } catch {
469
+ return "127.0.0.1";
470
+ }
471
+ }
472
+ async function clearAuthCookies(page) {
473
+ const domain = cookieHost(page);
474
+ try {
475
+ await page.deleteCookie({ name: AUTH_COOKIE_NAME, domain });
476
+ } catch {
477
+ }
478
+ try {
479
+ await page.deleteCookie({ name: AUTH_COOKIE_NAME });
480
+ } catch {
481
+ }
482
+ }
483
+ async function setAuthCookie(page, value) {
484
+ const domain = cookieHost(page);
485
+ await page.setCookie({
486
+ name: AUTH_COOKIE_NAME,
487
+ value,
488
+ domain,
489
+ path: "/",
490
+ sameSite: "None",
491
+ secure: true
492
+ });
493
+ }
494
+
40
495
  // src/dev/ipc/session-events.ts
41
496
  function subscribeDevEvents(shutdown) {
42
497
  const unsubs = [];
@@ -154,68 +609,159 @@ async function handleClearImpersonation(ctx) {
154
609
  }
155
610
 
156
611
  // src/dev/stdin-commands/browser.ts
612
+ var MIN_RECORDING_BYTES = 5e3;
157
613
  async function handleBrowser(ctx, cmd) {
158
614
  if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
159
615
  const steps = cmd.steps;
160
616
  if (!Array.isArray(steps) || steps.length === 0) {
161
617
  throw new CommandError('browser action requires a non-empty "steps" array', "INVALID_INPUT");
162
618
  }
163
- const preparedSteps = await injectScreenshotUploads(ctx, steps);
164
- const result = await ctx.state.proxy.dispatchBrowserCommand(preparedSteps);
165
- const resultSteps = result.steps ?? [];
166
- for (const step of resultSteps) {
167
- const stepResult = step.result;
168
- if (stepResult?.uploaded && stepResult?._publicUrl) {
169
- stepResult.url = stepResult._publicUrl;
170
- delete stepResult.uploaded;
171
- delete stepResult._publicUrl;
172
- delete stepResult.image;
619
+ const page = ctx.state.browser?.getActivePage();
620
+ if (!page) {
621
+ throw new CommandError(
622
+ "Sandbox browser unavailable \u2014 headless Chrome is required for automation",
623
+ "NO_BROWSER"
624
+ );
625
+ }
626
+ const resultsByIndex = new Array(
627
+ steps.length
628
+ );
629
+ let lastSnapshot = "";
630
+ let lastLogs = [];
631
+ let totalDuration = 0;
632
+ const allEvents = [];
633
+ let buffer = [];
634
+ const flushBuffer = async () => {
635
+ if (buffer.length === 0) return;
636
+ const batch = buffer.map((b) => b.step);
637
+ const out = await ctx.state.proxy.dispatchBrowserCommand(batch);
638
+ const outSteps = out.steps ?? [];
639
+ for (let i = 0; i < buffer.length; i++) {
640
+ const returned = outSteps[i] ?? {};
641
+ resultsByIndex[buffer[i].idx] = {
642
+ ...returned,
643
+ index: buffer[i].idx,
644
+ command: buffer[i].step.command
645
+ };
646
+ }
647
+ if (typeof out.snapshot === "string" && out.snapshot.length > 0) {
648
+ lastSnapshot = out.snapshot;
649
+ }
650
+ if (Array.isArray(out.logs)) lastLogs = out.logs;
651
+ if (typeof out.duration === "number") totalDuration += out.duration;
652
+ if (Array.isArray(out.events)) allEvents.push(...out.events);
653
+ buffer = [];
654
+ };
655
+ for (let i = 0; i < steps.length; i++) {
656
+ const step = steps[i];
657
+ const command = step.command;
658
+ if (command === "screenshotFullPage" || command === "screenshotViewport") {
659
+ await flushBuffer();
660
+ const captured = await captureScreenshotStep(ctx, page, step, command);
661
+ resultsByIndex[i] = { index: i, command, result: captured };
662
+ totalDuration += captured._durationMs ?? 0;
663
+ delete captured._durationMs;
664
+ } else {
665
+ buffer.push({ idx: i, step });
173
666
  }
174
667
  }
175
- const hasStepError = resultSteps.some((s) => s.error);
668
+ await flushBuffer();
669
+ const densified = resultsByIndex.map(
670
+ (r, idx) => r ?? { index: idx, command: steps[idx].command, error: "no result" }
671
+ );
672
+ const hasStepError = densified.some((s) => s?.error);
673
+ const recordingUrl = await uploadRecording(ctx, allEvents);
176
674
  return {
177
675
  success: !hasStepError,
178
676
  ...hasStepError ? { errorCode: "BROWSER_ERROR" } : {},
179
- steps: resultSteps,
180
- snapshot: result.snapshot,
181
- logs: result.logs,
182
- duration: result.duration
677
+ steps: densified,
678
+ snapshot: lastSnapshot,
679
+ logs: lastLogs,
680
+ duration: totalDuration,
681
+ ...recordingUrl ? { recordingUrl } : {}
183
682
  };
184
683
  }
185
- async function injectScreenshotUploads(ctx, steps) {
684
+ async function captureScreenshotStep(ctx, page, step, command) {
186
685
  const session = ctx.state.runner?.getSession();
187
686
  const appId = ctx.state.appConfig?.appId;
188
- if (!session || !appId) return steps;
189
- const prepared = [];
190
- for (const step of steps) {
191
- if (step.command === "screenshotViewport") {
192
- try {
193
- const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
194
- appId,
195
- session.sessionId,
196
- "jpg",
197
- "image/jpeg"
198
- );
199
- prepared.push({ ...step, uploadUrl, uploadFields, _publicUrl: publicUrl });
200
- } catch {
201
- prepared.push(step);
202
- }
203
- } else {
204
- prepared.push(step);
687
+ if (!session || !appId) {
688
+ throw new CommandError("No active session", "NO_SESSION");
689
+ }
690
+ const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
691
+ appId,
692
+ session.sessionId,
693
+ "jpg",
694
+ "image/jpeg"
695
+ );
696
+ const start = Date.now();
697
+ const r = await captureViaCdp(page, {
698
+ fullPage: command === "screenshotFullPage",
699
+ path: typeof step.path === "string" ? step.path : void 0,
700
+ uploadUrl,
701
+ uploadFields
702
+ });
703
+ return {
704
+ url: publicUrl,
705
+ width: r.width,
706
+ height: r.height,
707
+ ...r.styleMap ? { styleMap: r.styleMap } : {},
708
+ _durationMs: Date.now() - start
709
+ };
710
+ }
711
+ async function uploadRecording(ctx, events) {
712
+ if (events.length === 0) return null;
713
+ const session = ctx.state.runner?.getSession();
714
+ const appId = ctx.state.appConfig?.appId;
715
+ if (!session || !appId) return null;
716
+ const body = JSON.stringify(events);
717
+ if (body.length < MIN_RECORDING_BYTES) return null;
718
+ try {
719
+ const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
720
+ appId,
721
+ session.sessionId,
722
+ "json",
723
+ "application/json"
724
+ );
725
+ const form = new FormData();
726
+ for (const [k, v] of Object.entries(uploadFields)) form.append(k, v);
727
+ form.append(
728
+ "file",
729
+ new Blob([body], { type: "application/json" }),
730
+ "recording.json"
731
+ );
732
+ const res = await fetch(uploadUrl, { method: "POST", body: form });
733
+ if (!res.ok) {
734
+ log.warn("browser", "Recording upload failed", {
735
+ status: res.status,
736
+ bytes: body.length
737
+ });
738
+ return null;
205
739
  }
740
+ log.info("browser", "Recording uploaded", {
741
+ bytes: body.length,
742
+ events: events.length
743
+ });
744
+ return publicUrl;
745
+ } catch (err) {
746
+ log.warn("browser", "Recording upload errored", {
747
+ error: err instanceof Error ? err.message : String(err)
748
+ });
749
+ return null;
206
750
  }
207
- return prepared;
208
751
  }
209
752
 
210
753
  // src/dev/stdin-commands/screenshot-full-page.ts
211
754
  async function handleScreenshotFullPage(ctx, cmd) {
212
- if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
213
- if (!ctx.state.proxy.isBrowserConnected()) {
214
- throw new CommandError("No browser connected", "NO_BROWSER");
215
- }
216
755
  if (!ctx.state.runner?.getSession() || !ctx.state.appConfig?.appId) {
217
756
  throw new CommandError("No active session", "NO_SESSION");
218
757
  }
758
+ const page = ctx.state.browser?.getActivePage();
759
+ if (!page) {
760
+ throw new CommandError(
761
+ "Sandbox browser unavailable \u2014 headless Chrome is required for screenshots",
762
+ "NO_BROWSER"
763
+ );
764
+ }
219
765
  const startTime = Date.now();
220
766
  const session = ctx.state.runner.getSession();
221
767
  const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
@@ -224,23 +770,18 @@ async function handleScreenshotFullPage(ctx, cmd) {
224
770
  "jpg",
225
771
  "image/jpeg"
226
772
  );
227
- const steps = [];
228
- if (cmd.path) {
229
- steps.push({ command: "navigate", url: cmd.path });
230
- }
231
- steps.push({ command: "screenshotFullPage", uploadUrl, uploadFields });
232
- const result = await ctx.state.proxy.dispatchBrowserCommand(steps, 12e4);
233
- const resultSteps = result.steps;
234
- const stepResult = resultSteps?.[resultSteps.length - 1]?.result;
235
- if (!stepResult?.uploaded) {
236
- throw new CommandError("Screenshot capture or upload failed", "UPLOAD_FAILED");
237
- }
773
+ const r = await captureViaCdp(page, {
774
+ fullPage: true,
775
+ path: typeof cmd.path === "string" ? cmd.path : void 0,
776
+ uploadUrl,
777
+ uploadFields
778
+ });
238
779
  return {
239
780
  success: true,
240
781
  url: publicUrl,
241
- width: stepResult.width,
242
- height: stepResult.height,
243
- ...stepResult.styleMap ? { styleMap: stepResult.styleMap } : {},
782
+ width: r.width,
783
+ height: r.height,
784
+ ...r.styleMap ? { styleMap: r.styleMap } : {},
244
785
  duration: Date.now() - startTime
245
786
  };
246
787
  }
@@ -252,33 +793,6 @@ async function handleDevServerRestarting(ctx) {
252
793
  return { success: true };
253
794
  }
254
795
 
255
- // src/dev/stdin-commands/browser-status.ts
256
- async function handleBrowserStatus(ctx) {
257
- return { connected: ctx.state.proxy?.isBrowserConnected() ?? false };
258
- }
259
-
260
- // src/dev/stdin-commands/reset-browser.ts
261
- async function handleResetBrowser(ctx) {
262
- if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
263
- if (!ctx.state.proxy.isBrowserConnected()) throw new CommandError("No browser connected", "NO_BROWSER");
264
- const restoreResult = await ctx.state.proxy.dispatchBrowserCommand([
265
- { command: "restoreState" }
266
- ]);
267
- const stepResult = restoreResult.steps?.[0];
268
- const restored = stepResult?.result;
269
- if (restored?.restored) {
270
- const steps = [{ command: "reload" }];
271
- if (restored.path && restored.path !== "/") {
272
- steps.push({ command: "navigate", url: restored.path });
273
- }
274
- steps.push({ command: "snapshot" });
275
- await ctx.state.proxy.dispatchBrowserCommand(steps);
276
- return { success: true, restored: true, path: restored.path };
277
- }
278
- ctx.state.proxy.broadcastToClients("reload");
279
- return { success: true, restored: false };
280
- }
281
-
282
796
  // src/dev/stdin-commands/db-query.ts
283
797
  async function handleDbQuery(ctx, cmd) {
284
798
  if (!ctx.state.runner) throw new CommandError("No active session", "NO_SESSION");
@@ -321,32 +835,30 @@ async function handleDbQuery(ctx, cmd) {
321
835
 
322
836
  // src/dev/stdin-commands/setup-browser.ts
323
837
  async function handleSetupBrowser(ctx, cmd) {
324
- if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
325
- if (!ctx.state.proxy.isBrowserConnected()) {
326
- throw new CommandError("No browser connected", "NO_BROWSER");
327
- }
328
838
  if (!ctx.state.appConfig?.appId) throw new CommandError("No active session", "NO_SESSION");
839
+ const page = ctx.state.browser?.getActivePage();
840
+ if (!page) {
841
+ throw new CommandError(
842
+ "Sandbox browser unavailable \u2014 headless Chrome is required for setup-browser",
843
+ "NO_BROWSER"
844
+ );
845
+ }
329
846
  const auth = cmd.auth;
330
847
  const path = cmd.path || "/";
331
- const steps = [];
332
- steps.push({ command: "stashState" });
333
- steps.push({
334
- command: "evaluate",
335
- script: `document.cookie = '__ms_auth=; Max-Age=0; Path=/; Secure; SameSite=None'`
336
- });
848
+ await clearAuthCookies(page);
337
849
  if (auth) {
338
850
  const { cookie } = await createAuthSession(ctx.state.appConfig.appId, auth);
339
- steps.push({
340
- command: "evaluate",
341
- script: `document.cookie = '__ms_auth=${cookie}; Path=/; Secure; SameSite=None'`
342
- });
851
+ await setAuthCookie(page, cookie);
343
852
  }
344
- steps.push({ command: "reload" });
345
- if (path !== "/") {
346
- steps.push({ command: "navigate", url: path });
853
+ const absolute = new URL(path, page.url()).toString();
854
+ try {
855
+ await page.goto(absolute, { waitUntil: "networkidle0", timeout: 15e3 });
856
+ } catch (err) {
857
+ throw new CommandError(
858
+ `Navigation to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
859
+ "BROWSER_ERROR"
860
+ );
347
861
  }
348
- steps.push({ command: "snapshot" });
349
- await ctx.state.proxy.dispatchBrowserCommand(steps);
350
862
  return { success: true, path, authenticated: !!auth };
351
863
  }
352
864
 
@@ -358,8 +870,6 @@ var handlers = {
358
870
  "clear-impersonation": handleClearImpersonation,
359
871
  "browser": handleBrowser,
360
872
  "screenshotFullPage": handleScreenshotFullPage,
361
- "browser-status": handleBrowserStatus,
362
- "reset-browser": handleResetBrowser,
363
873
  "db-query": handleDbQuery,
364
874
  "setup-browser": handleSetupBrowser,
365
875
  "dev-server-restarting": handleDevServerRestarting
@@ -490,6 +1000,17 @@ async function startSession(cwd, opts, state, shutdown) {
490
1000
  }
491
1001
  runner.setProxyUrl(`http://${bindAddress === "0.0.0.0" ? "localhost" : bindAddress}:${state.proxyPort}`);
492
1002
  runner.setProxy(state.proxy);
1003
+ if (opts.sandboxBrowser && state.proxyPort !== null && !state.browser) {
1004
+ const webConfig = getWebInterfaceConfig(appConfig, cwd);
1005
+ const previewMode = webConfig?.defaultPreviewMode ?? "desktop";
1006
+ const supervisor = new BrowserSupervisor(state.proxyPort, previewMode);
1007
+ state.browser = supervisor;
1008
+ supervisor.start().catch((err) => {
1009
+ log.warn("browser", "Sandbox browser failed to start", {
1010
+ error: err instanceof Error ? err.message : String(err)
1011
+ });
1012
+ });
1013
+ }
493
1014
  }
494
1015
  emitEvent("session-started", {
495
1016
  sessionId: session.sessionId,
@@ -563,6 +1084,11 @@ async function teardownRunner(state) {
563
1084
  }
564
1085
  async function teardownAll(state) {
565
1086
  await teardownRunner(state);
1087
+ if (state.browser) {
1088
+ await state.browser.stop().catch(() => {
1089
+ });
1090
+ state.browser = null;
1091
+ }
566
1092
  state.proxy?.stop();
567
1093
  state.proxy = null;
568
1094
  state.proxyPort = null;
@@ -585,6 +1111,7 @@ async function startHeadless(opts = {}) {
585
1111
  const state = {
586
1112
  runner: null,
587
1113
  proxy: null,
1114
+ browser: null,
588
1115
  appConfig: null,
589
1116
  proxyPort: null,
590
1117
  unsubscribers: []
@@ -698,4 +1225,4 @@ async function startHeadless(opts = {}) {
698
1225
  export {
699
1226
  startHeadless
700
1227
  };
701
- //# sourceMappingURL=chunk-B7BNG7SI.js.map
1228
+ //# sourceMappingURL=chunk-M37JM7QT.js.map