@mindstudio-ai/local-model-tunnel 0.5.53 → 0.5.55

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-4HSYB4GX.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,349 @@ 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
+ await page.goto(opts.path, {
376
+ waitUntil: "networkidle0",
377
+ timeout: GOTO_TIMEOUT_MS
378
+ });
379
+ }
380
+ await page.waitForNetworkIdle({ timeout: SETTLE_TIMEOUT_MS, idleTime: SETTLE_IDLE_MS }).catch(() => {
381
+ });
382
+ if (opts.fullPage) {
383
+ await preRollScroll(page);
384
+ }
385
+ let width;
386
+ let height;
387
+ if (opts.fullPage) {
388
+ const dims = await page.evaluate(() => ({
389
+ width: document.documentElement.scrollWidth,
390
+ height: document.documentElement.scrollHeight
391
+ }));
392
+ width = dims.width;
393
+ height = dims.height;
394
+ } else {
395
+ const vp = page.viewport();
396
+ width = vp?.width ?? 0;
397
+ height = vp?.height ?? 0;
398
+ }
399
+ let styleMap;
400
+ try {
401
+ const result = await page.evaluate(() => {
402
+ const api = window.__MINDSTUDIO_BROWSER_AGENT__;
403
+ return api?.computeStyleMap?.() ?? null;
404
+ });
405
+ if (typeof result === "string" && result.length > 0) styleMap = result;
406
+ } catch {
407
+ }
408
+ const buf = await page.screenshot({
409
+ type: "jpeg",
410
+ quality: JPEG_QUALITY,
411
+ fullPage: opts.fullPage
412
+ });
413
+ await uploadToPresigned(opts.uploadUrl, opts.uploadFields, buf);
414
+ return {
415
+ uploaded: true,
416
+ width,
417
+ height,
418
+ ...styleMap ? { styleMap } : {}
419
+ };
420
+ }
421
+ async function preRollScroll(page) {
422
+ try {
423
+ const scrolled = await page.evaluate(() => {
424
+ const el = document.scrollingElement || document.documentElement;
425
+ const max = Math.max(
426
+ document.documentElement.scrollHeight,
427
+ document.body.scrollHeight
428
+ );
429
+ if (max <= window.innerHeight + 10) return false;
430
+ el.scrollTo({ top: max, left: 0, behavior: "instant" });
431
+ return true;
432
+ });
433
+ if (!scrolled) return;
434
+ await new Promise((r) => setTimeout(r, PREROLL_BOTTOM_DWELL_MS));
435
+ await page.waitForNetworkIdle({
436
+ timeout: PREROLL_NETWORK_IDLE_MS,
437
+ idleTime: SETTLE_IDLE_MS
438
+ }).catch(() => {
439
+ });
440
+ await page.evaluate(() => {
441
+ const el = document.scrollingElement || document.documentElement;
442
+ el.scrollTo({ top: 0, left: 0, behavior: "instant" });
443
+ });
444
+ await new Promise((r) => setTimeout(r, PREROLL_TOP_DWELL_MS));
445
+ } catch {
446
+ }
447
+ }
448
+ async function uploadToPresigned(uploadUrl, uploadFields, buf) {
449
+ const form = new FormData();
450
+ for (const [k, v] of Object.entries(uploadFields)) form.append(k, v);
451
+ form.append(
452
+ "file",
453
+ new Blob([buf], { type: "image/jpeg" }),
454
+ "screenshot.jpg"
455
+ );
456
+ const res = await fetch(uploadUrl, { method: "POST", body: form });
457
+ if (!res.ok) {
458
+ throw new Error(`Screenshot upload failed: ${res.status}`);
459
+ }
460
+ }
461
+
462
+ // src/dev/browser/cookies.ts
463
+ var AUTH_COOKIE_NAME = "__ms_auth";
464
+ function cookieHost(page) {
465
+ try {
466
+ return new URL(page.url()).hostname || "127.0.0.1";
467
+ } catch {
468
+ return "127.0.0.1";
469
+ }
470
+ }
471
+ async function clearAuthCookies(page) {
472
+ const domain = cookieHost(page);
473
+ try {
474
+ await page.deleteCookie({ name: AUTH_COOKIE_NAME, domain });
475
+ } catch {
476
+ }
477
+ try {
478
+ await page.deleteCookie({ name: AUTH_COOKIE_NAME });
479
+ } catch {
480
+ }
481
+ }
482
+ async function setAuthCookie(page, value) {
483
+ const domain = cookieHost(page);
484
+ await page.setCookie({
485
+ name: AUTH_COOKIE_NAME,
486
+ value,
487
+ domain,
488
+ path: "/",
489
+ sameSite: "None",
490
+ secure: true
491
+ });
492
+ }
493
+
40
494
  // src/dev/ipc/session-events.ts
41
495
  function subscribeDevEvents(shutdown) {
42
496
  const unsubs = [];
@@ -154,68 +608,159 @@ async function handleClearImpersonation(ctx) {
154
608
  }
155
609
 
156
610
  // src/dev/stdin-commands/browser.ts
611
+ var MIN_RECORDING_BYTES = 5e3;
157
612
  async function handleBrowser(ctx, cmd) {
158
613
  if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
159
614
  const steps = cmd.steps;
160
615
  if (!Array.isArray(steps) || steps.length === 0) {
161
616
  throw new CommandError('browser action requires a non-empty "steps" array', "INVALID_INPUT");
162
617
  }
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;
618
+ const page = ctx.state.browser?.getActivePage();
619
+ if (!page) {
620
+ throw new CommandError(
621
+ "Sandbox browser unavailable \u2014 headless Chrome is required for automation",
622
+ "NO_BROWSER"
623
+ );
624
+ }
625
+ const resultsByIndex = new Array(
626
+ steps.length
627
+ );
628
+ let lastSnapshot = "";
629
+ let lastLogs = [];
630
+ let totalDuration = 0;
631
+ const allEvents = [];
632
+ let buffer = [];
633
+ const flushBuffer = async () => {
634
+ if (buffer.length === 0) return;
635
+ const batch = buffer.map((b) => b.step);
636
+ const out = await ctx.state.proxy.dispatchBrowserCommand(batch);
637
+ const outSteps = out.steps ?? [];
638
+ for (let i = 0; i < buffer.length; i++) {
639
+ const returned = outSteps[i] ?? {};
640
+ resultsByIndex[buffer[i].idx] = {
641
+ ...returned,
642
+ index: buffer[i].idx,
643
+ command: buffer[i].step.command
644
+ };
645
+ }
646
+ if (typeof out.snapshot === "string" && out.snapshot.length > 0) {
647
+ lastSnapshot = out.snapshot;
648
+ }
649
+ if (Array.isArray(out.logs)) lastLogs = out.logs;
650
+ if (typeof out.duration === "number") totalDuration += out.duration;
651
+ if (Array.isArray(out.events)) allEvents.push(...out.events);
652
+ buffer = [];
653
+ };
654
+ for (let i = 0; i < steps.length; i++) {
655
+ const step = steps[i];
656
+ const command = step.command;
657
+ if (command === "screenshotFullPage" || command === "screenshotViewport") {
658
+ await flushBuffer();
659
+ const captured = await captureScreenshotStep(ctx, page, step, command);
660
+ resultsByIndex[i] = { index: i, command, result: captured };
661
+ totalDuration += captured._durationMs ?? 0;
662
+ delete captured._durationMs;
663
+ } else {
664
+ buffer.push({ idx: i, step });
173
665
  }
174
666
  }
175
- const hasStepError = resultSteps.some((s) => s.error);
667
+ await flushBuffer();
668
+ const densified = resultsByIndex.map(
669
+ (r, idx) => r ?? { index: idx, command: steps[idx].command, error: "no result" }
670
+ );
671
+ const hasStepError = densified.some((s) => s?.error);
672
+ const recordingUrl = await uploadRecording(ctx, allEvents);
176
673
  return {
177
674
  success: !hasStepError,
178
675
  ...hasStepError ? { errorCode: "BROWSER_ERROR" } : {},
179
- steps: resultSteps,
180
- snapshot: result.snapshot,
181
- logs: result.logs,
182
- duration: result.duration
676
+ steps: densified,
677
+ snapshot: lastSnapshot,
678
+ logs: lastLogs,
679
+ duration: totalDuration,
680
+ ...recordingUrl ? { recordingUrl } : {}
183
681
  };
184
682
  }
185
- async function injectScreenshotUploads(ctx, steps) {
683
+ async function captureScreenshotStep(ctx, page, step, command) {
186
684
  const session = ctx.state.runner?.getSession();
187
685
  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);
686
+ if (!session || !appId) {
687
+ throw new CommandError("No active session", "NO_SESSION");
688
+ }
689
+ const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
690
+ appId,
691
+ session.sessionId,
692
+ "jpg",
693
+ "image/jpeg"
694
+ );
695
+ const start = Date.now();
696
+ const r = await captureViaCdp(page, {
697
+ fullPage: command === "screenshotFullPage",
698
+ path: typeof step.path === "string" ? step.path : void 0,
699
+ uploadUrl,
700
+ uploadFields
701
+ });
702
+ return {
703
+ url: publicUrl,
704
+ width: r.width,
705
+ height: r.height,
706
+ ...r.styleMap ? { styleMap: r.styleMap } : {},
707
+ _durationMs: Date.now() - start
708
+ };
709
+ }
710
+ async function uploadRecording(ctx, events) {
711
+ if (events.length === 0) return null;
712
+ const session = ctx.state.runner?.getSession();
713
+ const appId = ctx.state.appConfig?.appId;
714
+ if (!session || !appId) return null;
715
+ const body = JSON.stringify(events);
716
+ if (body.length < MIN_RECORDING_BYTES) return null;
717
+ try {
718
+ const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
719
+ appId,
720
+ session.sessionId,
721
+ "json",
722
+ "application/json"
723
+ );
724
+ const form = new FormData();
725
+ for (const [k, v] of Object.entries(uploadFields)) form.append(k, v);
726
+ form.append(
727
+ "file",
728
+ new Blob([body], { type: "application/json" }),
729
+ "recording.json"
730
+ );
731
+ const res = await fetch(uploadUrl, { method: "POST", body: form });
732
+ if (!res.ok) {
733
+ log.warn("browser", "Recording upload failed", {
734
+ status: res.status,
735
+ bytes: body.length
736
+ });
737
+ return null;
205
738
  }
739
+ log.info("browser", "Recording uploaded", {
740
+ bytes: body.length,
741
+ events: events.length
742
+ });
743
+ return publicUrl;
744
+ } catch (err) {
745
+ log.warn("browser", "Recording upload errored", {
746
+ error: err instanceof Error ? err.message : String(err)
747
+ });
748
+ return null;
206
749
  }
207
- return prepared;
208
750
  }
209
751
 
210
752
  // src/dev/stdin-commands/screenshot-full-page.ts
211
753
  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
754
  if (!ctx.state.runner?.getSession() || !ctx.state.appConfig?.appId) {
217
755
  throw new CommandError("No active session", "NO_SESSION");
218
756
  }
757
+ const page = ctx.state.browser?.getActivePage();
758
+ if (!page) {
759
+ throw new CommandError(
760
+ "Sandbox browser unavailable \u2014 headless Chrome is required for screenshots",
761
+ "NO_BROWSER"
762
+ );
763
+ }
219
764
  const startTime = Date.now();
220
765
  const session = ctx.state.runner.getSession();
221
766
  const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
@@ -224,23 +769,18 @@ async function handleScreenshotFullPage(ctx, cmd) {
224
769
  "jpg",
225
770
  "image/jpeg"
226
771
  );
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
- }
772
+ const r = await captureViaCdp(page, {
773
+ fullPage: true,
774
+ path: typeof cmd.path === "string" ? cmd.path : void 0,
775
+ uploadUrl,
776
+ uploadFields
777
+ });
238
778
  return {
239
779
  success: true,
240
780
  url: publicUrl,
241
- width: stepResult.width,
242
- height: stepResult.height,
243
- ...stepResult.styleMap ? { styleMap: stepResult.styleMap } : {},
781
+ width: r.width,
782
+ height: r.height,
783
+ ...r.styleMap ? { styleMap: r.styleMap } : {},
244
784
  duration: Date.now() - startTime
245
785
  };
246
786
  }
@@ -252,33 +792,6 @@ async function handleDevServerRestarting(ctx) {
252
792
  return { success: true };
253
793
  }
254
794
 
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
795
  // src/dev/stdin-commands/db-query.ts
283
796
  async function handleDbQuery(ctx, cmd) {
284
797
  if (!ctx.state.runner) throw new CommandError("No active session", "NO_SESSION");
@@ -321,32 +834,30 @@ async function handleDbQuery(ctx, cmd) {
321
834
 
322
835
  // src/dev/stdin-commands/setup-browser.ts
323
836
  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
837
  if (!ctx.state.appConfig?.appId) throw new CommandError("No active session", "NO_SESSION");
838
+ const page = ctx.state.browser?.getActivePage();
839
+ if (!page) {
840
+ throw new CommandError(
841
+ "Sandbox browser unavailable \u2014 headless Chrome is required for setup-browser",
842
+ "NO_BROWSER"
843
+ );
844
+ }
329
845
  const auth = cmd.auth;
330
846
  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
- });
847
+ await clearAuthCookies(page);
337
848
  if (auth) {
338
849
  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
- });
850
+ await setAuthCookie(page, cookie);
343
851
  }
344
- steps.push({ command: "reload" });
345
- if (path !== "/") {
346
- steps.push({ command: "navigate", url: path });
852
+ const absolute = new URL(path, page.url()).toString();
853
+ try {
854
+ await page.goto(absolute, { waitUntil: "networkidle0", timeout: 15e3 });
855
+ } catch (err) {
856
+ throw new CommandError(
857
+ `Navigation to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
858
+ "BROWSER_ERROR"
859
+ );
347
860
  }
348
- steps.push({ command: "snapshot" });
349
- await ctx.state.proxy.dispatchBrowserCommand(steps);
350
861
  return { success: true, path, authenticated: !!auth };
351
862
  }
352
863
 
@@ -358,8 +869,6 @@ var handlers = {
358
869
  "clear-impersonation": handleClearImpersonation,
359
870
  "browser": handleBrowser,
360
871
  "screenshotFullPage": handleScreenshotFullPage,
361
- "browser-status": handleBrowserStatus,
362
- "reset-browser": handleResetBrowser,
363
872
  "db-query": handleDbQuery,
364
873
  "setup-browser": handleSetupBrowser,
365
874
  "dev-server-restarting": handleDevServerRestarting
@@ -490,6 +999,17 @@ async function startSession(cwd, opts, state, shutdown) {
490
999
  }
491
1000
  runner.setProxyUrl(`http://${bindAddress === "0.0.0.0" ? "localhost" : bindAddress}:${state.proxyPort}`);
492
1001
  runner.setProxy(state.proxy);
1002
+ if (opts.sandboxBrowser && state.proxyPort !== null && !state.browser) {
1003
+ const webConfig = getWebInterfaceConfig(appConfig, cwd);
1004
+ const previewMode = webConfig?.defaultPreviewMode ?? "desktop";
1005
+ const supervisor = new BrowserSupervisor(state.proxyPort, previewMode);
1006
+ state.browser = supervisor;
1007
+ supervisor.start().catch((err) => {
1008
+ log.warn("browser", "Sandbox browser failed to start", {
1009
+ error: err instanceof Error ? err.message : String(err)
1010
+ });
1011
+ });
1012
+ }
493
1013
  }
494
1014
  emitEvent("session-started", {
495
1015
  sessionId: session.sessionId,
@@ -563,6 +1083,11 @@ async function teardownRunner(state) {
563
1083
  }
564
1084
  async function teardownAll(state) {
565
1085
  await teardownRunner(state);
1086
+ if (state.browser) {
1087
+ await state.browser.stop().catch(() => {
1088
+ });
1089
+ state.browser = null;
1090
+ }
566
1091
  state.proxy?.stop();
567
1092
  state.proxy = null;
568
1093
  state.proxyPort = null;
@@ -585,6 +1110,7 @@ async function startHeadless(opts = {}) {
585
1110
  const state = {
586
1111
  runner: null,
587
1112
  proxy: null,
1113
+ browser: null,
588
1114
  appConfig: null,
589
1115
  proxyPort: null,
590
1116
  unsubscribers: []
@@ -698,4 +1224,4 @@ async function startHeadless(opts = {}) {
698
1224
  export {
699
1225
  startHeadless
700
1226
  };
701
- //# sourceMappingURL=chunk-MRDQMUYZ.js.map
1227
+ //# sourceMappingURL=chunk-5A5ASXUO.js.map