@mindstudio-ai/local-model-tunnel 0.5.9 → 0.5.11

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.
@@ -266,6 +266,147 @@ var DevPollError = class extends Error {
266
266
  }
267
267
  };
268
268
 
269
+ // src/dev/ndjson-log.ts
270
+ import fs2 from "fs";
271
+ import { join } from "path";
272
+ var NdjsonLog = class {
273
+ constructor(filename, maxLines = 500, keepLines = 300, maxBytes = 2 * 1024 * 1024) {
274
+ this.filename = filename;
275
+ this.maxLines = maxLines;
276
+ this.keepLines = keepLines;
277
+ this.maxBytes = maxBytes;
278
+ }
279
+ fd = null;
280
+ logPath = null;
281
+ lineCount = 0;
282
+ rotating = false;
283
+ init(projectRoot) {
284
+ this.close();
285
+ try {
286
+ const logsDir = join(projectRoot, ".logs");
287
+ fs2.mkdirSync(logsDir, { recursive: true });
288
+ this.logPath = join(logsDir, this.filename);
289
+ if (fs2.existsSync(this.logPath)) {
290
+ const content = fs2.readFileSync(this.logPath, "utf-8");
291
+ this.lineCount = content.split("\n").filter((l) => l.trim()).length;
292
+ } else {
293
+ this.lineCount = 0;
294
+ }
295
+ this.fd = fs2.openSync(this.logPath, "a");
296
+ log.info(`${this.filename} log initialized`, {
297
+ path: this.logPath,
298
+ existingEntries: this.lineCount
299
+ });
300
+ } catch (err) {
301
+ log.warn(`Failed to initialize ${this.filename} log`, {
302
+ error: err instanceof Error ? err.message : String(err)
303
+ });
304
+ this.fd = null;
305
+ this.logPath = null;
306
+ }
307
+ }
308
+ append(record) {
309
+ if (this.fd === null) return;
310
+ try {
311
+ const line = JSON.stringify(record) + "\n";
312
+ fs2.writeSync(this.fd, line);
313
+ this.lineCount++;
314
+ this.maybeRotate();
315
+ } catch (err) {
316
+ log.debug(`Failed to write ${this.filename} log entry`, {
317
+ error: err instanceof Error ? err.message : String(err)
318
+ });
319
+ }
320
+ }
321
+ close() {
322
+ if (this.fd !== null) {
323
+ try {
324
+ fs2.closeSync(this.fd);
325
+ } catch {
326
+ }
327
+ this.fd = null;
328
+ }
329
+ this.logPath = null;
330
+ this.lineCount = 0;
331
+ this.rotating = false;
332
+ }
333
+ maybeRotate() {
334
+ if (this.fd === null || this.logPath === null || this.rotating) return;
335
+ try {
336
+ let needsRotation = this.lineCount > this.maxLines;
337
+ if (!needsRotation) {
338
+ const stat = fs2.fstatSync(this.fd);
339
+ needsRotation = stat.size > this.maxBytes;
340
+ }
341
+ if (!needsRotation) return;
342
+ this.rotating = true;
343
+ const content = fs2.readFileSync(this.logPath, "utf-8");
344
+ const lines = content.split("\n").filter((l) => l.trim());
345
+ const kept = lines.slice(-this.keepLines);
346
+ fs2.closeSync(this.fd);
347
+ fs2.writeFileSync(this.logPath, kept.join("\n") + "\n", "utf-8");
348
+ this.fd = fs2.openSync(this.logPath, "a");
349
+ this.lineCount = kept.length;
350
+ log.debug(`${this.filename} log rotated`, { kept: this.lineCount });
351
+ } catch (err) {
352
+ log.debug(`${this.filename} log rotation failed`, {
353
+ error: err instanceof Error ? err.message : String(err)
354
+ });
355
+ } finally {
356
+ this.rotating = false;
357
+ }
358
+ }
359
+ };
360
+
361
+ // src/dev/request-log.ts
362
+ var ndjsonLog = new NdjsonLog("requests.ndjson");
363
+ function initRequestLog(projectRoot) {
364
+ ndjsonLog.init(projectRoot);
365
+ }
366
+ function logMethodExecution(entry) {
367
+ ndjsonLog.append({
368
+ type: "method",
369
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
370
+ requestId: entry.requestId,
371
+ sessionId: entry.sessionId,
372
+ method: entry.methodExport,
373
+ path: entry.methodPath,
374
+ input: entry.input,
375
+ roleOverride: entry.roleOverride ?? null,
376
+ authorizationToken: entry.authorizationToken,
377
+ databases: entry.databases,
378
+ success: entry.result.success,
379
+ output: entry.result.output ?? null,
380
+ error: entry.result.error ?? null,
381
+ stdout: entry.result.stdout ?? [],
382
+ duration: entry.duration,
383
+ stats: entry.result.stats ?? null
384
+ });
385
+ }
386
+ function logScenarioExecution(entry) {
387
+ ndjsonLog.append({
388
+ type: "scenario",
389
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
390
+ sessionId: entry.sessionId,
391
+ scenario: {
392
+ id: entry.scenario.id,
393
+ name: entry.scenario.name ?? entry.scenario.export,
394
+ export: entry.scenario.export,
395
+ path: entry.scenario.path
396
+ },
397
+ databases: entry.databases,
398
+ success: entry.result?.success ?? false,
399
+ output: entry.result?.output ?? null,
400
+ error: entry.result?.error ?? (entry.infrastructureError ? { message: entry.infrastructureError } : null),
401
+ stdout: entry.result?.stdout ?? [],
402
+ duration: entry.duration,
403
+ stats: entry.result?.stats ?? null
404
+ });
405
+ }
406
+ function closeRequestLog() {
407
+ ndjsonLog.close();
408
+ }
409
+
269
410
  // src/dev/events.ts
270
411
  import { EventEmitter } from "events";
271
412
  var DevEventEmitter = class extends EventEmitter {
@@ -352,7 +493,7 @@ var devRequestEvents = new DevEventEmitter();
352
493
  // src/dev/transpiler.ts
353
494
  import { unlink, mkdir, readdir } from "fs/promises";
354
495
  import { existsSync } from "fs";
355
- import { resolve, dirname, basename, join } from "path";
496
+ import { resolve, dirname, basename, join as join2 } from "path";
356
497
  import { build } from "esbuild";
357
498
  var Transpiler = class {
358
499
  projectRoot;
@@ -378,18 +519,18 @@ var Transpiler = class {
378
519
  const start = Date.now();
379
520
  const absolutePath = resolve(this.projectRoot, methodPath);
380
521
  const name = basename(absolutePath).replace(/\.[^.]+$/, "");
381
- log.debug("transpiler Transpiling", { methodPath });
522
+ log.debug("Transpiling method", { methodPath });
382
523
  const nodeModulesDir = findNearestNodeModules(dirname(absolutePath));
383
524
  if (!nodeModulesDir) {
384
- log.error("transpiler No node_modules found", { methodPath, searchStart: dirname(absolutePath) });
525
+ log.error("Cannot find node_modules for method", { methodPath, searchStart: dirname(absolutePath) });
385
526
  throw new Error(
386
527
  `No node_modules found near ${methodPath}. Run npm install first.`
387
528
  );
388
529
  }
389
- log.debug("transpiler Found node_modules", { path: nodeModulesDir });
390
- const outDir = join(nodeModulesDir, ".cache", "mindstudio-dev");
530
+ log.debug("Found node_modules", { path: nodeModulesDir });
531
+ const outDir = join2(nodeModulesDir, ".cache", "mindstudio-dev");
391
532
  await mkdir(outDir, { recursive: true });
392
- const outfile = join(outDir, `${name}.__ms_dev__.mjs`);
533
+ const outfile = join2(outDir, `${name}.__ms_dev__.mjs`);
393
534
  await build({
394
535
  entryPoints: [absolutePath],
395
536
  bundle: true,
@@ -402,14 +543,14 @@ var Transpiler = class {
402
543
  logLevel: "silent"
403
544
  });
404
545
  this.outputFiles.add(outfile);
405
- log.info(`transpiler Transpiled in ${Date.now() - start}ms`, { methodPath, outfile });
546
+ log.info(`Method transpiled in ${Date.now() - start}ms`, { methodPath, outfile });
406
547
  return outfile;
407
548
  }
408
549
  /**
409
550
  * Clean up all transpiled output files.
410
551
  */
411
552
  async cleanup() {
412
- log.debug("transpiler Cleaning up", { fileCount: this.outputFiles.size });
553
+ log.debug("Cleaning up transpiled files", { fileCount: this.outputFiles.size });
413
554
  for (const file of this.outputFiles) {
414
555
  await unlink(file).catch(() => {
415
556
  });
@@ -421,11 +562,11 @@ async function removeOrphanedDevFiles(dir) {
421
562
  const entries = await readdir(dir, { withFileTypes: true });
422
563
  for (const entry of entries) {
423
564
  if (entry.name === "node_modules" || entry.name === ".git") continue;
424
- const fullPath = join(dir, entry.name);
565
+ const fullPath = join2(dir, entry.name);
425
566
  if (entry.isDirectory()) {
426
567
  await removeOrphanedDevFiles(fullPath);
427
568
  } else if (entry.name.endsWith(".__ms_dev__.mjs")) {
428
- log.debug("transpiler Removing orphaned file", { path: fullPath });
569
+ log.debug("Removing orphaned transpiled file", { path: fullPath });
429
570
  await unlink(fullPath).catch(() => {
430
571
  });
431
572
  }
@@ -434,7 +575,7 @@ async function removeOrphanedDevFiles(dir) {
434
575
  function findNearestNodeModules(startDir) {
435
576
  let dir = startDir;
436
577
  while (true) {
437
- const candidate = join(dir, "node_modules");
578
+ const candidate = join2(dir, "node_modules");
438
579
  if (existsSync(candidate)) {
439
580
  return candidate;
440
581
  }
@@ -446,19 +587,18 @@ function findNearestNodeModules(startDir) {
446
587
  }
447
588
 
448
589
  // src/dev/executor.ts
449
- import { spawn } from "child_process";
590
+ import { fork } from "child_process";
450
591
  import { writeFile, unlink as unlink2 } from "fs/promises";
451
- import { join as join2 } from "path";
592
+ import { join as join3 } from "path";
452
593
  import { tmpdir } from "os";
453
594
  import { randomBytes } from "crypto";
454
595
  var EXECUTION_TIMEOUT_MS = 3e4;
455
- function buildBootstrapScript(opts) {
596
+ var worker = null;
597
+ var workerScriptPath = null;
598
+ var workerProjectRoot = null;
599
+ var pending = /* @__PURE__ */ new Map();
600
+ function buildWorkerScript() {
456
601
  return `
457
- global.ai = {
458
- auth: ${JSON.stringify(opts.auth)},
459
- databases: ${JSON.stringify(opts.databases)},
460
- };
461
-
462
602
  function serializeError(err) {
463
603
  if (!err) return { message: 'Unknown error' };
464
604
 
@@ -467,7 +607,6 @@ function serializeError(err) {
467
607
  stack: err.stack,
468
608
  };
469
609
 
470
- // Capture common extra properties from SDK/HTTP errors
471
610
  if (err.code !== undefined) serialized.code = err.code;
472
611
  if (err.statusCode !== undefined) serialized.statusCode = err.statusCode;
473
612
  if (err.status !== undefined) serialized.status = err.status;
@@ -481,7 +620,6 @@ function serializeError(err) {
481
620
  serialized.cause = serializeError(err.cause);
482
621
  }
483
622
 
484
- // Capture any other enumerable properties
485
623
  for (const key of Object.keys(err)) {
486
624
  if (!(key in serialized)) {
487
625
  try {
@@ -496,91 +634,162 @@ function serializeError(err) {
496
634
  return serialized;
497
635
  }
498
636
 
499
- // Capture console output from method code
500
- const _stdout = [];
501
- console.log = (...args) => _stdout.push(args.map(String).join(' '));
502
- console.warn = (...args) => _stdout.push(args.map(String).join(' '));
503
- console.error = (...args) => _stdout.push(args.map(String).join(' '));
637
+ // Save original console methods so we can restore after each request
638
+ const _origLog = console.log;
639
+ const _origWarn = console.warn;
640
+ const _origError = console.error;
504
641
 
505
- const _startTime = Date.now();
642
+ process.on('message', async (msg) => {
643
+ const { id, transpiledPath, methodExport, input, auth, databases, authorizationToken, apiBaseUrl, streamId } = msg;
506
644
 
507
- const { ${opts.methodExport} } = await import(${JSON.stringify(opts.transpiledPath + "?t=" + Date.now())});
645
+ // Update per-request env vars
646
+ process.env.CALLBACK_TOKEN = authorizationToken;
647
+ process.env.REMOTE_HOSTNAME = apiBaseUrl;
648
+ if (streamId) process.env.STREAM_ID = streamId;
649
+ else delete process.env.STREAM_ID;
508
650
 
509
- try {
510
- const returnValue = await ${opts.methodExport}(${JSON.stringify(opts.input)});
511
- const _stats = { memoryUsedBytes: process.memoryUsage().heapUsed, executionTimeMs: Date.now() - _startTime };
512
- process.stdout.write(JSON.stringify({ success: true, output: returnValue, stdout: _stdout, stats: _stats }));
513
- } catch (err) {
514
- const _stats = { memoryUsedBytes: process.memoryUsage().heapUsed, executionTimeMs: Date.now() - _startTime };
515
- process.stdout.write(JSON.stringify({
516
- success: false,
517
- error: serializeError(err),
518
- stdout: _stdout,
519
- stats: _stats,
520
- }));
521
- }
651
+ // Update global context
652
+ global.ai = { auth, databases };
653
+
654
+ // Capture console output for this request
655
+ const stdout = [];
656
+ console.log = (...args) => stdout.push(args.map(String).join(' '));
657
+ console.warn = (...args) => stdout.push(args.map(String).join(' '));
658
+ console.error = (...args) => stdout.push(args.map(String).join(' '));
659
+
660
+ const startTime = Date.now();
661
+
662
+ try {
663
+ // Cache-bust so code changes are picked up
664
+ const mod = await import(transpiledPath + '?t=' + Date.now());
665
+ const fn = mod[methodExport];
666
+ if (typeof fn !== 'function') {
667
+ throw new Error(methodExport + ' is not a function (got ' + typeof fn + ')');
668
+ }
669
+ const returnValue = await fn(input);
670
+ const stats = { memoryUsedBytes: process.memoryUsage().heapUsed, executionTimeMs: Date.now() - startTime };
671
+ process.send({ id, success: true, output: returnValue, stdout, stats });
672
+ } catch (err) {
673
+ const stats = { memoryUsedBytes: process.memoryUsage().heapUsed, executionTimeMs: Date.now() - startTime };
674
+ process.send({ id, success: false, error: serializeError(err), stdout, stats });
675
+ } finally {
676
+ // Restore console
677
+ console.log = _origLog;
678
+ console.warn = _origWarn;
679
+ console.error = _origError;
680
+ }
681
+ });
682
+
683
+ // Signal ready
684
+ process.send({ type: 'ready' });
522
685
  `;
523
686
  }
524
- async function executeMethod(opts) {
525
- const tempFile = join2(
687
+ async function ensureWorker(projectRoot) {
688
+ if (worker?.connected && workerProjectRoot === projectRoot) {
689
+ return worker;
690
+ }
691
+ if (worker) {
692
+ worker.removeAllListeners();
693
+ worker.kill();
694
+ worker = null;
695
+ }
696
+ if (workerScriptPath) {
697
+ await unlink2(workerScriptPath).catch(() => {
698
+ });
699
+ workerScriptPath = null;
700
+ }
701
+ const scriptPath = join3(
526
702
  tmpdir(),
527
- `ms-dev-${randomBytes(8).toString("hex")}.mjs`
703
+ `ms-dev-worker-${randomBytes(4).toString("hex")}.mjs`
528
704
  );
529
- const script = buildBootstrapScript(opts);
530
- try {
531
- await writeFile(tempFile, script, "utf-8");
532
- log.debug("executor Spawning node process", { methodExport: opts.methodExport, cwd: opts.projectRoot, tempFile });
533
- return await new Promise((resolve2, reject) => {
534
- const stdoutChunks = [];
535
- const stderrChunks = [];
536
- const child = spawn("node", [tempFile], {
537
- cwd: opts.projectRoot,
538
- env: {
539
- ...process.env,
540
- // Auth + config env vars read by @mindstudio-ai/agent SDK
541
- // for platform callbacks (db queries, etc.)
542
- CALLBACK_TOKEN: opts.authorizationToken,
543
- REMOTE_HOSTNAME: opts.apiBaseUrl,
544
- ...opts.streamId ? { STREAM_ID: opts.streamId } : {}
545
- },
546
- stdio: ["ignore", "pipe", "pipe"]
547
- });
548
- child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
549
- child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
550
- const timeout = setTimeout(() => {
551
- log.warn("executor Timeout after 30s, sending SIGKILL", { methodExport: opts.methodExport });
552
- child.kill("SIGKILL");
553
- reject(new Error("Method execution timed out after 30s"));
554
- }, EXECUTION_TIMEOUT_MS);
555
- child.on("close", (code) => {
556
- clearTimeout(timeout);
557
- const stdout = Buffer.concat(stdoutChunks).toString("utf-8").trim();
558
- const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
559
- log.debug("executor Process exited", { code, stdoutLen: stdout.length, stderrLen: stderr.length });
560
- if (stdout) {
561
- try {
562
- resolve2(JSON.parse(stdout));
563
- return;
564
- } catch {
565
- log.warn("executor Invalid JSON from stdout", { stdout: stdout.slice(0, 200) });
566
- }
567
- }
568
- const errorMessage = stderr || stdout || `Method process exited with code ${code ?? "unknown"}`;
569
- resolve2({
570
- success: false,
571
- error: { message: errorMessage }
572
- });
573
- });
574
- child.on("error", (err) => {
575
- clearTimeout(timeout);
576
- log.error("executor Process error", { error: err.message });
577
- reject(err);
705
+ await writeFile(scriptPath, buildWorkerScript(), "utf-8");
706
+ workerScriptPath = scriptPath;
707
+ workerProjectRoot = projectRoot;
708
+ log.debug("Spawning method execution process", { cwd: projectRoot, scriptPath });
709
+ const child = fork(scriptPath, [], {
710
+ cwd: projectRoot,
711
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
712
+ env: { ...process.env }
713
+ });
714
+ await new Promise((resolve2, reject) => {
715
+ const onMessage = (msg) => {
716
+ if (msg?.type === "ready") {
717
+ child.off("message", onMessage);
718
+ resolve2();
719
+ }
720
+ };
721
+ child.on("message", onMessage);
722
+ child.on("error", reject);
723
+ child.on("exit", (code) => reject(new Error(`Worker exited during startup with code ${code}`)));
724
+ });
725
+ child.on("message", (msg) => {
726
+ if (!msg?.id) return;
727
+ const req = pending.get(msg.id);
728
+ if (!req) return;
729
+ pending.delete(msg.id);
730
+ clearTimeout(req.timer);
731
+ req.resolve(msg);
732
+ });
733
+ child.on("exit", (code) => {
734
+ log.warn("Method execution process exited unexpectedly", { code });
735
+ for (const [id, req] of pending) {
736
+ clearTimeout(req.timer);
737
+ req.resolve({ success: false, error: { message: `Worker process exited with code ${code}` } });
738
+ }
739
+ pending.clear();
740
+ worker = null;
741
+ });
742
+ child.stderr?.on("data", (chunk) => {
743
+ const text = chunk.toString().trim();
744
+ if (text) log.warn("Method process stderr", { text: text.slice(0, 500) });
745
+ });
746
+ worker = child;
747
+ log.info("Method execution process ready", { pid: child.pid });
748
+ return child;
749
+ }
750
+ async function executeMethod(opts) {
751
+ const w = await ensureWorker(opts.projectRoot);
752
+ const id = randomBytes(8).toString("hex");
753
+ log.debug("Sending method to execution process", { id, methodExport: opts.methodExport });
754
+ return new Promise((resolve2) => {
755
+ const timer = setTimeout(() => {
756
+ pending.delete(id);
757
+ log.warn("Method execution timed out", { id, methodExport: opts.methodExport });
758
+ resolve2({
759
+ success: false,
760
+ error: { message: "Method execution timed out after 30s" }
578
761
  });
762
+ }, EXECUTION_TIMEOUT_MS);
763
+ pending.set(id, { resolve: resolve2, timer });
764
+ w.send({
765
+ id,
766
+ transpiledPath: opts.transpiledPath,
767
+ methodExport: opts.methodExport,
768
+ input: opts.input,
769
+ auth: opts.auth,
770
+ databases: opts.databases,
771
+ authorizationToken: opts.authorizationToken,
772
+ apiBaseUrl: opts.apiBaseUrl,
773
+ streamId: opts.streamId
579
774
  });
580
- } finally {
581
- await unlink2(tempFile).catch(() => {
775
+ });
776
+ }
777
+ async function cleanupWorker() {
778
+ if (worker) {
779
+ worker.removeAllListeners();
780
+ worker.kill();
781
+ worker = null;
782
+ }
783
+ if (workerScriptPath) {
784
+ await unlink2(workerScriptPath).catch(() => {
582
785
  });
786
+ workerScriptPath = null;
787
+ }
788
+ workerProjectRoot = null;
789
+ for (const [, req] of pending) {
790
+ clearTimeout(req.timer);
583
791
  }
792
+ pending.clear();
584
793
  }
585
794
 
586
795
  // src/api.ts
@@ -743,6 +952,33 @@ async function disconnectHeartbeat() {
743
952
  }
744
953
  }
745
954
 
955
+ // src/dev/runner.ts
956
+ import { randomBytes as randomBytes2 } from "crypto";
957
+
958
+ // src/dev/format-error.ts
959
+ function formatErrorForDisplay(error) {
960
+ const parts = [];
961
+ if (error.message) {
962
+ parts.push(String(error.message));
963
+ }
964
+ const code = error.code ?? error.statusCode ?? error.status;
965
+ if (code !== void 0) {
966
+ parts.push(`(code: ${code})`);
967
+ }
968
+ if (error.body) {
969
+ parts.push(`Response: ${String(error.body).slice(0, 200)}`);
970
+ } else if (error.response) {
971
+ parts.push(`Response: ${String(error.response).slice(0, 200)}`);
972
+ }
973
+ if (error.cause && typeof error.cause === "object") {
974
+ const cause = error.cause;
975
+ if (cause.message) {
976
+ parts.push(`Caused by: ${cause.message}`);
977
+ }
978
+ }
979
+ return parts.join("\n");
980
+ }
981
+
746
982
  // src/dev/runner.ts
747
983
  var DevRunner = class {
748
984
  constructor(appId, projectRoot, startOpts = {}) {
@@ -771,27 +1007,28 @@ var DevRunner = class {
771
1007
  if (this.isRunning) {
772
1008
  throw new Error("DevRunner is already running");
773
1009
  }
774
- log.info("runner Starting session", { appId: this.appId, branch: this.startOpts.branch });
1010
+ log.info("Dev session starting", { appId: this.appId, branch: this.startOpts.branch });
775
1011
  const session = await startDevSession(this.appId, this.startOpts);
776
1012
  this.session = session;
777
1013
  this.transpiler = new Transpiler(this.projectRoot);
778
1014
  this.isRunning = true;
779
1015
  this.backoffMs = 1e3;
780
- log.info("runner Session started", { sessionId: session.sessionId, branch: session.branch });
1016
+ log.info("Dev session started", { sessionId: session.sessionId, branch: session.branch });
781
1017
  this.pollLoop();
782
1018
  return session;
783
1019
  }
784
1020
  async stop() {
785
- log.info("runner Stopping session");
1021
+ log.info("Dev session stopping");
786
1022
  this.isRunning = false;
787
1023
  if (this.session) {
788
1024
  try {
789
1025
  await stopDevSession(this.appId, this.session.sessionId);
790
1026
  } catch (err) {
791
- log.warn("runner Failed to stop session cleanly", { error: err instanceof Error ? err.message : String(err) });
1027
+ log.warn("Failed to stop dev session cleanly", { error: err instanceof Error ? err.message : String(err) });
792
1028
  }
793
1029
  this.session = null;
794
1030
  }
1031
+ await cleanupWorker();
795
1032
  if (this.transpiler) {
796
1033
  await this.transpiler.cleanup();
797
1034
  this.transpiler = null;
@@ -803,7 +1040,7 @@ var DevRunner = class {
803
1040
  // Set role override for subsequent method executions.
804
1041
  async setImpersonation(roles) {
805
1042
  if (!this.session) return;
806
- log.info("runner Impersonating", { roles });
1043
+ log.info("Setting role override", { roles });
807
1044
  const result = await impersonate(this.appId, this.session.sessionId, roles);
808
1045
  await this.refreshClientContext();
809
1046
  devRequestEvents.emitImpersonate({ roles: result.roles });
@@ -811,7 +1048,7 @@ var DevRunner = class {
811
1048
  // Clear role override — revert to session's default roles.
812
1049
  async clearImpersonation() {
813
1050
  if (!this.session) return;
814
- log.info("runner Clearing impersonation");
1051
+ log.info("Clearing role override");
815
1052
  const result = await impersonate(this.appId, this.session.sessionId, null);
816
1053
  await this.refreshClientContext();
817
1054
  devRequestEvents.emitImpersonate({ roles: result.roles });
@@ -825,7 +1062,94 @@ var DevRunner = class {
825
1062
  this.session.clientContext = context;
826
1063
  this.proxy.updateClientContext(context);
827
1064
  } catch (err) {
828
- log.warn("runner Failed to refresh client context", { error: err instanceof Error ? err.message : String(err) });
1065
+ log.warn("Failed to refresh session context after role change", { error: err instanceof Error ? err.message : String(err) });
1066
+ }
1067
+ }
1068
+ // Run a method directly (not via poll loop). Used by headless stdin commands
1069
+ // and programmatic callers to test methods without a browser.
1070
+ async runMethod(opts) {
1071
+ if (!this.session || !this.transpiler) {
1072
+ return { success: false, error: { message: "Session not started" }, duration: 0 };
1073
+ }
1074
+ const requestId = randomBytes2(8).toString("hex");
1075
+ const startTime = Date.now();
1076
+ devRequestEvents.emitStart({
1077
+ id: requestId,
1078
+ type: "execute",
1079
+ method: opts.methodExport,
1080
+ timestamp: startTime
1081
+ });
1082
+ log.info("Method received (direct)", { requestId, method: opts.methodExport });
1083
+ try {
1084
+ const authorizationToken = await fetchCallbackToken(this.appId, this.session.sessionId);
1085
+ const transpiledPath = await this.transpiler.transpile(opts.methodPath);
1086
+ const result = await executeMethod({
1087
+ transpiledPath,
1088
+ methodExport: opts.methodExport,
1089
+ input: opts.input,
1090
+ auth: this.session.auth,
1091
+ databases: this.session.databases,
1092
+ authorizationToken,
1093
+ apiBaseUrl: getApiBaseUrl(),
1094
+ projectRoot: this.projectRoot
1095
+ });
1096
+ const duration = Date.now() - startTime;
1097
+ if (result.success) {
1098
+ log.info("Method complete", { requestId, method: opts.methodExport, duration });
1099
+ } else {
1100
+ log.warn("Method failed", {
1101
+ requestId,
1102
+ method: opts.methodExport,
1103
+ duration,
1104
+ error: result.error ? formatErrorForDisplay(result.error) : void 0
1105
+ });
1106
+ }
1107
+ logMethodExecution({
1108
+ requestId,
1109
+ sessionId: this.session.sessionId,
1110
+ methodExport: opts.methodExport,
1111
+ methodPath: opts.methodPath,
1112
+ input: opts.input,
1113
+ authorizationToken,
1114
+ databases: this.session.databases,
1115
+ result,
1116
+ duration
1117
+ });
1118
+ devRequestEvents.emitComplete({
1119
+ id: requestId,
1120
+ success: result.success,
1121
+ duration,
1122
+ error: result.error ? formatErrorForDisplay(result.error) : void 0
1123
+ });
1124
+ return {
1125
+ success: result.success,
1126
+ output: result.output,
1127
+ error: result.error ?? null,
1128
+ stdout: result.stdout,
1129
+ duration
1130
+ };
1131
+ } catch (err) {
1132
+ const message = err instanceof Error ? err.message : "Unknown error";
1133
+ const duration = Date.now() - startTime;
1134
+ log.error("Method error", { requestId, method: opts.methodExport, duration, error: message });
1135
+ logMethodExecution({
1136
+ requestId,
1137
+ sessionId: this.session.sessionId,
1138
+ methodExport: opts.methodExport,
1139
+ methodPath: opts.methodPath,
1140
+ input: opts.input,
1141
+ authorizationToken: "",
1142
+ databases: this.session.databases,
1143
+ result: { success: false, error: { message } },
1144
+ duration
1145
+ });
1146
+ devRequestEvents.emitComplete({
1147
+ id: requestId,
1148
+ success: false,
1149
+ duration,
1150
+ error: message
1151
+ });
1152
+ return { success: false, error: { message }, duration };
829
1153
  }
830
1154
  }
831
1155
  // Run a scenario: truncate tables → execute seed → impersonate roles.
@@ -841,16 +1165,15 @@ var DevRunner = class {
841
1165
  name: scenarioName,
842
1166
  timestamp: startTime
843
1167
  });
844
- log.info("runner Running scenario", { id: scenario.id, name: scenarioName });
1168
+ log.info("Scenario starting", { id: scenario.id, name: scenarioName });
845
1169
  try {
846
- log.debug("runner Truncating database for scenario");
1170
+ log.info("Resetting database for scenario");
847
1171
  const databases = await resetDevDatabase(this.appId, this.session.sessionId, "truncate");
848
1172
  this.session.databases = databases;
849
- log.debug("runner Transpiling scenario", { path: scenario.path });
1173
+ log.info("Transpiling scenario", { path: scenario.path });
850
1174
  const transpiledPath = await this.transpiler.transpile(scenario.path);
851
- log.debug("runner Fetching callback token for scenario");
852
1175
  const authorizationToken = await fetchCallbackToken(this.appId, this.session.sessionId);
853
- log.debug("runner Executing scenario seed", { export: scenario.export });
1176
+ log.info("Running scenario seed function", { export: scenario.export });
854
1177
  const result = await executeMethod({
855
1178
  transpiledPath,
856
1179
  methodExport: scenario.export,
@@ -863,7 +1186,14 @@ var DevRunner = class {
863
1186
  });
864
1187
  if (!result.success) {
865
1188
  const error = result.error?.message ?? "Scenario seed failed";
866
- log.error("runner Scenario seed failed", { id: scenario.id, error });
1189
+ log.error("Scenario seed function failed", { id: scenario.id, error });
1190
+ logScenarioExecution({
1191
+ sessionId: this.session.sessionId,
1192
+ scenario,
1193
+ databases: this.session.databases,
1194
+ result,
1195
+ duration: Date.now() - startTime
1196
+ });
867
1197
  devRequestEvents.emitScenarioComplete({
868
1198
  id: scenario.id,
869
1199
  success: false,
@@ -874,12 +1204,19 @@ var DevRunner = class {
874
1204
  return { success: false, databases, error };
875
1205
  }
876
1206
  if (scenario.roles.length > 0) {
877
- log.debug("runner Impersonating for scenario", { roles: scenario.roles });
1207
+ log.info("Setting role override for scenario", { roles: scenario.roles });
878
1208
  await impersonate(this.appId, this.session.sessionId, scenario.roles);
879
1209
  await this.refreshClientContext();
880
1210
  }
881
1211
  const duration = Date.now() - startTime;
882
- log.info("runner Scenario complete", { id: scenario.id, duration, roles: scenario.roles });
1212
+ log.info("Scenario complete", { id: scenario.id, duration, roles: scenario.roles });
1213
+ logScenarioExecution({
1214
+ sessionId: this.session.sessionId,
1215
+ scenario,
1216
+ databases: this.session.databases,
1217
+ result,
1218
+ duration
1219
+ });
883
1220
  devRequestEvents.emitScenarioComplete({
884
1221
  id: scenario.id,
885
1222
  success: true,
@@ -889,7 +1226,15 @@ var DevRunner = class {
889
1226
  return { success: true, databases };
890
1227
  } catch (err) {
891
1228
  const error = err instanceof Error ? err.message : "Unknown error";
892
- log.error("runner Scenario failed", { id: scenario.id, error });
1229
+ log.error("Scenario failed", { id: scenario.id, error });
1230
+ logScenarioExecution({
1231
+ sessionId: this.session.sessionId,
1232
+ scenario,
1233
+ databases: this.session.databases,
1234
+ result: null,
1235
+ infrastructureError: error,
1236
+ duration: Date.now() - startTime
1237
+ });
893
1238
  devRequestEvents.emitScenarioComplete({
894
1239
  id: scenario.id,
895
1240
  success: false,
@@ -910,7 +1255,7 @@ var DevRunner = class {
910
1255
  );
911
1256
  if (this.hadConnectionWarning) {
912
1257
  this.hadConnectionWarning = false;
913
- log.info("runner Connection restored");
1258
+ log.info("Connection to platform restored");
914
1259
  devRequestEvents.emitConnectionRestored();
915
1260
  }
916
1261
  if (request) {
@@ -919,31 +1264,31 @@ var DevRunner = class {
919
1264
  this.backoffMs = 1e3;
920
1265
  } catch (error) {
921
1266
  if (error instanceof DevPollError && error.statusCode === 404) {
922
- log.error("runner Session expired (404)");
1267
+ log.error("Dev session expired", { statusCode: 404 });
923
1268
  devRequestEvents.emitSessionExpired();
924
1269
  this.isRunning = false;
925
1270
  return;
926
1271
  }
927
1272
  if ((error instanceof DevPollError || error instanceof ApiError) && error.statusCode === 401) {
928
- log.warn("runner Auth token expired (401), attempting refresh");
1273
+ log.warn("Session token expired, re-authenticating");
929
1274
  const refreshed = await this.refreshAuth();
930
1275
  if (refreshed) {
931
1276
  this.backoffMs = 1e3;
932
1277
  continue;
933
1278
  }
934
- log.error("runner Auth refresh failed, stopping");
1279
+ log.error("Re-authentication failed");
935
1280
  devRequestEvents.emitSessionExpired();
936
1281
  this.isRunning = false;
937
1282
  return;
938
1283
  }
939
1284
  if (!this.hadConnectionWarning) {
940
1285
  this.hadConnectionWarning = true;
941
- log.warn("runner Connection lost, retrying...");
1286
+ log.warn("Lost connection to platform, retrying");
942
1287
  devRequestEvents.emitConnectionWarning(
943
1288
  "Lost connection to platform, retrying..."
944
1289
  );
945
1290
  }
946
- log.debug("runner Backing off", { ms: this.backoffMs });
1291
+ log.debug("Backing off", { ms: this.backoffMs });
947
1292
  await this.sleep(this.backoffMs);
948
1293
  this.backoffMs = Math.min(this.backoffMs * 2, 3e4);
949
1294
  }
@@ -957,9 +1302,9 @@ var DevRunner = class {
957
1302
  method: request.methodExport,
958
1303
  timestamp: startTime
959
1304
  });
960
- log.info("runner Request received", { requestId: request.requestId, method: request.methodExport });
1305
+ log.info("Method received", { requestId: request.requestId, method: request.methodExport });
961
1306
  try {
962
- log.debug("runner Transpiling", { path: request.methodPath });
1307
+ log.debug("Transpiling method", { path: request.methodPath });
963
1308
  const transpiledPath = await this.transpiler.transpile(request.methodPath);
964
1309
  const auth = request.roleOverride ? {
965
1310
  userId: this.session.auth.userId,
@@ -994,7 +1339,28 @@ var DevRunner = class {
994
1339
  devResult
995
1340
  );
996
1341
  const duration = Date.now() - startTime;
997
- log.info("runner Request complete", { requestId: request.requestId, success: result.success, duration });
1342
+ if (result.success) {
1343
+ log.info("Method complete", { requestId: request.requestId, method: request.methodExport, duration });
1344
+ } else {
1345
+ log.warn("Method failed", {
1346
+ requestId: request.requestId,
1347
+ method: request.methodExport,
1348
+ duration,
1349
+ error: result.error ? formatErrorForDisplay(result.error) : void 0
1350
+ });
1351
+ }
1352
+ logMethodExecution({
1353
+ requestId: request.requestId,
1354
+ sessionId: this.session.sessionId,
1355
+ methodExport: request.methodExport,
1356
+ methodPath: request.methodPath,
1357
+ input: request.input,
1358
+ roleOverride: request.roleOverride,
1359
+ authorizationToken: request.authorizationToken,
1360
+ databases: this.session.databases,
1361
+ result,
1362
+ duration
1363
+ });
998
1364
  devRequestEvents.emitComplete({
999
1365
  id: request.requestId,
1000
1366
  success: result.success,
@@ -1004,7 +1370,7 @@ var DevRunner = class {
1004
1370
  } catch (error) {
1005
1371
  const message = error instanceof Error ? error.message : "Unknown error";
1006
1372
  const duration = Date.now() - startTime;
1007
- log.error("runner Request failed", { requestId: request.requestId, duration, error: message });
1373
+ log.error("Method error", { requestId: request.requestId, method: request.methodExport, duration, error: message });
1008
1374
  try {
1009
1375
  await submitDevResult(
1010
1376
  this.appId,
@@ -1017,8 +1383,20 @@ var DevRunner = class {
1017
1383
  }
1018
1384
  );
1019
1385
  } catch (submitErr) {
1020
- log.error("runner Failed to submit error result", { error: submitErr instanceof Error ? submitErr.message : String(submitErr) });
1386
+ log.error("Failed to report method error to platform", { error: submitErr instanceof Error ? submitErr.message : String(submitErr) });
1021
1387
  }
1388
+ logMethodExecution({
1389
+ requestId: request.requestId,
1390
+ sessionId: this.session.sessionId,
1391
+ methodExport: request.methodExport,
1392
+ methodPath: request.methodPath,
1393
+ input: request.input,
1394
+ roleOverride: request.roleOverride,
1395
+ authorizationToken: request.authorizationToken,
1396
+ databases: this.session.databases,
1397
+ result: { success: false, error: { message } },
1398
+ duration: Date.now() - startTime
1399
+ });
1022
1400
  devRequestEvents.emitComplete({
1023
1401
  id: request.requestId,
1024
1402
  success: false,
@@ -1036,14 +1414,14 @@ var DevRunner = class {
1036
1414
  const POLL_INTERVAL = 2e3;
1037
1415
  const MAX_ATTEMPTS = 30;
1038
1416
  try {
1039
- log.info("runner Auth expired, requesting re-authentication");
1417
+ log.info("Session token expired, requesting re-authentication");
1040
1418
  const { url, token } = await requestDeviceAuth();
1041
1419
  devRequestEvents.emitAuthRefreshStart(url);
1042
1420
  try {
1043
1421
  const open = (await import("open")).default;
1044
1422
  await open(url);
1045
1423
  } catch {
1046
- log.warn("runner Could not open browser for auth \u2014 user must visit URL manually");
1424
+ log.warn("Could not open browser \u2014 visit URL to re-authenticate");
1047
1425
  }
1048
1426
  for (let i = 0; i < MAX_ATTEMPTS; i++) {
1049
1427
  await this.sleep(POLL_INTERVAL);
@@ -1054,7 +1432,7 @@ var DevRunner = class {
1054
1432
  if (result.userId) {
1055
1433
  setUserId(result.userId);
1056
1434
  }
1057
- log.info("runner Auth refreshed successfully");
1435
+ log.info("Re-authentication successful");
1058
1436
  devRequestEvents.emitAuthRefreshSuccess();
1059
1437
  return true;
1060
1438
  }
@@ -1062,11 +1440,11 @@ var DevRunner = class {
1062
1440
  break;
1063
1441
  }
1064
1442
  }
1065
- log.error("runner Auth refresh timed out or was denied");
1443
+ log.error("Re-authentication timed out or was denied");
1066
1444
  devRequestEvents.emitAuthRefreshFailed();
1067
1445
  return false;
1068
1446
  } catch (err) {
1069
- log.error("runner Auth refresh failed", { error: err instanceof Error ? err.message : String(err) });
1447
+ log.error("Re-authentication failed", { error: err instanceof Error ? err.message : String(err) });
1070
1448
  devRequestEvents.emitAuthRefreshFailed();
1071
1449
  return false;
1072
1450
  }
@@ -1075,42 +1453,72 @@ var DevRunner = class {
1075
1453
  return new Promise((resolve2) => setTimeout(resolve2, ms));
1076
1454
  }
1077
1455
  };
1078
- function formatErrorForDisplay(error) {
1079
- const parts = [];
1080
- if (error.message) {
1081
- parts.push(String(error.message));
1082
- }
1083
- const code = error.code ?? error.statusCode ?? error.status;
1084
- if (code !== void 0) {
1085
- parts.push(`(code: ${code})`);
1086
- }
1087
- if (error.body) {
1088
- parts.push(`Response: ${String(error.body).slice(0, 200)}`);
1089
- } else if (error.response) {
1090
- parts.push(`Response: ${String(error.response).slice(0, 200)}`);
1091
- }
1092
- if (error.cause && typeof error.cause === "object") {
1093
- const cause = error.cause;
1094
- if (cause.message) {
1095
- parts.push(`Caused by: ${cause.message}`);
1096
- }
1456
+
1457
+ // src/dev/browser-log.ts
1458
+ var ndjsonLog2 = new NdjsonLog("browser.ndjson");
1459
+ function initBrowserLog(projectRoot) {
1460
+ ndjsonLog2.init(projectRoot);
1461
+ }
1462
+ function appendBrowserLogEntries(entries) {
1463
+ for (const entry of entries) {
1464
+ ndjsonLog2.append({
1465
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1466
+ ...entry
1467
+ });
1097
1468
  }
1098
- return parts.join("\n");
1469
+ }
1470
+ function closeBrowserLog() {
1471
+ ndjsonLog2.close();
1099
1472
  }
1100
1473
 
1101
1474
  // src/dev/proxy.ts
1102
1475
  import http from "http";
1476
+ import { randomBytes as randomBytes3 } from "crypto";
1103
1477
  var DevProxy = class {
1104
- constructor(upstreamPort, clientContext, bindAddress = "127.0.0.1") {
1478
+ constructor(upstreamPort, clientContext, bindAddress = "127.0.0.1", browserAgentUrl) {
1105
1479
  this.upstreamPort = upstreamPort;
1106
1480
  this.clientContext = clientContext;
1107
1481
  this.bindAddress = bindAddress;
1482
+ this.browserAgentUrl = browserAgentUrl;
1108
1483
  }
1109
1484
  server = null;
1110
1485
  proxyPort = null;
1486
+ commandQueue = [];
1487
+ pendingResults = /* @__PURE__ */ new Map();
1488
+ lastBrowserPoll = 0;
1111
1489
  updateClientContext(context) {
1112
1490
  this.clientContext = context;
1113
- log.info("proxy Client context updated");
1491
+ log.info("Dev proxy context updated after role change");
1492
+ }
1493
+ /**
1494
+ * Whether a browser agent is actively polling for commands.
1495
+ * Based on whether we've seen a poll within the last 500ms.
1496
+ */
1497
+ isBrowserConnected() {
1498
+ return Date.now() - this.lastBrowserPoll < 500;
1499
+ }
1500
+ /**
1501
+ * Dispatch a browser command and wait for the result.
1502
+ * The command is queued for the browser agent to pick up via polling.
1503
+ * Returns a promise that resolves when the browser posts the result back.
1504
+ */
1505
+ dispatchBrowserCommand(steps, timeoutMs = 3e4) {
1506
+ if (!this.isBrowserConnected()) {
1507
+ return Promise.reject(
1508
+ new Error("No browser connected, please refresh the MindStudio preview")
1509
+ );
1510
+ }
1511
+ const id = randomBytes3(4).toString("hex");
1512
+ log.info("Browser command queued", { id, stepCount: steps.length, commands: steps.map((s) => s.command) });
1513
+ return new Promise((resolve2, reject) => {
1514
+ const timeout = setTimeout(() => {
1515
+ this.pendingResults.delete(id);
1516
+ log.warn("Browser command timed out", { id, pendingCount: this.pendingResults.size, queueLength: this.commandQueue.length });
1517
+ reject(new Error("Browser command timed out"));
1518
+ }, timeoutMs);
1519
+ this.pendingResults.set(id, { resolve: resolve2, timeout });
1520
+ this.commandQueue.push({ id, steps });
1521
+ });
1114
1522
  }
1115
1523
  async start(preferredPort) {
1116
1524
  const server = http.createServer((req, res) => {
@@ -1125,10 +1533,10 @@ var DevProxy = class {
1125
1533
  const assignedPort = await this.listenOnPort(server, port);
1126
1534
  this.server = server;
1127
1535
  this.proxyPort = assignedPort;
1128
- log.info("proxy Started", { port: assignedPort, bind: this.bindAddress });
1536
+ log.info("Dev proxy started", { port: assignedPort, bind: this.bindAddress });
1129
1537
  return assignedPort;
1130
1538
  } catch {
1131
- log.warn("proxy Port in use, trying next", { port });
1539
+ log.warn("Proxy port in use, trying next", { port });
1132
1540
  }
1133
1541
  }
1134
1542
  throw new Error("Failed to start proxy server");
@@ -1153,77 +1561,196 @@ var DevProxy = class {
1153
1561
  }
1154
1562
  stop() {
1155
1563
  if (this.server) {
1156
- log.info("proxy Stopping");
1564
+ log.info("Dev proxy stopping");
1157
1565
  this.server.close();
1158
1566
  this.server = null;
1159
1567
  this.proxyPort = null;
1160
1568
  }
1569
+ for (const [id, pending2] of this.pendingResults) {
1570
+ clearTimeout(pending2.timeout);
1571
+ pending2.resolve({ id, steps: [], error: "Proxy stopped" });
1572
+ }
1573
+ this.pendingResults.clear();
1574
+ this.commandQueue.length = 0;
1161
1575
  }
1162
1576
  getPort() {
1163
1577
  return this.proxyPort;
1164
1578
  }
1579
+ // ---------------------------------------------------------------------------
1580
+ // CORS helper
1581
+ // ---------------------------------------------------------------------------
1582
+ corsHeaders(req) {
1583
+ const origin = req.headers.origin;
1584
+ if (!origin) return {};
1585
+ return {
1586
+ "access-control-allow-origin": origin,
1587
+ "access-control-allow-private-network": "true"
1588
+ };
1589
+ }
1590
+ // ---------------------------------------------------------------------------
1591
+ // Request routing
1592
+ // ---------------------------------------------------------------------------
1165
1593
  handleRequest(clientReq, clientRes) {
1166
- const origin = clientReq.headers.origin;
1167
- if (clientReq.method === "OPTIONS" && origin) {
1594
+ if (clientReq.url?.startsWith("/__mindstudio_dev__/")) {
1595
+ if (clientReq.url === "/__mindstudio_dev__/logs" && clientReq.method === "POST") {
1596
+ this.handleBrowserLogs(clientReq, clientRes);
1597
+ return;
1598
+ }
1599
+ if (clientReq.url === "/__mindstudio_dev__/commands" && clientReq.method === "GET") {
1600
+ this.handleGetCommand(clientReq, clientRes);
1601
+ return;
1602
+ }
1603
+ if (clientReq.url === "/__mindstudio_dev__/results" && clientReq.method === "POST") {
1604
+ this.handlePostResult(clientReq, clientRes);
1605
+ return;
1606
+ }
1607
+ }
1608
+ if (clientReq.method === "OPTIONS" && clientReq.headers.origin) {
1168
1609
  clientRes.writeHead(204, {
1169
- "Access-Control-Allow-Origin": origin,
1170
- "Access-Control-Allow-Private-Network": "true",
1171
- "Access-Control-Allow-Methods": "GET, OPTIONS",
1172
- "Access-Control-Allow-Headers": "*"
1610
+ ...this.corsHeaders(clientReq),
1611
+ "access-control-allow-methods": "GET, POST, OPTIONS",
1612
+ "access-control-allow-headers": "*"
1173
1613
  });
1174
1614
  clientRes.end();
1175
1615
  return;
1176
1616
  }
1177
- const options = {
1178
- hostname: "127.0.0.1",
1179
- port: this.upstreamPort,
1180
- path: clientReq.url,
1181
- method: clientReq.method,
1182
- headers: { ...clientReq.headers, host: `localhost:${this.upstreamPort}` }
1183
- };
1184
- const upstreamReq = http.request(options, (upstreamRes) => {
1185
- const contentType = upstreamRes.headers["content-type"] ?? "";
1186
- const isHtml = contentType.startsWith("text/html");
1187
- if (isHtml) {
1188
- const chunks = [];
1189
- upstreamRes.on("data", (chunk) => chunks.push(chunk));
1190
- upstreamRes.on("end", () => {
1191
- let html = Buffer.concat(chunks).toString("utf-8");
1192
- html = injectClientContext(html, this.clientContext);
1193
- const headers = { ...upstreamRes.headers };
1194
- headers["content-length"] = String(Buffer.byteLength(html, "utf-8"));
1195
- headers["cache-control"] = "no-store, no-cache, must-revalidate";
1196
- delete headers["content-encoding"];
1617
+ this.forwardToUpstream(clientReq, clientRes);
1618
+ }
1619
+ // ---------------------------------------------------------------------------
1620
+ // Upstream forwarding
1621
+ // ---------------------------------------------------------------------------
1622
+ forwardToUpstream(clientReq, clientRes) {
1623
+ const cors = this.corsHeaders(clientReq);
1624
+ const upstreamReq = http.request(
1625
+ {
1626
+ hostname: "127.0.0.1",
1627
+ port: this.upstreamPort,
1628
+ path: clientReq.url,
1629
+ method: clientReq.method,
1630
+ headers: { ...clientReq.headers, host: `localhost:${this.upstreamPort}` }
1631
+ },
1632
+ (upstreamRes) => {
1633
+ const contentType = upstreamRes.headers["content-type"] ?? "";
1634
+ const isHtml = contentType.startsWith("text/html");
1635
+ if (isHtml) {
1636
+ const chunks = [];
1637
+ upstreamRes.on("data", (chunk) => chunks.push(chunk));
1638
+ upstreamRes.on("end", () => {
1639
+ let html = Buffer.concat(chunks).toString("utf-8");
1640
+ html = this.injectScripts(html);
1641
+ const headers = {
1642
+ ...upstreamRes.headers,
1643
+ ...cors,
1644
+ "content-length": String(Buffer.byteLength(html, "utf-8")),
1645
+ "cache-control": "no-store, no-cache, must-revalidate"
1646
+ };
1647
+ delete headers["content-encoding"];
1648
+ delete headers["etag"];
1649
+ log.debug("Dev proxy injected context into HTML", { path: clientReq.url, size: html.length });
1650
+ clientRes.writeHead(upstreamRes.statusCode ?? 200, headers);
1651
+ clientRes.end(html);
1652
+ });
1653
+ } else {
1654
+ const headers = {
1655
+ ...upstreamRes.headers,
1656
+ ...cors,
1657
+ "cache-control": "no-store, no-cache, must-revalidate"
1658
+ };
1197
1659
  delete headers["etag"];
1198
- if (origin) {
1199
- headers["access-control-allow-origin"] = origin;
1200
- headers["access-control-allow-private-network"] = "true";
1201
- }
1202
- log.debug("proxy HTML injected", { path: clientReq.url, size: html.length });
1203
1660
  clientRes.writeHead(upstreamRes.statusCode ?? 200, headers);
1204
- clientRes.end(html);
1205
- });
1206
- } else {
1207
- const headers = { ...upstreamRes.headers };
1208
- headers["cache-control"] = "no-store, no-cache, must-revalidate";
1209
- delete headers["etag"];
1210
- if (origin) {
1211
- headers["access-control-allow-origin"] = origin;
1212
- headers["access-control-allow-private-network"] = "true";
1661
+ upstreamRes.pipe(clientRes);
1213
1662
  }
1214
- clientRes.writeHead(upstreamRes.statusCode ?? 200, headers);
1215
- upstreamRes.pipe(clientRes);
1216
1663
  }
1217
- });
1664
+ );
1218
1665
  upstreamReq.on("error", (err) => {
1219
- log.warn("proxy Upstream error", { path: clientReq.url, error: err.message });
1666
+ log.warn("Dev proxy cannot reach dev server", { path: clientReq.url, error: err.message });
1220
1667
  clientRes.writeHead(502);
1221
1668
  clientRes.end(`Proxy error: ${err.message}`);
1222
1669
  });
1223
1670
  clientReq.pipe(upstreamReq);
1224
1671
  }
1672
+ // ---------------------------------------------------------------------------
1673
+ // Browser agent endpoints
1674
+ // ---------------------------------------------------------------------------
1675
+ handleBrowserLogs(clientReq, clientRes) {
1676
+ const chunks = [];
1677
+ clientReq.on("data", (chunk) => chunks.push(chunk));
1678
+ clientReq.on("end", () => {
1679
+ try {
1680
+ const body = Buffer.concat(chunks).toString("utf-8");
1681
+ const entries = JSON.parse(body);
1682
+ if (Array.isArray(entries)) {
1683
+ appendBrowserLogEntries(entries);
1684
+ }
1685
+ } catch {
1686
+ }
1687
+ clientRes.writeHead(204, this.corsHeaders(clientReq));
1688
+ clientRes.end();
1689
+ });
1690
+ }
1691
+ handleGetCommand(clientReq, clientRes) {
1692
+ this.lastBrowserPoll = Date.now();
1693
+ const command = this.commandQueue.shift();
1694
+ if (command) {
1695
+ log.info("Browser command dispatched to agent", { id: command.id, commands: command.steps.map((s) => s.command) });
1696
+ clientRes.writeHead(200, {
1697
+ ...this.corsHeaders(clientReq),
1698
+ "content-type": "application/json",
1699
+ "cache-control": "no-store"
1700
+ });
1701
+ clientRes.end(JSON.stringify(command));
1702
+ } else {
1703
+ clientRes.writeHead(204, {
1704
+ ...this.corsHeaders(clientReq),
1705
+ "cache-control": "no-store"
1706
+ });
1707
+ clientRes.end();
1708
+ }
1709
+ }
1710
+ handlePostResult(clientReq, clientRes) {
1711
+ const chunks = [];
1712
+ clientReq.on("data", (chunk) => chunks.push(chunk));
1713
+ clientReq.on("end", () => {
1714
+ try {
1715
+ const body = Buffer.concat(chunks).toString("utf-8");
1716
+ const result = JSON.parse(body);
1717
+ if (result?.id) {
1718
+ const pending2 = this.pendingResults.get(result.id);
1719
+ if (pending2) {
1720
+ log.info("Browser command result received", { id: result.id, stepCount: result.steps?.length, duration: result.duration });
1721
+ clearTimeout(pending2.timeout);
1722
+ this.pendingResults.delete(result.id);
1723
+ pending2.resolve(result);
1724
+ } else {
1725
+ log.warn("Browser command result received but no pending command found", { id: result.id, pendingIds: [...this.pendingResults.keys()] });
1726
+ }
1727
+ } else {
1728
+ log.warn("Browser command result received with no id", { bodyLength: body.length });
1729
+ }
1730
+ } catch (err) {
1731
+ log.warn("Browser command result parse error", { error: err instanceof Error ? err.message : String(err) });
1732
+ }
1733
+ clientRes.writeHead(204, this.corsHeaders(clientReq));
1734
+ clientRes.end();
1735
+ });
1736
+ }
1737
+ /**
1738
+ * Inject window.__MINDSTUDIO__ context and browser agent script tag into HTML.
1739
+ */
1740
+ injectScripts(html) {
1741
+ const contextScript = `<script>window.__MINDSTUDIO__=${JSON.stringify(this.clientContext)};</script>`;
1742
+ const agentUrl = this.browserAgentUrl || "https://unpkg.com/@mindstudio-ai/browser-agent/dist/index.js";
1743
+ const agentScript = `<script async src="${agentUrl}"></script>`;
1744
+ const injection = `${contextScript}
1745
+ ${agentScript}`;
1746
+ if (html.includes("</head>")) {
1747
+ return html.replace("</head>", `${injection}
1748
+ </head>`);
1749
+ }
1750
+ return injection + "\n" + html;
1751
+ }
1225
1752
  handleUpgrade(clientReq, clientSocket, head) {
1226
- log.debug("proxy WebSocket upgrade", { path: clientReq.url });
1753
+ log.debug("Dev proxy WebSocket upgrade", { path: clientReq.url });
1227
1754
  const options = {
1228
1755
  hostname: "127.0.0.1",
1229
1756
  port: this.upstreamPort,
@@ -1260,22 +1787,14 @@ var DevProxy = class {
1260
1787
  upstreamReq.end();
1261
1788
  }
1262
1789
  };
1263
- function injectClientContext(html, context) {
1264
- const script = `<script>window.__MINDSTUDIO__=${JSON.stringify(context)};</script>`;
1265
- if (html.includes("</head>")) {
1266
- return html.replace("</head>", `${script}
1267
- </head>`);
1268
- }
1269
- return script + "\n" + html;
1270
- }
1271
1790
 
1272
1791
  // src/dev/app-config.ts
1273
1792
  import { readFileSync, existsSync as existsSync2 } from "fs";
1274
- import { join as join3, dirname as dirname2 } from "path";
1793
+ import { join as join4, dirname as dirname2 } from "path";
1275
1794
  function detectAppConfig(cwd = process.cwd()) {
1276
- const appJsonPath = join3(cwd, "mindstudio.json");
1795
+ const appJsonPath = join4(cwd, "mindstudio.json");
1277
1796
  if (!existsSync2(appJsonPath)) {
1278
- log.debug("config mindstudio.json not found", { path: appJsonPath });
1797
+ log.debug("mindstudio.json not found", { path: appJsonPath });
1279
1798
  return null;
1280
1799
  }
1281
1800
  try {
@@ -1294,7 +1813,7 @@ function detectAppConfig(cwd = process.cwd()) {
1294
1813
  scenarios: parsed.scenarios ?? [],
1295
1814
  interfaces: parsed.interfaces ?? []
1296
1815
  };
1297
- log.debug("config Detected mindstudio.json", {
1816
+ log.info("Loaded mindstudio.json", {
1298
1817
  appId: config2.appId,
1299
1818
  roles: config2.roles.length,
1300
1819
  methods: config2.methods.length,
@@ -1304,7 +1823,7 @@ function detectAppConfig(cwd = process.cwd()) {
1304
1823
  });
1305
1824
  return config2;
1306
1825
  } catch (err) {
1307
- log.warn("config Failed to parse mindstudio.json", { error: err instanceof Error ? err.message : String(err) });
1826
+ log.warn("Failed to parse mindstudio.json", { error: err instanceof Error ? err.message : String(err) });
1308
1827
  return null;
1309
1828
  }
1310
1829
  }
@@ -1315,7 +1834,7 @@ function getWebInterfaceConfig(appConfig, cwd = process.cwd()) {
1315
1834
  if (!webInterface) {
1316
1835
  return null;
1317
1836
  }
1318
- const configPath = join3(cwd, webInterface.path);
1837
+ const configPath = join4(cwd, webInterface.path);
1319
1838
  if (!existsSync2(configPath)) {
1320
1839
  return null;
1321
1840
  }
@@ -1341,22 +1860,27 @@ function getWebProjectDir(appConfig, cwd = process.cwd()) {
1341
1860
  if (!webInterface) {
1342
1861
  return null;
1343
1862
  }
1344
- return dirname2(join3(cwd, webInterface.path));
1863
+ return dirname2(join4(cwd, webInterface.path));
1345
1864
  }
1346
1865
  function readTableSources(appConfig, cwd = process.cwd()) {
1347
1866
  const results = [];
1348
1867
  for (const table of appConfig.tables) {
1349
- const filePath = join3(cwd, table.path);
1868
+ const filePath = join4(cwd, table.path);
1350
1869
  if (!existsSync2(filePath)) {
1870
+ log.warn("Table source file not found", { table: table.export, path: table.path });
1351
1871
  continue;
1352
1872
  }
1353
1873
  try {
1354
1874
  const source = readFileSync(filePath, "utf-8");
1355
1875
  const name = table.export;
1356
1876
  results.push({ name, source });
1357
- } catch {
1877
+ } catch (err) {
1878
+ log.warn("Table source file unreadable", { table: table.export, path: table.path, error: err instanceof Error ? err.message : String(err) });
1358
1879
  }
1359
1880
  }
1881
+ if (results.length < appConfig.tables.length) {
1882
+ log.warn("Missing " + (appConfig.tables.length - results.length) + " table source file(s)", { found: results.length, expected: appConfig.tables.length });
1883
+ }
1360
1884
  return results;
1361
1885
  }
1362
1886
  function findDirsNeedingInstall(appConfig, cwd = process.cwd()) {
@@ -1365,9 +1889,9 @@ function findDirsNeedingInstall(appConfig, cwd = process.cwd()) {
1365
1889
  const firstMethodPath = appConfig.methods[0].path;
1366
1890
  const parts = firstMethodPath.split("/");
1367
1891
  for (let i = parts.length - 1; i >= 1; i--) {
1368
- const candidate = join3(cwd, ...parts.slice(0, i));
1369
- if (existsSync2(join3(candidate, "package.json"))) {
1370
- if (!existsSync2(join3(candidate, "node_modules"))) {
1892
+ const candidate = join4(cwd, ...parts.slice(0, i));
1893
+ if (existsSync2(join4(candidate, "package.json"))) {
1894
+ if (!existsSync2(join4(candidate, "node_modules"))) {
1371
1895
  dirs.push(candidate);
1372
1896
  }
1373
1897
  break;
@@ -1375,8 +1899,8 @@ function findDirsNeedingInstall(appConfig, cwd = process.cwd()) {
1375
1899
  }
1376
1900
  }
1377
1901
  const webProjectDir = getWebProjectDir(appConfig, cwd);
1378
- if (webProjectDir && existsSync2(join3(webProjectDir, "package.json"))) {
1379
- if (!existsSync2(join3(webProjectDir, "node_modules"))) {
1902
+ if (webProjectDir && existsSync2(join4(webProjectDir, "package.json"))) {
1903
+ if (!existsSync2(join4(webProjectDir, "node_modules"))) {
1380
1904
  dirs.push(webProjectDir);
1381
1905
  }
1382
1906
  }
@@ -1405,11 +1929,11 @@ function detectGitBranch() {
1405
1929
 
1406
1930
  // src/dev/table-watcher.ts
1407
1931
  import { watch } from "chokidar";
1408
- import { join as join4, dirname as dirname3, basename as basename2 } from "path";
1932
+ import { join as join5, dirname as dirname3, basename as basename2 } from "path";
1409
1933
  function watchTableFiles(tables, cwd, onChanged) {
1410
1934
  if (tables.length === 0) return () => {
1411
1935
  };
1412
- const filePaths = tables.map((t) => join4(cwd, t.path));
1936
+ const filePaths = tables.map((t) => join5(cwd, t.path));
1413
1937
  let syncTimer;
1414
1938
  const watcher = watch(filePaths, {
1415
1939
  ignoreInitial: true,
@@ -1422,13 +1946,13 @@ function watchTableFiles(tables, cwd, onChanged) {
1422
1946
  });
1423
1947
  const dirToFiles = /* @__PURE__ */ new Map();
1424
1948
  for (const table of tables) {
1425
- const absPath = join4(cwd, table.path);
1949
+ const absPath = join5(cwd, table.path);
1426
1950
  const dir = dirname3(absPath);
1427
1951
  const file = basename2(absPath);
1428
1952
  if (!dirToFiles.has(dir)) dirToFiles.set(dir, /* @__PURE__ */ new Set());
1429
1953
  dirToFiles.get(dir).add(file);
1430
1954
  }
1431
- log.info("table-watcher Watching files", {
1955
+ log.info("Watching table source files", {
1432
1956
  dirs: dirToFiles.size,
1433
1957
  tables: tables.length
1434
1958
  });
@@ -1440,9 +1964,9 @@ function watchTableFiles(tables, cwd, onChanged) {
1440
1964
 
1441
1965
  // src/dev/config-watcher.ts
1442
1966
  import { watch as watch2 } from "chokidar";
1443
- import { join as join5 } from "path";
1967
+ import { join as join6 } from "path";
1444
1968
  function watchConfigFile(cwd, onChanged) {
1445
- const configPath = join5(cwd, "mindstudio.json");
1969
+ const configPath = join6(cwd, "mindstudio.json");
1446
1970
  let debounceTimer;
1447
1971
  const watcher = watch2(configPath, {
1448
1972
  ignoreInitial: true,
@@ -1451,11 +1975,10 @@ function watchConfigFile(cwd, onChanged) {
1451
1975
  watcher.on("all", () => {
1452
1976
  clearTimeout(debounceTimer);
1453
1977
  debounceTimer = setTimeout(() => {
1454
- log.info("config-watcher mindstudio.json changed");
1455
1978
  onChanged();
1456
1979
  }, 500);
1457
1980
  });
1458
- log.info("config-watcher Watching mindstudio.json", { path: configPath });
1981
+ log.info("Watching mindstudio.json for changes", { path: configPath });
1459
1982
  return () => {
1460
1983
  clearTimeout(debounceTimer);
1461
1984
  watcher.close();
@@ -1482,6 +2005,7 @@ export {
1482
2005
  initLoggerHeadless,
1483
2006
  initLoggerInteractive,
1484
2007
  syncSchema,
2008
+ fetchCallbackToken,
1485
2009
  devRequestEvents,
1486
2010
  pollForRequest,
1487
2011
  submitProgress,
@@ -1493,7 +2017,11 @@ export {
1493
2017
  pollDeviceAuth,
1494
2018
  getEditorSessions,
1495
2019
  disconnectHeartbeat,
2020
+ initRequestLog,
2021
+ closeRequestLog,
1496
2022
  DevRunner,
2023
+ initBrowserLog,
2024
+ closeBrowserLog,
1497
2025
  DevProxy,
1498
2026
  detectAppConfig,
1499
2027
  getWebInterfaceConfig,
@@ -1505,4 +2033,4 @@ export {
1505
2033
  watchTableFiles,
1506
2034
  watchConfigFile
1507
2035
  };
1508
- //# sourceMappingURL=chunk-4CMGJFH3.js.map
2036
+ //# sourceMappingURL=chunk-XP4GPID6.js.map