@buda-ai/connector 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./chunk-MCKGQKYU.js";
2
3
 
3
4
  // src/config.ts
4
5
  import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
@@ -159,7 +160,7 @@ function getConnectorPaths(serverUrl, configRoot = path.join(homedir(), ".buda")
159
160
  return {
160
161
  configDir: configRoot,
161
162
  configPath: path.join(configRoot, "device.json"),
162
- logPath: path.join(configRoot, "connector.log"),
163
+ logPath: path.join(configRoot, "logs", "connector.log"),
163
164
  workdirRoot: path.join(configRoot, "agents"),
164
165
  scopedLegacyConfigPath: path.join(scopedLegacyDir, "device.json"),
165
166
  scopedLegacyWorkdirRoot: path.join(scopedLegacyDir, "agents")
@@ -184,6 +185,13 @@ function getConnectorRelayUrl(serverUrl, connectorId) {
184
185
  url.searchParams.set("connectorId", connectorId);
185
186
  return url.toString();
186
187
  }
188
+ function getConnectorTerminalUrl(serverUrl, connectorId, sessionId) {
189
+ const url = new URL("/api/connectors/terminal", serverUrl);
190
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
191
+ url.searchParams.set("connectorId", connectorId);
192
+ url.searchParams.set("sessionId", sessionId);
193
+ return url.toString();
194
+ }
187
195
  function getWorkdirRoot(launchConfig) {
188
196
  return launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || getConnectorPaths(getServerUrl(launchConfig)).workdirRoot;
189
197
  }
@@ -401,6 +409,349 @@ function parseLaunchConfig(raw) {
401
409
  };
402
410
  }
403
411
 
412
+ // src/console-dashboard.ts
413
+ import { closeSync, openSync } from "fs";
414
+ import { ReadStream, WriteStream } from "tty";
415
+ import { format, stripVTControlCharacters } from "util";
416
+ var MAX_LINES_PER_PANE = 500;
417
+ var MIN_DASHBOARD_WIDTH = 60;
418
+ var MIN_DASHBOARD_HEIGHT = 18;
419
+ var colors = {
420
+ cyan: "\x1B[36m",
421
+ dim: "\x1B[2m",
422
+ red: "\x1B[31m",
423
+ reset: "\x1B[0m",
424
+ reverse: "\x1B[7m",
425
+ yellow: "\x1B[33m"
426
+ };
427
+ var openControllingTerminal = () => {
428
+ const inputPath = process.platform === "win32" ? "CONIN$" : "/dev/tty";
429
+ const outputPath = process.platform === "win32" ? "CONOUT$" : "/dev/tty";
430
+ let inputFd;
431
+ let outputFd;
432
+ try {
433
+ inputFd = openSync(inputPath, "r");
434
+ outputFd = openSync(outputPath, "w");
435
+ const input = new ReadStream(inputFd);
436
+ const output = new WriteStream(outputFd);
437
+ return {
438
+ input,
439
+ output,
440
+ close: () => {
441
+ input.destroy();
442
+ output.destroy();
443
+ }
444
+ };
445
+ } catch {
446
+ if (inputFd !== void 0) closeSync(inputFd);
447
+ if (outputFd !== void 0) closeSync(outputFd);
448
+ return void 0;
449
+ }
450
+ };
451
+ var resolveDashboardStreams = () => {
452
+ if (process.stdin.isTTY && process.stdout.isTTY) {
453
+ return { input: process.stdin, output: process.stdout };
454
+ }
455
+ if (process.stdin.isTTY && process.stderr.isTTY) {
456
+ return { input: process.stdin, output: process.stderr };
457
+ }
458
+ return openControllingTerminal();
459
+ };
460
+ var sanitize = (value) => Array.from(stripVTControlCharacters(value).replaceAll("\r", "")).filter((character) => {
461
+ const code = character.charCodeAt(0);
462
+ return code === 9 || code >= 32;
463
+ }).join("");
464
+ var truncate = (value, width) => {
465
+ if (width <= 0) return "";
466
+ if (value.length <= width) return value.padEnd(width);
467
+ if (width === 1) return value.slice(0, 1);
468
+ return `${value.slice(0, width - 1)}~`;
469
+ };
470
+ var formatDuration = (durationMs) => {
471
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1e3));
472
+ const hours = Math.floor(totalSeconds / 3600);
473
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
474
+ const seconds = totalSeconds % 60;
475
+ return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
476
+ };
477
+ var createDashboardState = (startedAt = Date.now()) => ({
478
+ activePane: "activity",
479
+ activity: [],
480
+ agentDebug: [],
481
+ heartbeat: "Waiting",
482
+ relay: "Starting",
483
+ agent: "Starting",
484
+ server: "Unknown",
485
+ startedAt,
486
+ scroll: { activity: 0, agentDebug: 0 }
487
+ });
488
+ var classifyDashboardLine = (rawLine) => {
489
+ const line = sanitize(rawLine).trimEnd();
490
+ const prefix = "[buda-connector] ";
491
+ if (line.startsWith(prefix)) {
492
+ const message = line.slice(prefix.length);
493
+ const routineHealthCheck = /^relay request GET \/healthz\b/i.test(message);
494
+ return { pane: routineHealthCheck ? "agentDebug" : "activity", message };
495
+ }
496
+ return { pane: "agentDebug", message: line };
497
+ };
498
+ var classifyDashboardLevel = (fallback, formattedMessage, rawLine) => {
499
+ const normalizedLine = sanitize(rawLine).toLowerCase();
500
+ if (normalizedLine.includes("[error]") || normalizedLine.includes("[fatal]")) return "error";
501
+ if (normalizedLine.includes("[warning]") || normalizedLine.includes("[warn]")) return "warn";
502
+ const benignBunnyDiagnostic = [
503
+ "[bunny-agent:pi] getappendsystemprompt:",
504
+ "[bunny-agent:pi] loadskills",
505
+ "[bunny-agent:pi] resume:",
506
+ "[bunny-agent:pi] runner:"
507
+ ].some((prefix) => formattedMessage.toLowerCase().startsWith(prefix));
508
+ return benignBunnyDiagnostic ? "info" : fallback;
509
+ };
510
+ var updateStatusFromActivity = (state, message) => {
511
+ const normalized = message.toLowerCase();
512
+ if (normalized.startsWith("server ")) return { ...state, server: message.slice(7) };
513
+ if (normalized.startsWith("relay connected")) return { ...state, relay: "Connected" };
514
+ if (normalized.startsWith("relay reconnecting")) return { ...state, relay: "Retrying" };
515
+ if (normalized.startsWith("relay access revoked"))
516
+ return { ...state, relay: "Waiting for access" };
517
+ if (normalized.startsWith("relay disconnected") || normalized.startsWith("relay error") || normalized.startsWith("relay ping failed")) {
518
+ return { ...state, relay: "Disconnected" };
519
+ }
520
+ if (normalized.startsWith("bunny daemon listening") || normalized.startsWith("bunny daemon http")) {
521
+ return { ...state, agent: "Ready" };
522
+ }
523
+ if (normalized.startsWith("bunny daemon stopped")) return { ...state, agent: "Stopped" };
524
+ if (normalized.startsWith("heartbeat failed")) return { ...state, heartbeat: "Failed" };
525
+ return state;
526
+ };
527
+ var appendDashboardLine = (state, pane, level, message, time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })) => {
528
+ const current = state[pane];
529
+ const last = current.at(-1);
530
+ const nextLines = last?.message === message && last.level === level ? [...current.slice(0, -1), { ...last, count: last.count + 1, time }] : [...current, { count: 1, level, message, time }];
531
+ const trimmed = nextLines.slice(-MAX_LINES_PER_PANE);
532
+ const scroll = state.scroll[pane] > 0 ? { ...state.scroll, [pane]: Math.min(trimmed.length - 1, state.scroll[pane] + 1) } : state.scroll;
533
+ const next = { ...state, [pane]: trimmed, scroll };
534
+ return pane === "activity" ? updateStatusFromActivity(next, message) : next;
535
+ };
536
+ var scrollDashboardPane = (state, pane, delta) => {
537
+ const maxScroll = Math.max(0, state[pane].length - 1);
538
+ const nextScroll = delta === "follow" ? 0 : delta === "oldest" ? maxScroll : Math.max(0, Math.min(maxScroll, state.scroll[pane] + delta));
539
+ return { ...state, scroll: { ...state.scroll, [pane]: nextScroll } };
540
+ };
541
+ var parseDashboardMouseAction = (input, size) => {
542
+ const mouseEventPrefix = "\x1B[<";
543
+ if (!input.includes(mouseEventPrefix)) return void 0;
544
+ const event = input.split(mouseEventPrefix).at(-1);
545
+ const match = event?.match(/^(\d+);\d+;(\d+)[Mm]/);
546
+ if (!match) return void 0;
547
+ const button = Number(match[1]);
548
+ const row = Number(match[2]);
549
+ const availablePaneRows = Math.max(0, size.height - 6);
550
+ const activityHeight = Math.floor(availablePaneRows / 2);
551
+ const activityStart = 6;
552
+ const debugStart = activityStart + activityHeight;
553
+ const pane = row >= activityStart && row < debugStart ? "activity" : row >= debugStart && row < size.height ? "agentDebug" : void 0;
554
+ if (!pane) return void 0;
555
+ if (button === 64) return { pane, delta: 3 };
556
+ if (button === 65) return { pane, delta: -3 };
557
+ if (button === 0) return { pane };
558
+ return void 0;
559
+ };
560
+ var getTerminalCopyGesture = (termProgram = process.env.TERM_PROGRAM) => termProgram === "Apple_Terminal" ? "Fn+drag copy" : "Shift+drag copy";
561
+ var renderStatus = (state, width, now) => {
562
+ const innerWidth = width - 2;
563
+ const status = (value) => value;
564
+ return [
565
+ `+${"-".repeat(innerWidth)}+`,
566
+ `|${colors.cyan}${truncate(" Buda Connector", innerWidth)}${colors.reset}|`,
567
+ `|${truncate(` Relay ${status(state.relay)} Agent ${status(state.agent)} Heartbeat ${status(state.heartbeat)}`, innerWidth)}|`,
568
+ `|${truncate(` Server ${state.server} Uptime ${formatDuration(now - state.startedAt)}`, innerWidth)}|`,
569
+ `+${"-".repeat(innerWidth)}+`
570
+ ];
571
+ };
572
+ var renderPane = (title, lines, height, width, active, scroll) => {
573
+ const innerWidth = width - 2;
574
+ const contentHeight = Math.max(1, height - 2);
575
+ const end = Math.max(0, lines.length - scroll);
576
+ const visible = lines.slice(Math.max(0, end - contentHeight), end);
577
+ const headerSuffix = scroll === 0 ? "following" : `${scroll} lines behind`;
578
+ const titleText = ` ${title} ${active ? "[active]" : ""}`;
579
+ const headerFill = Math.max(1, innerWidth - titleText.length - headerSuffix.length);
580
+ const output = [
581
+ `${active ? colors.reverse : ""}+${titleText}${"-".repeat(headerFill)}${headerSuffix}+${colors.reset}`
582
+ ];
583
+ for (let index = 0; index < contentHeight; index += 1) {
584
+ const line = visible[index];
585
+ if (!line) {
586
+ output.push(`|${" ".repeat(innerWidth)}|`);
587
+ continue;
588
+ }
589
+ const levelColor = line.level === "error" ? colors.red : line.level === "warn" ? colors.yellow : colors.dim;
590
+ const count = line.count > 1 ? ` x${line.count}` : "";
591
+ const content = `${line.time} ${line.level.toUpperCase().padEnd(5)} ${line.message}${count}`;
592
+ output.push(`|${levelColor}${truncate(content, innerWidth)}${colors.reset}|`);
593
+ }
594
+ output.push(`+${"-".repeat(innerWidth)}+`);
595
+ return output;
596
+ };
597
+ var renderFooter = (width) => `${colors.dim}${truncate(` ${getTerminalCopyGesture()} | Click/wheel pane | Tab switch pane | F follow`, width)}${colors.reset}`;
598
+ var renderDashboard = (state, size, now = Date.now()) => {
599
+ const width = Math.max(10, size.width);
600
+ const height = Math.max(4, size.height);
601
+ if (width < MIN_DASHBOARD_WIDTH || height < MIN_DASHBOARD_HEIGHT) {
602
+ return [
603
+ truncate("Buda Connector", width),
604
+ truncate("Terminal too small for dashboard. Resize or restart with --plain.", width),
605
+ truncate(`Relay ${state.relay} | Agent ${state.agent}`, width),
606
+ truncate(`Heartbeat ${state.heartbeat}`, width)
607
+ ].slice(0, height).join("\n");
608
+ }
609
+ const availablePaneRows = height - 6;
610
+ const activityHeight = Math.floor(availablePaneRows / 2);
611
+ const debugHeight = availablePaneRows - activityHeight;
612
+ return [
613
+ ...renderStatus(state, width, now),
614
+ ...renderPane(
615
+ "Activity",
616
+ state.activity,
617
+ activityHeight,
618
+ width,
619
+ state.activePane === "activity",
620
+ state.scroll.activity
621
+ ),
622
+ ...renderPane(
623
+ "Agent Debug",
624
+ state.agentDebug,
625
+ debugHeight,
626
+ width,
627
+ state.activePane === "agentDebug",
628
+ state.scroll.agentDebug
629
+ ),
630
+ renderFooter(width)
631
+ ].join("\n");
632
+ };
633
+ var ConnectorConsoleDashboard = class {
634
+ constructor(streams) {
635
+ this.streams = streams;
636
+ }
637
+ state = createDashboardState();
638
+ stopped = false;
639
+ renderQueued = false;
640
+ originalConsole = {
641
+ error: console.error,
642
+ log: console.log,
643
+ warn: console.warn
644
+ };
645
+ start() {
646
+ console.log = (...args) => this.capture("info", args);
647
+ console.warn = (...args) => this.capture("warn", args);
648
+ console.error = (...args) => this.capture("error", args);
649
+ this.streams.output.write("\x1B[?1049h\x1B[?25l\x1B[?1000h\x1B[?1006h");
650
+ if (this.streams.input.isTTY) this.streams.input.setRawMode(true);
651
+ this.streams.input.resume();
652
+ this.streams.input.on("data", this.handleInput);
653
+ this.streams.output.on("resize", this.scheduleRender);
654
+ process.once("exit", this.stop);
655
+ this.scheduleRender();
656
+ }
657
+ updateStatus(update) {
658
+ this.state = { ...this.state, ...update };
659
+ this.scheduleRender();
660
+ }
661
+ stop = () => {
662
+ if (this.stopped) return;
663
+ this.stopped = true;
664
+ console.log = this.originalConsole.log;
665
+ console.warn = this.originalConsole.warn;
666
+ console.error = this.originalConsole.error;
667
+ this.streams.input.off("data", this.handleInput);
668
+ this.streams.output.off("resize", this.scheduleRender);
669
+ process.off("exit", this.stop);
670
+ if (this.streams.input.isTTY) this.streams.input.setRawMode(false);
671
+ this.streams.input.pause();
672
+ this.streams.output.write("\x1B[?1006l\x1B[?1000l\x1B[?25h\x1B[?1049l");
673
+ this.streams.close?.();
674
+ if (activeDashboard === this) activeDashboard = void 0;
675
+ };
676
+ capture(level, args) {
677
+ const formatted = format(...args);
678
+ for (const rawLine of formatted.split("\n")) {
679
+ const { pane, message } = classifyDashboardLine(rawLine);
680
+ if (!message) continue;
681
+ this.state = appendDashboardLine(
682
+ this.state,
683
+ pane,
684
+ classifyDashboardLevel(level, formatted, rawLine),
685
+ message
686
+ );
687
+ }
688
+ this.scheduleRender();
689
+ }
690
+ handleInput = (chunk) => {
691
+ const key = chunk.toString();
692
+ if (key === "") {
693
+ this.stop();
694
+ process.kill(process.pid, "SIGINT");
695
+ return;
696
+ }
697
+ const size = {
698
+ width: this.streams.output.columns || 80,
699
+ height: this.streams.output.rows || 24
700
+ };
701
+ const mouseAction = parseDashboardMouseAction(key, size);
702
+ if (mouseAction) {
703
+ this.state = { ...this.state, activePane: mouseAction.pane };
704
+ if (mouseAction.delta !== void 0) {
705
+ this.state = scrollDashboardPane(this.state, mouseAction.pane, mouseAction.delta);
706
+ }
707
+ } else if (key === " ") {
708
+ this.state = {
709
+ ...this.state,
710
+ activePane: this.state.activePane === "activity" ? "agentDebug" : "activity"
711
+ };
712
+ } else if (key === "f" || key === "F" || key === "\x1B[F") {
713
+ this.state = scrollDashboardPane(this.state, this.state.activePane, "follow");
714
+ } else if (key === "\x1B[H") {
715
+ this.state = scrollDashboardPane(this.state, this.state.activePane, "oldest");
716
+ } else if (key === "\x1B[A") {
717
+ this.state = scrollDashboardPane(this.state, this.state.activePane, 1);
718
+ } else if (key === "\x1B[B") {
719
+ this.state = scrollDashboardPane(this.state, this.state.activePane, -1);
720
+ } else if (key === "\x1B[5~") {
721
+ this.state = scrollDashboardPane(this.state, this.state.activePane, 8);
722
+ } else if (key === "\x1B[6~") {
723
+ this.state = scrollDashboardPane(this.state, this.state.activePane, -8);
724
+ } else {
725
+ return;
726
+ }
727
+ this.scheduleRender();
728
+ };
729
+ scheduleRender = () => {
730
+ if (this.stopped || this.renderQueued) return;
731
+ this.renderQueued = true;
732
+ setImmediate(() => {
733
+ this.renderQueued = false;
734
+ if (this.stopped) return;
735
+ const width = this.streams.output.columns || 80;
736
+ const height = this.streams.output.rows || 24;
737
+ this.streams.output.write(`\x1B[H\x1B[2J${renderDashboard(this.state, { width, height })}`);
738
+ });
739
+ };
740
+ };
741
+ var activeDashboard;
742
+ var startConnectorConsoleDashboard = (options = {}) => {
743
+ if (options.plain) return void 0;
744
+ const streams = options.streams ?? resolveDashboardStreams();
745
+ if (!streams?.input.isTTY || !streams.output.isTTY) return void 0;
746
+ if (activeDashboard) return activeDashboard;
747
+ activeDashboard = new ConnectorConsoleDashboard(streams);
748
+ activeDashboard.start();
749
+ return activeDashboard;
750
+ };
751
+ var updateConnectorDashboardStatus = (update) => {
752
+ activeDashboard?.updateStatus(update);
753
+ };
754
+
404
755
  // src/onboard.ts
405
756
  import { cancel, intro, isCancel, log, note, outro, spinner, text } from "@clack/prompts";
406
757
 
@@ -409,8 +760,78 @@ import { mkdir as mkdir3 } from "fs/promises";
409
760
  import { createDaemon } from "@bunny-agent/daemon";
410
761
 
411
762
  // src/logger.ts
412
- import { appendFile, mkdir as mkdir2 } from "fs/promises";
763
+ import { appendFile, mkdir as mkdir2, readdir, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
764
+ import path2 from "path";
765
+ var CONNECTOR_LOG_MAX_BYTES = 2 * 1024 * 1024;
766
+ var CONNECTOR_LOG_BACKUP_COUNT = 2;
767
+ var CONNECTOR_LOG_RETENTION_DAYS = 7;
413
768
  var pendingLogs = [];
769
+ var fileWriteQueue = Promise.resolve();
770
+ var lastPrunedDate = "";
771
+ var ignoreMissingFile = (error) => {
772
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
773
+ return;
774
+ }
775
+ throw error;
776
+ };
777
+ var formatConnectorLogDate = (date) => {
778
+ const year = date.getFullYear();
779
+ const month = String(date.getMonth() + 1).padStart(2, "0");
780
+ const day = String(date.getDate()).padStart(2, "0");
781
+ return `${year}-${month}-${day}`;
782
+ };
783
+ var getDatedConnectorLogPath = (baseLogPath, date = /* @__PURE__ */ new Date()) => {
784
+ const parsed = path2.parse(baseLogPath);
785
+ return path2.join(parsed.dir, `${parsed.name}-${formatConnectorLogDate(date)}${parsed.ext}`);
786
+ };
787
+ async function pruneConnectorLogs(baseLogPath, now = /* @__PURE__ */ new Date(), retentionDays = CONNECTOR_LOG_RETENTION_DAYS) {
788
+ const parsed = path2.parse(baseLogPath);
789
+ const prefix = `${parsed.name}-`;
790
+ const cutoff = new Date(now);
791
+ cutoff.setHours(0, 0, 0, 0);
792
+ cutoff.setDate(cutoff.getDate() - Math.max(0, retentionDays - 1));
793
+ const cutoffDate = formatConnectorLogDate(cutoff);
794
+ const files = await readdir(parsed.dir).catch(() => []);
795
+ const expired = files.filter((fileName) => {
796
+ if (!fileName.startsWith(prefix)) return false;
797
+ const date = fileName.slice(prefix.length, prefix.length + 10);
798
+ const suffix = fileName.slice(prefix.length + 10);
799
+ const isDatedLog = /^\d{4}-\d{2}-\d{2}$/.test(date);
800
+ const segment = suffix.startsWith(`${parsed.ext}.`) ? suffix.slice(parsed.ext.length + 1) : "";
801
+ const isLogSegment = suffix === parsed.ext || /^\d+$/.test(segment);
802
+ return isDatedLog && isLogSegment && date < cutoffDate;
803
+ });
804
+ await Promise.all(expired.map((fileName) => unlink3(path2.join(parsed.dir, fileName))));
805
+ return expired.length;
806
+ }
807
+ async function rotateConnectorLogIfNeeded(logPath, maxBytes = CONNECTOR_LOG_MAX_BYTES, backupCount = CONNECTOR_LOG_BACKUP_COUNT) {
808
+ const currentSize = await stat2(logPath).then((value) => value.size).catch((error) => {
809
+ ignoreMissingFile(error);
810
+ return 0;
811
+ });
812
+ if (currentSize < maxBytes) return false;
813
+ await unlink3(`${logPath}.${backupCount}`).catch(ignoreMissingFile);
814
+ for (let index = backupCount - 1; index >= 1; index -= 1) {
815
+ await rename2(`${logPath}.${index}`, `${logPath}.${index + 1}`).catch(ignoreMissingFile);
816
+ }
817
+ await rename2(logPath, `${logPath}.1`).catch(ignoreMissingFile);
818
+ return true;
819
+ }
820
+ var appendConnectorLog = (baseLogPath, line, now = /* @__PURE__ */ new Date()) => {
821
+ const date = formatConnectorLogDate(now);
822
+ const logPath = getDatedConnectorLogPath(baseLogPath, now);
823
+ const write = async () => {
824
+ if (lastPrunedDate !== date) {
825
+ await pruneConnectorLogs(baseLogPath, now).catch(() => void 0);
826
+ lastPrunedDate = date;
827
+ }
828
+ await rotateConnectorLogIfNeeded(logPath);
829
+ await appendFile(logPath, line);
830
+ };
831
+ const queuedWrite = fileWriteQueue.then(write, write);
832
+ fileWriteQueue = queuedWrite.catch(() => void 0);
833
+ return queuedWrite;
834
+ };
414
835
  async function writeLog(level, message, metadata) {
415
836
  const entry = {
416
837
  level,
@@ -421,8 +842,8 @@ async function writeLog(level, message, metadata) {
421
842
  pendingLogs.push(entry);
422
843
  if (pendingLogs.length > 100) pendingLogs.splice(0, pendingLogs.length - 100);
423
844
  const paths = getConnectorPaths(getServerUrl());
424
- await mkdir2(paths.configDir, { recursive: true }).catch(() => void 0);
425
- await appendFile(paths.logPath, `${JSON.stringify(entry)}
845
+ await mkdir2(path2.dirname(paths.logPath), { recursive: true }).catch(() => void 0);
846
+ await appendConnectorLog(paths.logPath, `${JSON.stringify(entry)}
426
847
  `).catch(() => void 0);
427
848
  const line = `[buda-connector] ${message}`;
428
849
  if (level === "error") console.error(line);
@@ -436,6 +857,347 @@ function restorePendingLogs(logs) {
436
857
  pendingLogs.unshift(...logs);
437
858
  }
438
859
 
860
+ // src/terminal.ts
861
+ import { randomBytes as randomBytes2 } from "crypto";
862
+ import path3 from "path";
863
+ import WebSocket, { WebSocketServer } from "ws";
864
+ var TERMINAL_IDLE_TTL_MS = 10 * 6e4;
865
+ var TERMINAL_HANDSHAKE_TIMEOUT_MS = 1e4;
866
+ var LOCAL_TERMINAL_GRANT_TTL_MS = 3e4;
867
+ var LOCAL_TERMINAL_CLOSE_SPAWN_FAILED = 4401;
868
+ var MAX_BUFFER_CHUNKS = 1e3;
869
+ var MAX_TERMINAL_FRAME_BYTES = 1024 * 1024;
870
+ var normalizeTerminalCloseCode = (code, fallback = 1011) => code >= 1e3 && code <= 1014 && ![1004, 1005, 1006].includes(code) || code >= 3e3 && code <= 4999 ? code : fallback;
871
+ var truncateTerminalCloseReason = (reason, maxBytes = 120) => {
872
+ let result = reason;
873
+ while (Buffer.byteLength(result, "utf8") > maxBytes) result = result.slice(0, -1);
874
+ return result;
875
+ };
876
+ var sessions = /* @__PURE__ */ new Map();
877
+ var localGrants = /* @__PURE__ */ new Map();
878
+ var installedServers = /* @__PURE__ */ new WeakSet();
879
+ var isSafeId = (value) => typeof value === "string" && /^[A-Za-z0-9_.:-]{1,160}$/.test(value);
880
+ var parseDimension = (value, fallback, min, max) => typeof value === "number" && Number.isSafeInteger(value) ? Math.max(min, Math.min(max, value)) : fallback;
881
+ var parseConnectorTerminalRequest = (request) => {
882
+ if (request.method !== "CONNECT" || request.path !== "/api/terminal") {
883
+ throw new Error("Invalid connector terminal request.");
884
+ }
885
+ if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
886
+ throw new Error("Connector terminal request body is invalid.");
887
+ }
888
+ const body = request.body;
889
+ if (!isSafeId(body.sessionId) || !isSafeId(body.tabId)) {
890
+ throw new Error("Connector terminal session identity is invalid.");
891
+ }
892
+ if (typeof body.token !== "string" || body.token.length < 32 || body.token.length > 2048) {
893
+ throw new Error("Connector terminal token is invalid.");
894
+ }
895
+ return {
896
+ sessionId: body.sessionId,
897
+ tabId: body.tabId,
898
+ token: body.token,
899
+ cols: parseDimension(body.cols, 80, 10, 500),
900
+ rows: parseDimension(body.rows, 24, 5, 200)
901
+ };
902
+ };
903
+ var clearIdleTimer = (session) => {
904
+ if (!session.idleTimer) return;
905
+ clearTimeout(session.idleTimer);
906
+ session.idleTimer = null;
907
+ };
908
+ var scheduleIdleCleanup = (sessionKey, session) => {
909
+ clearIdleTimer(session);
910
+ session.idleTimer = setTimeout(() => {
911
+ if (session.clients.size > 0 || sessions.get(sessionKey) !== session) return;
912
+ sessions.delete(sessionKey);
913
+ try {
914
+ session.pty.kill();
915
+ } catch {
916
+ }
917
+ }, TERMINAL_IDLE_TTL_MS);
918
+ session.idleTimer.unref();
919
+ };
920
+ var sendJson = (socket, value) => {
921
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(value));
922
+ };
923
+ var LocalTerminalUnavailableError = class extends Error {
924
+ };
925
+ var serializeErrorMessage = (error) => error instanceof Error ? error.message : "Unknown error";
926
+ var createTerminalSession = async (grant, nodePtyLoader) => {
927
+ let nodePty;
928
+ try {
929
+ nodePty = await nodePtyLoader();
930
+ } catch (error) {
931
+ void writeLog(
932
+ "error",
933
+ `local terminal could not load node-pty: ${serializeErrorMessage(error)}`
934
+ );
935
+ throw new LocalTerminalUnavailableError(
936
+ "Local terminal is unavailable because node-pty could not be loaded. The connector remains available for other capabilities."
937
+ );
938
+ }
939
+ const shell = process.platform === "win32" ? "powershell.exe" : process.env.SHELL || "/bin/bash";
940
+ const shellArgs = process.platform === "win32" ? [] : ["-l"];
941
+ let pty;
942
+ try {
943
+ pty = nodePty.spawn(shell, shellArgs, {
944
+ name: "xterm-256color",
945
+ cols: grant.cols,
946
+ rows: grant.rows,
947
+ cwd: path3.resolve(grant.cwd),
948
+ env: { ...process.env, TERM: "xterm-256color" }
949
+ });
950
+ } catch (error) {
951
+ const message = serializeErrorMessage(error);
952
+ void writeLog(
953
+ "error",
954
+ `local terminal failed to spawn shell=${shell} cwd=${grant.cwd}: ${message}`
955
+ );
956
+ throw new LocalTerminalUnavailableError(
957
+ "Local terminal is unavailable because node-pty could not start a shell. The connector remains available for other capabilities."
958
+ );
959
+ }
960
+ const session = {
961
+ pty,
962
+ clients: /* @__PURE__ */ new Set(),
963
+ buffer: [],
964
+ cols: grant.cols,
965
+ rows: grant.rows,
966
+ idleTimer: null
967
+ };
968
+ sessions.set(grant.sessionKey, session);
969
+ pty.onData((data) => {
970
+ session.buffer.push(data);
971
+ if (session.buffer.length > MAX_BUFFER_CHUNKS) session.buffer.shift();
972
+ for (const socket of session.clients) {
973
+ if (socket.readyState === WebSocket.OPEN) socket.send(data);
974
+ }
975
+ });
976
+ pty.onExit(({ exitCode }) => {
977
+ if (sessions.get(grant.sessionKey) === session) sessions.delete(grant.sessionKey);
978
+ clearIdleTimer(session);
979
+ for (const socket of session.clients) {
980
+ sendJson(socket, { type: "status", message: `exited (code ${exitCode})` });
981
+ socket.close(1e3, "Terminal exited");
982
+ }
983
+ session.clients.clear();
984
+ });
985
+ return session;
986
+ };
987
+ var attachLocalTerminal = async (socket, grant, nodePtyLoader) => {
988
+ const pendingMessages = [];
989
+ let session;
990
+ let started = false;
991
+ const handleMessage = (data) => {
992
+ if (!session) {
993
+ pendingMessages.push(data);
994
+ return;
995
+ }
996
+ try {
997
+ const message = JSON.parse(data);
998
+ if (message.type === "bridge-start") {
999
+ if (!started) {
1000
+ started = true;
1001
+ sendJson(socket, { type: "status", message: "connected" });
1002
+ for (const chunk of session.buffer) socket.send(chunk);
1003
+ }
1004
+ return;
1005
+ }
1006
+ if (message.type === "input" && typeof message.data === "string") {
1007
+ session.pty.write(message.data);
1008
+ return;
1009
+ }
1010
+ if (message.type === "resize") {
1011
+ const cols = parseDimension(message.cols, session.cols, 10, 500);
1012
+ const rows = parseDimension(message.rows, session.rows, 5, 200);
1013
+ if (cols !== session.cols || rows !== session.rows) {
1014
+ session.cols = cols;
1015
+ session.rows = rows;
1016
+ session.pty.resize(cols, rows);
1017
+ }
1018
+ return;
1019
+ }
1020
+ } catch {
1021
+ }
1022
+ session.pty.write(data);
1023
+ };
1024
+ socket.on("message", (raw) => handleMessage(raw.toString()));
1025
+ session = sessions.get(grant.sessionKey);
1026
+ if (!session) session = await createTerminalSession(grant, nodePtyLoader);
1027
+ else clearIdleTimer(session);
1028
+ session.clients.add(socket);
1029
+ for (const data of pendingMessages.splice(0)) handleMessage(data);
1030
+ const detach = () => {
1031
+ if (!session) return;
1032
+ session.clients.delete(socket);
1033
+ if (session.clients.size === 0) scheduleIdleCleanup(grant.sessionKey, session);
1034
+ };
1035
+ socket.once("close", detach);
1036
+ socket.once("error", detach);
1037
+ };
1038
+ var installConnectorTerminalEndpoint = (server, options = {}) => {
1039
+ if (installedServers.has(server)) return;
1040
+ installedServers.add(server);
1041
+ const terminalServer = new WebSocketServer({ noServer: true });
1042
+ server.on("upgrade", (request, socket, head) => {
1043
+ const url = new URL(request.url ?? "/", "http://connector.local");
1044
+ if (url.pathname !== "/api/terminal") return;
1045
+ const tokenHeader = request.headers["x-buda-local-terminal-token"];
1046
+ const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
1047
+ const grant = token ? localGrants.get(token) : void 0;
1048
+ if (!token || !grant || grant.expiresAt <= Date.now()) {
1049
+ if (token) localGrants.delete(token);
1050
+ socket.destroy();
1051
+ return;
1052
+ }
1053
+ localGrants.delete(token);
1054
+ terminalServer.handleUpgrade(request, socket, head, (client) => {
1055
+ void attachLocalTerminal(
1056
+ client,
1057
+ grant,
1058
+ options.nodePtyLoader ?? (() => import("./lib-UX6C35VI.js"))
1059
+ ).catch((error) => {
1060
+ const message = error instanceof LocalTerminalUnavailableError ? error.message : "Local terminal is unavailable. The connector remains available for other capabilities.";
1061
+ sendJson(client, { type: "error", fatal: true, message });
1062
+ client.close(LOCAL_TERMINAL_CLOSE_SPAWN_FAILED, "Local terminal unavailable");
1063
+ });
1064
+ });
1065
+ });
1066
+ server.once("close", () => {
1067
+ for (const client of terminalServer.clients) client.terminate();
1068
+ terminalServer.close();
1069
+ localGrants.clear();
1070
+ for (const session of sessions.values()) {
1071
+ clearIdleTimer(session);
1072
+ try {
1073
+ session.pty.kill();
1074
+ } catch {
1075
+ }
1076
+ }
1077
+ sessions.clear();
1078
+ });
1079
+ };
1080
+ var registerLocalTerminal = (input) => {
1081
+ if (!isSafeId(input.workspaceKey) || !isSafeId(input.tabId)) {
1082
+ throw new Error("Invalid local terminal workspace session.");
1083
+ }
1084
+ const token = randomBytes2(32).toString("base64url");
1085
+ localGrants.set(token, {
1086
+ sessionKey: `${input.workspaceKey}:${input.tabId}`,
1087
+ workspaceKey: input.workspaceKey,
1088
+ cwd: input.cwd,
1089
+ cols: input.cols,
1090
+ rows: input.rows,
1091
+ expiresAt: Date.now() + LOCAL_TERMINAL_GRANT_TTL_MS
1092
+ });
1093
+ const cleanupTimer = setTimeout(() => localGrants.delete(token), LOCAL_TERMINAL_GRANT_TTL_MS);
1094
+ cleanupTimer.unref();
1095
+ return token;
1096
+ };
1097
+ var toLocalTerminalUrl = (baseUrl) => {
1098
+ const url = new URL("/api/terminal", baseUrl);
1099
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1100
+ return url.toString();
1101
+ };
1102
+ var connectSocket = async (socket) => new Promise((resolve, reject) => {
1103
+ const onOpen = () => {
1104
+ socket.removeListener("error", onError);
1105
+ resolve();
1106
+ };
1107
+ const onError = (error) => {
1108
+ socket.removeListener("open", onOpen);
1109
+ reject(error);
1110
+ };
1111
+ socket.once("open", onOpen);
1112
+ socket.once("error", onError);
1113
+ });
1114
+ var proxyConnectorTerminalTunnel = async (input) => {
1115
+ const cloud = new WebSocket(
1116
+ getConnectorTerminalUrl(input.serverUrl, input.connectorId, input.sessionId),
1117
+ {
1118
+ headers: { "X-Buda-Terminal-Token": input.cloudToken },
1119
+ handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
1120
+ }
1121
+ );
1122
+ const local = new WebSocket(toLocalTerminalUrl(input.localBaseUrl), {
1123
+ headers: { "X-Buda-Local-Terminal-Token": input.localToken },
1124
+ handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
1125
+ });
1126
+ try {
1127
+ await Promise.all([connectSocket(cloud), connectSocket(local)]);
1128
+ } catch (error) {
1129
+ cloud.terminate();
1130
+ local.terminate();
1131
+ throw error;
1132
+ }
1133
+ let bridgeAccepted = false;
1134
+ let pendingBytes = 0;
1135
+ const pendingCloudFrames = [];
1136
+ const pendingLocalFrames = [];
1137
+ const bufferFrame = (frames, data, isBinary) => {
1138
+ const bytes = Array.isArray(data) ? data.reduce((total, chunk) => total + chunk.byteLength, 0) : data.byteLength;
1139
+ if (pendingBytes + bytes > MAX_TERMINAL_FRAME_BYTES) {
1140
+ cloud.close(1009, "Terminal handshake buffer too large");
1141
+ local.close(1009, "Terminal handshake buffer too large");
1142
+ return false;
1143
+ }
1144
+ pendingBytes += bytes;
1145
+ frames.push([data, isBinary]);
1146
+ return true;
1147
+ };
1148
+ cloud.on("message", (data, isBinary) => {
1149
+ if (!bridgeAccepted) {
1150
+ if (!isBinary && data.toString() === '{"type":"bridge-accepted"}') {
1151
+ bridgeAccepted = true;
1152
+ local.send(JSON.stringify({ type: "bridge-start" }));
1153
+ for (const [frame, binary] of pendingCloudFrames) {
1154
+ if (local.readyState === WebSocket.OPEN) local.send(frame, { binary });
1155
+ }
1156
+ for (const [frame, binary] of pendingLocalFrames) {
1157
+ if (cloud.readyState === WebSocket.OPEN) cloud.send(frame, { binary });
1158
+ }
1159
+ pendingCloudFrames.length = 0;
1160
+ pendingLocalFrames.length = 0;
1161
+ pendingBytes = 0;
1162
+ return;
1163
+ }
1164
+ bufferFrame(pendingCloudFrames, data, isBinary);
1165
+ return;
1166
+ }
1167
+ if (local.readyState === WebSocket.OPEN) local.send(data, { binary: isBinary });
1168
+ });
1169
+ local.on("message", (data, isBinary) => {
1170
+ if (!bridgeAccepted) {
1171
+ bufferFrame(pendingLocalFrames, data, isBinary);
1172
+ return;
1173
+ }
1174
+ if (cloud.readyState === WebSocket.OPEN) cloud.send(data, { binary: isBinary });
1175
+ });
1176
+ cloud.once("close", (code, reason) => {
1177
+ if (!bridgeAccepted) {
1178
+ void writeLog(
1179
+ "warn",
1180
+ `terminal cloud tunnel closed before readiness session=${input.sessionId} code=${code} reason=${truncateTerminalCloseReason(reason.toString())}`
1181
+ );
1182
+ }
1183
+ if (local.readyState === WebSocket.OPEN) {
1184
+ local.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
1185
+ }
1186
+ });
1187
+ local.once("close", (code, reason) => {
1188
+ if (cloud.readyState === WebSocket.OPEN) {
1189
+ cloud.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
1190
+ }
1191
+ });
1192
+ cloud.once("error", () => local.terminate());
1193
+ local.once("error", () => cloud.terminate());
1194
+ cloud.send(JSON.stringify({ type: "bridge-ready" }));
1195
+ await writeLog(
1196
+ "info",
1197
+ `terminal proxy connected session=${input.sessionId} workspace=${input.workspaceKey}`
1198
+ );
1199
+ };
1200
+
439
1201
  // src/bunny-daemon.ts
440
1202
  async function startEmbeddedBunnyDaemon(config, launchConfig) {
441
1203
  const host = getBunnyDaemonHost(launchConfig);
@@ -443,6 +1205,7 @@ async function startEmbeddedBunnyDaemon(config, launchConfig) {
443
1205
  const root = config.workdirRoot;
444
1206
  await mkdir3(root, { recursive: true });
445
1207
  const server = createDaemon({ host, port, root });
1208
+ installConnectorTerminalEndpoint(server);
446
1209
  await new Promise((resolve, reject) => {
447
1210
  const onError = (error) => {
448
1211
  server.off("listening", onListening);
@@ -485,11 +1248,11 @@ import { platform as platform3, release } from "os";
485
1248
  import { accessSync, constants, statSync } from "fs";
486
1249
  import { mkdir as mkdir4 } from "fs/promises";
487
1250
  import { platform as getPlatform, homedir as homedir2 } from "os";
488
- import path2 from "path";
1251
+ import path4 from "path";
489
1252
  var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
490
1253
  var MAX_SCREENCAST_DIMENSION = 2560;
491
1254
  var SESSION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,320}$/;
492
- var sessions = /* @__PURE__ */ new Map();
1255
+ var sessions2 = /* @__PURE__ */ new Map();
493
1256
  var sessionPromises = /* @__PURE__ */ new Map();
494
1257
  var getBrowserMode = () => {
495
1258
  const configured = process.env.BUDA_CONNECTOR_BROWSER_MODE?.trim().toLowerCase();
@@ -605,7 +1368,7 @@ var normalizeSessionId = (value) => {
605
1368
  return SESSION_ID_PATTERN.test(normalized) ? normalized : null;
606
1369
  };
607
1370
  async function ensureSession(sessionId, initialViewport = DEFAULT_VIEWPORT) {
608
- const activeSession = sessions.get(sessionId);
1371
+ const activeSession = sessions2.get(sessionId);
609
1372
  if (activeSession) return activeSession;
610
1373
  let sessionPromise = sessionPromises.get(sessionId);
611
1374
  if (!sessionPromise) {
@@ -624,9 +1387,9 @@ async function ensureSession(sessionId, initialViewport = DEFAULT_VIEWPORT) {
624
1387
  navigationQueue: Promise.resolve(),
625
1388
  clientIds: /* @__PURE__ */ new Set()
626
1389
  };
627
- sessions.set(sessionId, nextSession);
1390
+ sessions2.set(sessionId, nextSession);
628
1391
  browser.on("disconnected", () => {
629
- if (sessions.get(sessionId) === nextSession) sessions.delete(sessionId);
1392
+ if (sessions2.get(sessionId) === nextSession) sessions2.delete(sessionId);
630
1393
  });
631
1394
  return nextSession;
632
1395
  })();
@@ -671,9 +1434,9 @@ async function stopScreencast(s) {
671
1434
  s.isScreencasting = false;
672
1435
  }
673
1436
  async function closeSession(sessionId) {
674
- const activeSession = sessions.get(sessionId) ?? await sessionPromises.get(sessionId)?.catch(() => null);
1437
+ const activeSession = sessions2.get(sessionId) ?? await sessionPromises.get(sessionId)?.catch(() => null);
675
1438
  if (!activeSession) return;
676
- if (sessions.get(sessionId) === activeSession) sessions.delete(sessionId);
1439
+ if (sessions2.get(sessionId) === activeSession) sessions2.delete(sessionId);
677
1440
  await stopScreencast(activeSession);
678
1441
  await activeSession.cdpSession.detach().catch(() => {
679
1442
  });
@@ -682,7 +1445,7 @@ async function closeSession(sessionId) {
682
1445
  await writeLog("info", `browser session stopped ${sessionId}`);
683
1446
  }
684
1447
  async function closeAllBrowserSessions() {
685
- const sessionIds = /* @__PURE__ */ new Set([...sessions.keys(), ...sessionPromises.keys()]);
1448
+ const sessionIds = /* @__PURE__ */ new Set([...sessions2.keys(), ...sessionPromises.keys()]);
686
1449
  await Promise.all([...sessionIds].map((sessionId) => closeSession(sessionId)));
687
1450
  }
688
1451
  async function resizeBrowser(s, viewport) {
@@ -791,15 +1554,15 @@ var resolveScreenshotPath = (workdirRoot, filePath) => {
791
1554
  if (!workdirRoot) throw new Error("Connector workdir is unavailable for screenshots.");
792
1555
  const raw = typeof filePath === "string" ? filePath.trim() : "";
793
1556
  if (!raw) throw new Error("file_path is required");
794
- const agentRoot = path2.resolve(workdirRoot, "agent");
1557
+ const agentRoot = path4.resolve(workdirRoot, "agent");
795
1558
  const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
796
- const absolutePath = path2.resolve(agentRoot, relativeInput);
797
- if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path2.sep}`)) {
1559
+ const absolutePath = path4.resolve(agentRoot, relativeInput);
1560
+ if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path4.sep}`)) {
798
1561
  throw new Error("file_path must stay under the connector agent drive");
799
1562
  }
800
1563
  return {
801
1564
  absolutePath,
802
- relativePath: path2.relative(agentRoot, absolutePath).split(path2.sep).join("/")
1565
+ relativePath: path4.relative(agentRoot, absolutePath).split(path4.sep).join("/")
803
1566
  };
804
1567
  };
805
1568
  async function handleBrowserRequest(requestPath, method, body, context = {}) {
@@ -834,7 +1597,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
834
1597
  const input = body ?? {};
835
1598
  const clientId = normalizeSessionId(input.clientId);
836
1599
  if (!clientId) return jsonResponse(400, { ok: false, error: "valid client id required" });
837
- const s = sessions.get(sessionId);
1600
+ const s = sessions2.get(sessionId);
838
1601
  if (!s) return jsonResponse(200, { ok: true, closed: false });
839
1602
  s.clientIds.delete(clientId);
840
1603
  if (s.clientIds.size > 0) return jsonResponse(200, { ok: true, closed: false });
@@ -915,7 +1678,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
915
1678
  const input = body ?? {};
916
1679
  const s = await ensureSession(sessionId);
917
1680
  const screenshotPath = resolveScreenshotPath(context.workdirRoot, input.file_path);
918
- await mkdir4(path2.dirname(screenshotPath.absolutePath), { recursive: true });
1681
+ await mkdir4(path4.dirname(screenshotPath.absolutePath), { recursive: true });
919
1682
  await s.page.screenshot({
920
1683
  path: screenshotPath.absolutePath,
921
1684
  fullPage: input.fullPage !== false
@@ -943,7 +1706,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
943
1706
  return jsonResponse(200, { ok: true });
944
1707
  }
945
1708
  if (route === "location" && method === "GET") {
946
- const s = sessions.get(sessionId);
1709
+ const s = sessions2.get(sessionId);
947
1710
  if (!s) return jsonResponse(200, { ok: true, url: "", title: "" });
948
1711
  return jsonResponse(200, { ok: true, ...await getPageResult(s) });
949
1712
  }
@@ -974,7 +1737,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
974
1737
  if (s.screencastClients.size === 0) void stopScreencast(s);
975
1738
  };
976
1739
  checkInterval = setInterval(() => {
977
- if (sessions.get(sessionId) !== s) {
1740
+ if (sessions2.get(sessionId) !== s) {
978
1741
  cleanup();
979
1742
  controller.close();
980
1743
  }
@@ -999,15 +1762,15 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
999
1762
  // src/bundled.ts
1000
1763
  import { createHash as createHash3 } from "crypto";
1001
1764
  import { cpSync, existsSync, rmSync } from "fs";
1002
- import { readdir, readFile as readFile3 } from "fs/promises";
1003
- import path3 from "path";
1765
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
1766
+ import path5 from "path";
1004
1767
  import { fileURLToPath } from "url";
1005
- var PKG_ROOT = path3.resolve(fileURLToPath(import.meta.url), "../..");
1006
- var BUNDLED_DIR = path3.join(PKG_ROOT, "agent-templates");
1007
- var SYSTEM_SKILLS_DIR = path3.join(BUNDLED_DIR, "system-skills");
1008
- var SCRIPTS_DIR = path3.join(BUNDLED_DIR, "scripts");
1009
- var SHARED_DIR = path3.join(BUNDLED_DIR, "shared");
1010
- var MONOREPO_SRC = path3.resolve(PKG_ROOT, "../buda/agent-templates");
1768
+ var PKG_ROOT = path5.resolve(fileURLToPath(import.meta.url), "../..");
1769
+ var BUNDLED_DIR = path5.join(PKG_ROOT, "agent-templates");
1770
+ var SYSTEM_SKILLS_DIR = path5.join(BUNDLED_DIR, "system-skills");
1771
+ var SCRIPTS_DIR = path5.join(BUNDLED_DIR, "scripts");
1772
+ var SHARED_DIR = path5.join(BUNDLED_DIR, "shared");
1773
+ var MONOREPO_SRC = path5.resolve(PKG_ROOT, "../buda/agent-templates");
1011
1774
  function syncTemplatesFromMonorepo() {
1012
1775
  if (!existsSync(MONOREPO_SRC)) {
1013
1776
  throw new Error(`Cannot sync agent-templates: source missing at ${MONOREPO_SRC}`);
@@ -1016,12 +1779,12 @@ function syncTemplatesFromMonorepo() {
1016
1779
  cpSync(MONOREPO_SRC, BUNDLED_DIR, {
1017
1780
  recursive: true,
1018
1781
  dereference: true,
1019
- filter: (src) => !src.split(path3.sep).includes("extensions")
1782
+ filter: (src) => !src.split(path5.sep).includes("extensions")
1020
1783
  });
1021
1784
  }
1022
1785
  ensureBundledDir();
1023
1786
  function ensureBundledDir() {
1024
- const missingRequiredTemplate = !existsSync(path3.join(SHARED_DIR, "BOOTSTRAP.md"));
1787
+ const missingRequiredTemplate = !existsSync(path5.join(SHARED_DIR, "BOOTSTRAP.md"));
1025
1788
  if (existsSync(BUNDLED_DIR) && !missingRequiredTemplate) return;
1026
1789
  if (!existsSync(MONOREPO_SRC)) {
1027
1790
  console.warn(
@@ -1035,7 +1798,7 @@ function ensureBundledDir() {
1035
1798
  var cached = null;
1036
1799
  async function readBundledManifest() {
1037
1800
  if (cached) return cached;
1038
- const systemSkillNames = (await readdir(SYSTEM_SKILLS_DIR, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
1801
+ const systemSkillNames = (await readdir2(SYSTEM_SKILLS_DIR, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
1039
1802
  const version = await hashDir(BUNDLED_DIR);
1040
1803
  cached = {
1041
1804
  version,
@@ -1055,14 +1818,14 @@ async function hashDir(dir) {
1055
1818
  return hash.digest("hex");
1056
1819
  }
1057
1820
  async function walk(dir, visit, base = dir) {
1058
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
1821
+ const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
1059
1822
  entries.sort((a, b) => a.name.localeCompare(b.name));
1060
1823
  for (const entry of entries) {
1061
- const abs = path3.join(dir, entry.name);
1824
+ const abs = path5.join(dir, entry.name);
1062
1825
  if (entry.isDirectory()) {
1063
1826
  await walk(abs, visit, base);
1064
1827
  } else if (entry.isFile()) {
1065
- await visit(path3.relative(base, abs), abs);
1828
+ await visit(path5.relative(base, abs), abs);
1066
1829
  }
1067
1830
  }
1068
1831
  }
@@ -1239,18 +2002,18 @@ import {
1239
2002
  lstat,
1240
2003
  mkdir as mkdir5,
1241
2004
  open as open2,
1242
- readdir as readdir2,
2005
+ readdir as readdir3,
1243
2006
  readFile as readFile4,
1244
2007
  readlink,
1245
2008
  realpath,
1246
- rename as rename2,
2009
+ rename as rename3,
1247
2010
  rm,
1248
2011
  symlink,
1249
- unlink as unlink3
2012
+ unlink as unlink4
1250
2013
  } from "fs/promises";
1251
- import path4 from "path";
2014
+ import path6 from "path";
1252
2015
  var MIGRATION_VERSION = 1;
1253
- var MIGRATION_DIRECTORY = path4.join(".buda", "migrations", "legacy-workspace-v1");
2016
+ var MIGRATION_DIRECTORY = path6.join(".buda", "migrations", "legacy-workspace-v1");
1254
2017
  var JOURNAL_FILENAME = "journal.json";
1255
2018
  var IMMUTABLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
1256
2019
  var LEGACY_TEMPLATE_FILES = [
@@ -1295,9 +2058,9 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
1295
2058
  );
1296
2059
  }
1297
2060
  const canonicalRoot = await realpath(storageRoot);
1298
- const migrationDirectory = path4.join(canonicalRoot, MIGRATION_DIRECTORY);
2061
+ const migrationDirectory = path6.join(canonicalRoot, MIGRATION_DIRECTORY);
1299
2062
  await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
1300
- const journalPath = path4.join(migrationDirectory, JOURNAL_FILENAME);
2063
+ const journalPath = path6.join(migrationDirectory, JOURNAL_FILENAME);
1301
2064
  let journal = await readJournal(journalPath);
1302
2065
  if (journal) validateJournalPaths(journal, canonicalRoot, migrationDirectory);
1303
2066
  if (journal?.status === "complete") {
@@ -1347,7 +2110,7 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
1347
2110
  await writeJournal(journalPath, journal);
1348
2111
  await checkpoint(options, "before-copy", item.id);
1349
2112
  await rm(item.staging, { recursive: true, force: true });
1350
- await ensurePrivateDirectoryWithin(storageRoot, path4.dirname(item.staging));
2113
+ await ensurePrivateDirectoryWithin(storageRoot, path6.dirname(item.staging));
1351
2114
  await copyTree(item.source, item.staging, () => checkpoint(options, "during-copy", item.id));
1352
2115
  await checkpoint(options, "after-copy", item.id);
1353
2116
  item.phase = "copied";
@@ -1369,9 +2132,9 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
1369
2132
  }
1370
2133
  if (item.phase === "verified") {
1371
2134
  await checkpoint(options, "before-rename", item.id);
1372
- await ensurePrivateDirectoryWithin(storageRoot, path4.dirname(item.target));
1373
- await rename2(item.staging, item.target);
1374
- await syncDirectory(path4.dirname(item.target));
2135
+ await ensurePrivateDirectoryWithin(storageRoot, path6.dirname(item.target));
2136
+ await rename3(item.staging, item.target);
2137
+ await syncDirectory(path6.dirname(item.target));
1375
2138
  await checkpoint(options, "after-rename", item.id);
1376
2139
  item.phase = "committed";
1377
2140
  await writeJournal(journalPath, journal);
@@ -1402,12 +2165,12 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
1402
2165
  const ownership = owner ? "explicit-owner" : "shared-legacy";
1403
2166
  const label = owner ? `Legacy Connector root assigned to ${owner.spaceId}/${owner.agentId}` : "Shared legacy Connector root (ownership unassigned)";
1404
2167
  const sources = await inventoryLegacySources(storageRoot);
1405
- const stagingRoot = path4.join(migrationDirectory, "staging", randomUUID2());
2168
+ const stagingRoot = path6.join(migrationDirectory, "staging", randomUUID2());
1406
2169
  const items = sources.map(({ id, source }) => ({
1407
2170
  id,
1408
2171
  source,
1409
2172
  target: deriveTarget(storageRoot, id, owner),
1410
- staging: path4.join(stagingRoot, id),
2173
+ staging: path6.join(stagingRoot, id),
1411
2174
  phase: "pending"
1412
2175
  }));
1413
2176
  assertDistinctTargets(items);
@@ -1424,24 +2187,24 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
1424
2187
  };
1425
2188
  };
1426
2189
  var inventoryLegacySources = async (storageRoot) => {
1427
- const rootEntries = await readdir2(storageRoot, { withFileTypes: true });
2190
+ const rootEntries = await readdir3(storageRoot, { withFileTypes: true });
1428
2191
  const templateNames = new Set(LEGACY_TEMPLATE_FILES);
1429
2192
  for (const entry of rootEntries) {
1430
2193
  if (entry.name.endsWith(".md")) templateNames.add(entry.name);
1431
2194
  }
1432
2195
  const candidates = [
1433
- { id: "agent", source: path4.join(storageRoot, "agent") },
1434
- { id: "space", source: path4.join(storageRoot, "space") },
1435
- { id: "logs", source: path4.join(storageRoot, "logs") },
1436
- { id: "connector-log", source: path4.join(storageRoot, "connector.log") },
1437
- { id: "runtime-config", source: path4.join(storageRoot, "runtime-config") },
1438
- { id: "runtime-config-hidden", source: path4.join(storageRoot, ".runtime-config") },
1439
- { id: "runtime-config-file", source: path4.join(storageRoot, "runtime-config.json") },
1440
- { id: "runtime-config-hidden-file", source: path4.join(storageRoot, ".runtime-config.json") },
1441
- { id: "bunny-runtime", source: path4.join(storageRoot, ".bunny") },
2196
+ { id: "agent", source: path6.join(storageRoot, "agent") },
2197
+ { id: "space", source: path6.join(storageRoot, "space") },
2198
+ { id: "logs", source: path6.join(storageRoot, "logs") },
2199
+ { id: "connector-log", source: path6.join(storageRoot, "connector.log") },
2200
+ { id: "runtime-config", source: path6.join(storageRoot, "runtime-config") },
2201
+ { id: "runtime-config-hidden", source: path6.join(storageRoot, ".runtime-config") },
2202
+ { id: "runtime-config-file", source: path6.join(storageRoot, "runtime-config.json") },
2203
+ { id: "runtime-config-hidden-file", source: path6.join(storageRoot, ".runtime-config.json") },
2204
+ { id: "bunny-runtime", source: path6.join(storageRoot, ".bunny") },
1442
2205
  ...[...templateNames].sort().map((name) => ({
1443
2206
  id: `template-${name}`,
1444
- source: path4.join(storageRoot, name)
2207
+ source: path6.join(storageRoot, name)
1445
2208
  }))
1446
2209
  ];
1447
2210
  const found = [];
@@ -1454,29 +2217,29 @@ var inventoryLegacySources = async (storageRoot) => {
1454
2217
  `Legacy migration source ${candidate.id} must not be a symbolic link`
1455
2218
  );
1456
2219
  }
1457
- if (info.isDirectory() && (await readdir2(candidate.source)).length === 0) continue;
2220
+ if (info.isDirectory() && (await readdir3(candidate.source)).length === 0) continue;
1458
2221
  found.push(candidate);
1459
2222
  }
1460
2223
  return found;
1461
2224
  };
1462
2225
  var deriveTarget = (storageRoot, id, owner) => {
1463
- if (!owner) return path4.join(storageRoot, ".buda", "legacy", "shared", id);
1464
- const spaceRoot = path4.join(storageRoot, ".buda", "spaces", owner.spaceId);
1465
- const agentRoot = path4.join(spaceRoot, "agents", owner.agentId);
1466
- if (id === "agent") return path4.join(agentRoot, "workspace", "legacy-connector-root");
1467
- if (id === "space") return path4.join(spaceRoot, "shared", "legacy-connector-root");
2226
+ if (!owner) return path6.join(storageRoot, ".buda", "legacy", "shared", id);
2227
+ const spaceRoot = path6.join(storageRoot, ".buda", "spaces", owner.spaceId);
2228
+ const agentRoot = path6.join(spaceRoot, "agents", owner.agentId);
2229
+ if (id === "agent") return path6.join(agentRoot, "workspace", "legacy-connector-root");
2230
+ if (id === "space") return path6.join(spaceRoot, "shared", "legacy-connector-root");
1468
2231
  if (id.startsWith("template-")) {
1469
- return path4.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
2232
+ return path6.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
1470
2233
  }
1471
- return path4.join(agentRoot, "state", "legacy-connector-root", id);
2234
+ return path6.join(agentRoot, "state", "legacy-connector-root", id);
1472
2235
  };
1473
2236
  var copyTree = async (source, destination, onEntryCopied) => {
1474
2237
  const info = await lstat(source);
1475
2238
  if (info.isDirectory()) {
1476
2239
  await mkdir5(destination, { mode: info.mode & 4095 });
1477
- const entries = (await readdir2(source)).sort();
2240
+ const entries = (await readdir3(source)).sort();
1478
2241
  for (const entry of entries) {
1479
- await copyTree(path4.join(source, entry), path4.join(destination, entry), onEntryCopied);
2242
+ await copyTree(path6.join(source, entry), path6.join(destination, entry), onEntryCopied);
1480
2243
  }
1481
2244
  await chmod2(destination, info.mode & 4095);
1482
2245
  return;
@@ -1511,8 +2274,8 @@ var verifyTree = async (source, destination) => {
1511
2274
  }
1512
2275
  if (sourceInfo.isDirectory()) {
1513
2276
  const [sourceEntries, destinationEntries] = await Promise.all([
1514
- readdir2(source),
1515
- readdir2(destination)
2277
+ readdir3(source),
2278
+ readdir3(destination)
1516
2279
  ]);
1517
2280
  sourceEntries.sort();
1518
2281
  destinationEntries.sort();
@@ -1520,7 +2283,7 @@ var verifyTree = async (source, destination) => {
1520
2283
  throw verifyFailure("directory entries differ");
1521
2284
  }
1522
2285
  for (const entry of sourceEntries) {
1523
- await verifyTree(path4.join(source, entry), path4.join(destination, entry));
2286
+ await verifyTree(path6.join(source, entry), path6.join(destination, entry));
1524
2287
  }
1525
2288
  return;
1526
2289
  }
@@ -1567,7 +2330,7 @@ var readJournal = async (journalPath) => {
1567
2330
  };
1568
2331
  var writeJournal = async (journalPath, journal) => {
1569
2332
  journal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1570
- await ensurePrivateDirectory(path4.dirname(journalPath));
2333
+ await ensurePrivateDirectory(path6.dirname(journalPath));
1571
2334
  const temporaryPath = `${journalPath}.${process.pid}.${randomUUID2()}.tmp`;
1572
2335
  const handle = await open2(
1573
2336
  temporaryPath,
@@ -1582,11 +2345,11 @@ var writeJournal = async (journalPath, journal) => {
1582
2345
  await handle.close();
1583
2346
  }
1584
2347
  try {
1585
- await rename2(temporaryPath, journalPath);
2348
+ await rename3(temporaryPath, journalPath);
1586
2349
  await chmod2(journalPath, 384);
1587
- await syncDirectory(path4.dirname(journalPath));
2350
+ await syncDirectory(path6.dirname(journalPath));
1588
2351
  } finally {
1589
- await unlink3(temporaryPath).catch(() => void 0);
2352
+ await unlink4(temporaryPath).catch(() => void 0);
1590
2353
  }
1591
2354
  };
1592
2355
  var syncDirectory = async (directory) => {
@@ -1614,10 +2377,10 @@ var ensurePrivateDirectoryWithin = async (storageRoot, directory) => {
1614
2377
  if (directory !== storageRoot && !isPathInside(storageRoot, directory)) {
1615
2378
  throw corrupt("Legacy migration control path escapes protected storage");
1616
2379
  }
1617
- const relative = path4.relative(storageRoot, directory);
2380
+ const relative = path6.relative(storageRoot, directory);
1618
2381
  let current = storageRoot;
1619
- for (const segment of relative.split(path4.sep).filter(Boolean)) {
1620
- current = path4.join(current, segment);
2382
+ for (const segment of relative.split(path6.sep).filter(Boolean)) {
2383
+ current = path6.join(current, segment);
1621
2384
  await ensurePrivateDirectory(current);
1622
2385
  }
1623
2386
  };
@@ -1642,7 +2405,7 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
1642
2405
  const stagingPaths = /* @__PURE__ */ new Set();
1643
2406
  for (const item of journal.items) {
1644
2407
  const sourceRelative = sourceRelativePath(item.id);
1645
- if (!sourceRelative || item.source !== path4.join(storageRoot, sourceRelative)) {
2408
+ if (!sourceRelative || item.source !== path6.join(storageRoot, sourceRelative)) {
1646
2409
  throw corrupt("Legacy migration journal contains an unexpected source path");
1647
2410
  }
1648
2411
  if (item.target !== deriveTarget(storageRoot, item.id, journal.owner)) {
@@ -1651,8 +2414,8 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
1651
2414
  if (!isPathInside(migrationDirectory, item.staging)) {
1652
2415
  throw corrupt("Legacy migration journal staging path escapes the migration directory");
1653
2416
  }
1654
- const stagingParent = path4.dirname(item.staging);
1655
- if (path4.dirname(stagingParent) !== path4.join(migrationDirectory, "staging") || item.staging !== path4.join(stagingParent, item.id)) {
2417
+ const stagingParent = path6.dirname(item.staging);
2418
+ if (path6.dirname(stagingParent) !== path6.join(migrationDirectory, "staging") || item.staging !== path6.join(stagingParent, item.id)) {
1656
2419
  throw corrupt("Legacy migration journal staging path is not canonical");
1657
2420
  }
1658
2421
  stagingParents.add(stagingParent);
@@ -1682,7 +2445,7 @@ var sourceRelativePath = (id) => {
1682
2445
  if (known[id]) return known[id];
1683
2446
  if (!id.startsWith("template-")) return null;
1684
2447
  const name = id.slice("template-".length);
1685
- return name.endsWith(".md") && path4.basename(name) === name ? name : null;
2448
+ return name.endsWith(".md") && path6.basename(name) === name ? name : null;
1686
2449
  };
1687
2450
  var validateJournalOwnership = (journal) => {
1688
2451
  if (journal.ownership === "explicit-owner") {
@@ -1755,8 +2518,8 @@ var exists = async (filePath) => {
1755
2518
  }
1756
2519
  };
1757
2520
  var isPathInside = (parent, child) => {
1758
- const relative = path4.relative(parent, child);
1759
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
2521
+ const relative = path6.relative(parent, child);
2522
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
1760
2523
  };
1761
2524
  var isMissing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
1762
2525
  var isAlreadyExists2 = (error) => error instanceof Error && "code" in error && error.code === "EEXIST";
@@ -1768,9 +2531,9 @@ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT"
1768
2531
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
1769
2532
 
1770
2533
  // src/relay.ts
1771
- import path6 from "path";
2534
+ import path8 from "path";
1772
2535
  import { createNextHandler } from "@bunny-agent/daemon/nextjs";
1773
- import WebSocket from "ws";
2536
+ import WebSocket2 from "ws";
1774
2537
 
1775
2538
  // src/execution-identity.ts
1776
2539
  import {
@@ -1941,14 +2704,14 @@ import {
1941
2704
  copyFile as copyFile2,
1942
2705
  lstat as lstat2,
1943
2706
  mkdir as mkdir6,
1944
- readdir as readdir3,
2707
+ readdir as readdir4,
1945
2708
  readFile as readFile5,
1946
2709
  realpath as realpath2,
1947
2710
  rm as rm2,
1948
2711
  symlink as symlink2,
1949
2712
  writeFile as writeFile2
1950
2713
  } from "fs/promises";
1951
- import path5 from "path";
2714
+ import path7 from "path";
1952
2715
  var MOUNT_DIRECTORY = "mount";
1953
2716
  var ARCHIVE_MARKER = "archived.json";
1954
2717
  var AGENT_CONTEXT_TEMPLATE_FILES = [
@@ -1960,8 +2723,8 @@ var AGENT_CONTEXT_TEMPLATE_FILES = [
1960
2723
  "BOOTSTRAP.md",
1961
2724
  "MEMORY.md"
1962
2725
  ];
1963
- var deriveSystemVolume = (descriptor) => path5.join(
1964
- path5.dirname(path5.dirname(path5.dirname(path5.dirname(descriptor.sharedVolume)))),
2726
+ var deriveSystemVolume = (descriptor) => path7.join(
2727
+ path7.dirname(path7.dirname(path7.dirname(path7.dirname(descriptor.sharedVolume)))),
1965
2728
  "system"
1966
2729
  );
1967
2730
  var WorkspaceVolumeLifecycle = class {
@@ -1973,14 +2736,14 @@ var WorkspaceVolumeLifecycle = class {
1973
2736
  async mount() {
1974
2737
  await this.ensureVolumes();
1975
2738
  await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
1976
- const mountRoot = path5.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
2739
+ const mountRoot = path7.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
1977
2740
  await mkdir6(mountRoot, { recursive: true, mode: 448 });
1978
2741
  const mounts = {
1979
2742
  mountRoot,
1980
- agent: path5.join(mountRoot, "agent"),
1981
- space: path5.join(mountRoot, "space"),
1982
- system: path5.join(mountRoot, "system"),
1983
- temporary: path5.join(mountRoot, "tmp")
2743
+ agent: path7.join(mountRoot, "agent"),
2744
+ space: path7.join(mountRoot, "space"),
2745
+ system: path7.join(mountRoot, "system"),
2746
+ temporary: path7.join(mountRoot, "tmp")
1984
2747
  };
1985
2748
  await Promise.all([
1986
2749
  ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
@@ -1988,18 +2751,18 @@ var WorkspaceVolumeLifecycle = class {
1988
2751
  ensureVolumeLink(this.systemVolume, mounts.system),
1989
2752
  ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
1990
2753
  ]);
1991
- const daemonVolumes = path5.join(mountRoot, "volumes");
2754
+ const daemonVolumes = path7.join(mountRoot, "volumes");
1992
2755
  await mkdir6(daemonVolumes, { recursive: true, mode: 448 });
1993
2756
  await Promise.all([
1994
- ensureVolumeLink(this.descriptor.privateVolume, path5.join(daemonVolumes, "agent")),
1995
- ensureVolumeLink(this.descriptor.sharedVolume, path5.join(daemonVolumes, "space")),
1996
- ensureVolumeLink(this.systemVolume, path5.join(daemonVolumes, "system")),
1997
- ensureVolumeLink(this.descriptor.temporaryVolume, path5.join(daemonVolumes, "tmp"))
2757
+ ensureVolumeLink(this.descriptor.privateVolume, path7.join(daemonVolumes, "agent")),
2758
+ ensureVolumeLink(this.descriptor.sharedVolume, path7.join(daemonVolumes, "space")),
2759
+ ensureVolumeLink(this.systemVolume, path7.join(daemonVolumes, "system")),
2760
+ ensureVolumeLink(this.descriptor.temporaryVolume, path7.join(daemonVolumes, "tmp"))
1998
2761
  ]);
1999
2762
  return mounts;
2000
2763
  }
2001
2764
  async unmount() {
2002
- await rm2(path5.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
2765
+ await rm2(path7.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
2003
2766
  recursive: true,
2004
2767
  force: true
2005
2768
  });
@@ -2008,33 +2771,33 @@ var WorkspaceVolumeLifecycle = class {
2008
2771
  await this.unmount();
2009
2772
  await mkdir6(this.descriptor.stateVolume, { recursive: true, mode: 448 });
2010
2773
  await writeFile2(
2011
- path5.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
2774
+ path7.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
2012
2775
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
2013
2776
  `,
2014
2777
  { encoding: "utf8", mode: 384 }
2015
2778
  );
2016
2779
  }
2017
2780
  async restoreAgent() {
2018
- await rm2(path5.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
2781
+ await rm2(path7.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
2019
2782
  return this.mount();
2020
2783
  }
2021
2784
  async deleteAgent(options) {
2022
2785
  requireConfirmation(options);
2023
2786
  await this.unmount();
2024
- const agentRoot = path5.dirname(this.descriptor.privateVolume);
2787
+ const agentRoot = path7.dirname(this.descriptor.privateVolume);
2025
2788
  await rm2(agentRoot, { recursive: true, force: true });
2026
2789
  }
2027
2790
  async archiveSpace() {
2028
2791
  await mkdir6(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
2029
2792
  await writeFile2(
2030
- path5.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
2793
+ path7.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
2031
2794
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
2032
2795
  `,
2033
2796
  { encoding: "utf8", mode: 384 }
2034
2797
  );
2035
2798
  }
2036
2799
  async restoreSpace() {
2037
- await rm2(path5.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
2800
+ await rm2(path7.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
2038
2801
  }
2039
2802
  async deleteSpace(options) {
2040
2803
  requireConfirmation(options);
@@ -2046,11 +2809,11 @@ var WorkspaceVolumeLifecycle = class {
2046
2809
  const bootstrapped = [];
2047
2810
  const skipped = [];
2048
2811
  const entries = new Set(
2049
- (await readdir3(templateDirectory, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => entry.name)
2812
+ (await readdir4(templateDirectory, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => entry.name)
2050
2813
  );
2051
2814
  for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
2052
2815
  if (!entries.has(filename)) continue;
2053
- const destination = path5.join(this.descriptor.privateVolume, filename);
2816
+ const destination = path7.join(this.descriptor.privateVolume, filename);
2054
2817
  try {
2055
2818
  await lstat2(destination);
2056
2819
  skipped.push(filename);
@@ -2058,7 +2821,7 @@ var WorkspaceVolumeLifecycle = class {
2058
2821
  if (!isMissingFile(error)) throw error;
2059
2822
  try {
2060
2823
  await copyFile2(
2061
- path5.join(templateDirectory, filename),
2824
+ path7.join(templateDirectory, filename),
2062
2825
  destination,
2063
2826
  constants3.COPYFILE_EXCL
2064
2827
  );
@@ -2121,6 +2884,20 @@ var RELAY_RECONNECT_JITTER = 0.2;
2121
2884
  var RELAY_FAST_RECONNECT_MS = 500;
2122
2885
  var CONNECTOR_SOCKET_REPLACED_CLOSE_CODE = 4001;
2123
2886
  var CONNECTOR_SOCKET_REVOKED_CLOSE_CODE = 4003;
2887
+ var SAFE_TRACE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
2888
+ var getBudaCodingRunSessionId = (body) => {
2889
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
2890
+ const record = body;
2891
+ for (const key of ["systemEnv", "env"]) {
2892
+ const env = record[key];
2893
+ if (!env || typeof env !== "object" || Array.isArray(env)) continue;
2894
+ const value = env.BUDA_SESSION_ID;
2895
+ if (typeof value !== "string") continue;
2896
+ const normalized = value.trim();
2897
+ if (SAFE_TRACE_ID_PATTERN.test(normalized)) return normalized;
2898
+ }
2899
+ return void 0;
2900
+ };
2124
2901
  var shouldReconnectConnectorRelay = (closeCode) => closeCode !== CONNECTOR_SOCKET_REPLACED_CLOSE_CODE && closeCode !== CONNECTOR_SOCKET_REVOKED_CLOSE_CODE;
2125
2902
  var getConnectorRelayReconnectBaseDelay = (hadOpenConnection, backoffMs) => hadOpenConnection ? RELAY_FAST_RECONNECT_MS : backoffMs;
2126
2903
  var formatConnectorRelayError = (error, depth = 0) => {
@@ -2145,6 +2922,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2145
2922
  let pingTimer = null;
2146
2923
  let pongTimer = null;
2147
2924
  let stopped = false;
2925
+ let suspendedForAccessRevocation = false;
2148
2926
  let reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
2149
2927
  let hadOpenConnection = false;
2150
2928
  let activeRelayProtocol = relayProtocol;
@@ -2171,7 +2949,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2171
2949
  return Math.max(reconnectingEstablishedSocket ? 250 : 500, Math.round(base + jitter));
2172
2950
  };
2173
2951
  const scheduleReconnect = (reason) => {
2174
- if (stopped || reconnectTimer) return;
2952
+ if (stopped || suspendedForAccessRevocation || reconnectTimer) return;
2175
2953
  const delay = nextReconnectDelay();
2176
2954
  void writeLog("info", `relay reconnecting in ${delay}ms (${reason})`);
2177
2955
  clearTimers();
@@ -2186,7 +2964,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2186
2964
  }, delay);
2187
2965
  };
2188
2966
  const connect = () => {
2189
- if (stopped) return;
2967
+ if (stopped || suspendedForAccessRevocation) return;
2190
2968
  const relayUrl = getConnectorRelayUrl(config.serverUrl, config.connectorId);
2191
2969
  const oauthToken = launchConfig?.oauthToken?.trim();
2192
2970
  const token = config.token?.trim() || getConnectorToken(launchConfig);
@@ -2195,7 +2973,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2195
2973
  if (token) headers["X-Buda-Connector-Token"] = token;
2196
2974
  headers["X-Buda-Connector-Id"] = config.connectorId;
2197
2975
  if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
2198
- const ws = new WebSocket(relayUrl, {
2976
+ const ws = new WebSocket2(relayUrl, {
2199
2977
  headers: Object.keys(headers).length > 0 ? headers : void 0,
2200
2978
  handshakeTimeout: RELAY_HANDSHAKE_TIMEOUT_MS
2201
2979
  });
@@ -2214,7 +2992,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2214
2992
  hadOpenConnection = true;
2215
2993
  reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
2216
2994
  pingTimer = setInterval(() => {
2217
- if (ws.readyState !== WebSocket.OPEN) return;
2995
+ if (ws.readyState !== WebSocket2.OPEN) return;
2218
2996
  try {
2219
2997
  ws.ping();
2220
2998
  } catch (error) {
@@ -2244,17 +3022,22 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2244
3022
  config.connectorId,
2245
3023
  activeRelayProtocol,
2246
3024
  replayGuard,
2247
- workspaceRegistry
3025
+ workspaceRegistry,
3026
+ config.serverUrl,
3027
+ bunnyDaemon.baseUrl
2248
3028
  );
2249
3029
  });
2250
3030
  ws.on("close", (code) => {
2251
3031
  detach();
2252
3032
  if (!shouldReconnectConnectorRelay(code)) {
2253
- stopped = true;
2254
- void writeLog(
2255
- "warn",
2256
- code === CONNECTOR_SOCKET_REPLACED_CLOSE_CODE ? "relay replaced by another process for this connector" : "relay access revoked; restore access and restart the Connector to reconnect"
2257
- );
3033
+ clearTimers();
3034
+ if (code === CONNECTOR_SOCKET_REPLACED_CLOSE_CODE) {
3035
+ stopped = true;
3036
+ void writeLog("warn", "relay replaced by another process for this connector");
3037
+ } else {
3038
+ suspendedForAccessRevocation = true;
3039
+ void writeLog("warn", "relay access revoked; waiting for access to be restored");
3040
+ }
2258
3041
  return;
2259
3042
  }
2260
3043
  void writeLog("warn", "relay disconnected");
@@ -2280,6 +3063,19 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2280
3063
  void writeLog("info", `relay protocol key updated ${nextRelayProtocol.keyId}`);
2281
3064
  }
2282
3065
  },
3066
+ resumeAfterAccessRestore() {
3067
+ if (stopped || !suspendedForAccessRevocation) return;
3068
+ suspendedForAccessRevocation = false;
3069
+ reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
3070
+ hadOpenConnection = false;
3071
+ void writeLog("info", "relay access restored; reconnecting");
3072
+ try {
3073
+ connect();
3074
+ } catch (error) {
3075
+ void writeLog("warn", `relay reconnect threw: ${formatConnectorRelayError(error)}`);
3076
+ scheduleReconnect("access-restored");
3077
+ }
3078
+ },
2283
3079
  stop() {
2284
3080
  stopped = true;
2285
3081
  if (reconnectTimer) {
@@ -2291,8 +3087,8 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2291
3087
  }
2292
3088
  };
2293
3089
  }
2294
- async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry) {
2295
- if (!socket || socket.readyState !== WebSocket.OPEN) return;
3090
+ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl) {
3091
+ if (!socket || socket.readyState !== WebSocket2.OPEN) return;
2296
3092
  let message;
2297
3093
  try {
2298
3094
  message = JSON.parse(raw);
@@ -2309,6 +3105,52 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2309
3105
  return;
2310
3106
  }
2311
3107
  if (message.type === "event") {
3108
+ if (message.event.channel === "terminal-open") {
3109
+ try {
3110
+ if (!workspaceRegistry || !serverUrl || !localDaemonBaseUrl) {
3111
+ throw new ConnectorRelayProtocolError("PROTOCOL_UNSUPPORTED");
3112
+ }
3113
+ const identity = verifyConnectorExecutionIdentity({
3114
+ protocolVersion: message.event.payload.protocolVersion,
3115
+ executionIdentity: message.event.payload.executionIdentity,
3116
+ request: message.event.payload.request,
3117
+ protocol: relayProtocol,
3118
+ expectedConnectorId,
3119
+ replayGuard
3120
+ });
3121
+ const terminal = parseConnectorTerminalRequest(message.event.payload.request);
3122
+ if (terminal.sessionId !== message.event.sessionId) {
3123
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
3124
+ }
3125
+ const descriptor = await workspaceRegistry.resolve(identity.spaceId, identity.agentId);
3126
+ if (descriptor.spaceId !== identity.spaceId || descriptor.agentId !== identity.agentId) {
3127
+ throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
3128
+ }
3129
+ const mounts = await new WorkspaceVolumeLifecycle(descriptor).mount();
3130
+ const localToken = registerLocalTerminal({
3131
+ workspaceKey: descriptor.workspaceKey,
3132
+ tabId: terminal.tabId,
3133
+ cwd: mounts.agent,
3134
+ cols: terminal.cols,
3135
+ rows: terminal.rows
3136
+ });
3137
+ await proxyConnectorTerminalTunnel({
3138
+ serverUrl,
3139
+ connectorId: expectedConnectorId,
3140
+ sessionId: terminal.sessionId,
3141
+ cloudToken: terminal.token,
3142
+ localBaseUrl: localDaemonBaseUrl,
3143
+ localToken,
3144
+ workspaceKey: descriptor.workspaceKey
3145
+ });
3146
+ } catch (error) {
3147
+ await writeLog(
3148
+ "warn",
3149
+ `terminal open rejected: ${error instanceof Error ? error.message : String(error)}`
3150
+ );
3151
+ }
3152
+ return;
3153
+ }
2312
3154
  await writeLog("warn", "relay rejected unsigned browser input event");
2313
3155
  return;
2314
3156
  }
@@ -2334,6 +3176,15 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2334
3176
  }
2335
3177
  const controller = new AbortController();
2336
3178
  inflight.set(message.requestId, controller);
3179
+ const codingRunStartedAt = message.request.path === "/api/coding/run" ? Date.now() : void 0;
3180
+ const codingRunTrace = codingRunStartedAt !== void 0 && executionIdentity ? {
3181
+ requestId: message.requestId,
3182
+ connectorId: executionIdentity.connectorId,
3183
+ agentId: executionIdentity.agentId,
3184
+ runId: executionIdentity.runId,
3185
+ budaSessionId: getBudaCodingRunSessionId(message.request.body) ?? null
3186
+ } : void 0;
3187
+ let codingRunStatus = null;
2337
3188
  try {
2338
3189
  const requestPath = message.request.path;
2339
3190
  const isBrowserRequest = requestPath.startsWith("/browser/") || requestPath === "/browser";
@@ -2394,6 +3245,14 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2394
3245
  signal: controller.signal
2395
3246
  });
2396
3247
  }
3248
+ if (codingRunTrace && codingRunStartedAt !== void 0) {
3249
+ codingRunStatus = response.status;
3250
+ await writeLog("info", "relay request coding run started", {
3251
+ ...codingRunTrace,
3252
+ status: codingRunStatus,
3253
+ durationMs: Date.now() - codingRunStartedAt
3254
+ });
3255
+ }
2397
3256
  sendRelayMessage(socket, {
2398
3257
  type: "start",
2399
3258
  requestId: message.requestId,
@@ -2436,7 +3295,21 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2436
3295
  type: "end",
2437
3296
  requestId: message.requestId
2438
3297
  });
3298
+ if (codingRunTrace && codingRunStartedAt !== void 0) {
3299
+ await writeLog("info", "relay request coding run completed", {
3300
+ ...codingRunTrace,
3301
+ status: codingRunStatus,
3302
+ durationMs: Date.now() - codingRunStartedAt
3303
+ });
3304
+ }
2439
3305
  } catch (error) {
3306
+ if (codingRunTrace && codingRunStartedAt !== void 0) {
3307
+ await writeLog("error", "relay request coding run error", {
3308
+ ...codingRunTrace,
3309
+ status: codingRunStatus,
3310
+ durationMs: Date.now() - codingRunStartedAt
3311
+ });
3312
+ }
2440
3313
  sendRelayError(socket, message.requestId, error);
2441
3314
  } finally {
2442
3315
  inflight.delete(message.requestId);
@@ -2461,12 +3334,12 @@ var normalizeWorkspaceRequest = (request, mounts) => {
2461
3334
  }
2462
3335
  const body = request.body;
2463
3336
  const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
2464
- const normalized = path6.posix.normalize(cwd);
3337
+ const normalized = path8.posix.normalize(cwd);
2465
3338
  const [mount, relative] = normalized === "/agent" || normalized.startsWith("/agent/") ? [mounts.agent, normalized.slice("/agent".length)] : normalized === "/space" || normalized.startsWith("/space/") ? [mounts.space, normalized.slice("/space".length)] : [];
2466
3339
  if (!mount || relative === void 0) {
2467
3340
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
2468
3341
  }
2469
- return { ...request, body: { ...body, cwd: path6.join(mount, relative) } };
3342
+ return { ...request, body: { ...body, cwd: path8.join(mount, relative) } };
2470
3343
  };
2471
3344
  function sendRelayError(socket, requestId, error) {
2472
3345
  sendRelayMessage(socket, {
@@ -2477,7 +3350,7 @@ function sendRelayError(socket, requestId, error) {
2477
3350
  });
2478
3351
  }
2479
3352
  function sendRelayMessage(socket, message) {
2480
- if (socket.readyState !== WebSocket.OPEN) return;
3353
+ if (socket.readyState !== WebSocket2.OPEN) return;
2481
3354
  socket.send(JSON.stringify(message));
2482
3355
  }
2483
3356
  function headersToRecord(headers) {
@@ -2497,14 +3370,14 @@ function getHeaderValue(headers, name) {
2497
3370
  }
2498
3371
 
2499
3372
  // src/workdir-layout.ts
2500
- import { lstat as lstat3, mkdir as mkdir7, symlink as symlink3, unlink as unlink4 } from "fs/promises";
2501
- import path7 from "path";
3373
+ import { lstat as lstat3, mkdir as mkdir7, symlink as symlink3, unlink as unlink5 } from "fs/promises";
3374
+ import path9 from "path";
2502
3375
  async function provisionWorkdirLayout(workdirRoot) {
2503
3376
  const manifest = await readBundledManifest();
2504
3377
  await mkdir7(workdirRoot, { recursive: true });
2505
- await mkdir7(path7.join(workdirRoot, "system", ".agents"), { recursive: true });
2506
- const systemSkillsLink = path7.join(workdirRoot, "system", ".agents", "skills");
2507
- const systemScriptsLink = path7.join(workdirRoot, "system", "scripts");
3378
+ await mkdir7(path9.join(workdirRoot, "system", ".agents"), { recursive: true });
3379
+ const systemSkillsLink = path9.join(workdirRoot, "system", ".agents", "skills");
3380
+ const systemScriptsLink = path9.join(workdirRoot, "system", "scripts");
2508
3381
  await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
2509
3382
  await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
2510
3383
  await writeLog("info", "workdir layout provisioned", {
@@ -2524,7 +3397,7 @@ async function ensureSymlink(target, linkPath) {
2524
3397
  try {
2525
3398
  const existing = await lstat3(linkPath);
2526
3399
  if (existing.isSymbolicLink()) {
2527
- await unlink4(linkPath);
3400
+ await unlink5(linkPath);
2528
3401
  } else if (existing.isDirectory()) {
2529
3402
  await writeLog(
2530
3403
  "warn",
@@ -2532,11 +3405,11 @@ async function ensureSymlink(target, linkPath) {
2532
3405
  );
2533
3406
  return;
2534
3407
  } else {
2535
- await unlink4(linkPath);
3408
+ await unlink5(linkPath);
2536
3409
  }
2537
3410
  } catch {
2538
3411
  }
2539
- await mkdir7(path7.dirname(linkPath), { recursive: true });
3412
+ await mkdir7(path9.dirname(linkPath), { recursive: true });
2540
3413
  await symlink3(target, linkPath, "dir");
2541
3414
  }
2542
3415
 
@@ -2549,11 +3422,11 @@ import {
2549
3422
  open as open3,
2550
3423
  readFile as readFile6,
2551
3424
  realpath as realpath3,
2552
- rename as rename3,
3425
+ rename as rename4,
2553
3426
  statfs,
2554
- unlink as unlink5
3427
+ unlink as unlink6
2555
3428
  } from "fs/promises";
2556
- import path8 from "path";
3429
+ import path10 from "path";
2557
3430
  var REGISTRY_VERSION = 1;
2558
3431
  var REGISTRY_DIRECTORY = ".buda";
2559
3432
  var REGISTRY_FILENAME = "workspace-registry.json";
@@ -2577,9 +3450,9 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
2577
3450
  await mkdir8(storageRoot, { recursive: true, mode: 448 });
2578
3451
  await rejectUnsafeFilesystem(storageRoot);
2579
3452
  const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
2580
- const registryDirectory = path8.join(canonicalRoot, REGISTRY_DIRECTORY);
3453
+ const registryDirectory = path10.join(canonicalRoot, REGISTRY_DIRECTORY);
2581
3454
  await ensurePrivateDirectory2(registryDirectory);
2582
- const registryPath = path8.join(registryDirectory, REGISTRY_FILENAME);
3455
+ const registryPath = path10.join(registryDirectory, REGISTRY_FILENAME);
2583
3456
  const document = await withExclusiveFileLock(registryPath, async () => {
2584
3457
  let current;
2585
3458
  try {
@@ -2655,16 +3528,16 @@ var readRegistryDocument = async (registryPath) => {
2655
3528
  }
2656
3529
  };
2657
3530
  var deriveWorkspace = (storageRoot, spaceId, agentId) => {
2658
- const spaceRoot = path8.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
2659
- const agentRoot = path8.join(spaceRoot, "agents", agentId);
3531
+ const spaceRoot = path10.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
3532
+ const agentRoot = path10.join(spaceRoot, "agents", agentId);
2660
3533
  return {
2661
3534
  workspaceKey: `${spaceId}:${agentId}`,
2662
3535
  spaceId,
2663
3536
  agentId,
2664
- privateVolume: path8.join(agentRoot, "workspace"),
2665
- sharedVolume: path8.join(spaceRoot, "shared"),
2666
- stateVolume: path8.join(agentRoot, "state"),
2667
- temporaryVolume: path8.join(agentRoot, "tmp")
3537
+ privateVolume: path10.join(agentRoot, "workspace"),
3538
+ sharedVolume: path10.join(spaceRoot, "shared"),
3539
+ stateVolume: path10.join(agentRoot, "state"),
3540
+ temporaryVolume: path10.join(agentRoot, "tmp")
2668
3541
  };
2669
3542
  };
2670
3543
  var parseRegistry = (raw) => {
@@ -2785,10 +3658,10 @@ var writeRegistryAtomically = async (registryPath, document) => {
2785
3658
  await handle.close();
2786
3659
  }
2787
3660
  try {
2788
- await rename3(temporaryPath, registryPath);
3661
+ await rename4(temporaryPath, registryPath);
2789
3662
  await chmod3(registryPath, 384);
2790
3663
  } catch (error) {
2791
- await unlink5(temporaryPath).catch(() => void 0);
3664
+ await unlink6(temporaryPath).catch(() => void 0);
2792
3665
  throw error;
2793
3666
  }
2794
3667
  };
@@ -2811,8 +3684,8 @@ var assertContained = (root, candidate) => {
2811
3684
  if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
2812
3685
  };
2813
3686
  var isPathInside2 = (parent, child) => {
2814
- const relative = path8.relative(parent, child);
2815
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
3687
+ const relative = path10.relative(parent, child);
3688
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path10.sep}`) && !path10.isAbsolute(relative);
2816
3689
  };
2817
3690
  var toDescriptor = (workspace) => ({
2818
3691
  workspaceKey: workspace.workspaceKey,
@@ -2842,7 +3715,7 @@ var corrupt2 = (message) => new WorkspaceRegistryError("REGISTRY_CORRUPT", messa
2842
3715
  var collision = (message) => new WorkspaceRegistryError("WORKSPACE_COLLISION", message);
2843
3716
 
2844
3717
  // src/onboard.ts
2845
- async function runOnboard(launchConfig) {
3718
+ async function runOnboard(launchConfig, options = {}) {
2846
3719
  intro("Welcome to Buda Connector");
2847
3720
  const existing = await readDeviceConfig(launchConfig);
2848
3721
  let relayProtocol;
@@ -2935,23 +3808,33 @@ async function runOnboard(launchConfig) {
2935
3808
  launchConfig
2936
3809
  );
2937
3810
  s.stop("Daemon running");
2938
- await writeLog("info", `server ${config.serverUrl}`);
2939
- await writeLog("info", `config ${paths.configPath}`);
2940
- await writeLog("info", `workdir root ${config.workdirRoot}`);
2941
- await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
2942
- await writeLog("info", `log file ${paths.logPath}`);
2943
3811
  note(
2944
3812
  [
2945
3813
  `Server: ${config.serverUrl}`,
2946
3814
  `Bunny daemon: ${bunnyDaemon.baseUrl}`,
2947
3815
  `Relay: connected`,
2948
- `Logs: ${paths.logPath}`
3816
+ `Logs: ${getDatedConnectorLogPath(paths.logPath)}`
2949
3817
  ].join("\n"),
2950
3818
  "Status"
2951
3819
  );
2952
3820
  outro("Buda Connector is running. Press Ctrl+C to stop.");
3821
+ const dashboard = startConnectorConsoleDashboard({ plain: options.plain });
3822
+ updateConnectorDashboardStatus({
3823
+ agent: "Ready",
3824
+ relay: "Connected",
3825
+ server: config.serverUrl
3826
+ });
3827
+ await writeLog("info", `server ${config.serverUrl}`);
3828
+ await writeLog("info", `config ${paths.configPath}`);
3829
+ await writeLog("info", `workdir root ${config.workdirRoot}`);
3830
+ await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
3831
+ await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
2953
3832
  const interval = setInterval(() => {
2954
- heartbeatConnector(config, launchConfig, bunnyDaemon).catch((error) => {
3833
+ heartbeatConnector(config, launchConfig, bunnyDaemon).then(() => {
3834
+ updateConnectorDashboardStatus({
3835
+ heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
3836
+ });
3837
+ }).catch((error) => {
2955
3838
  void writeLog(
2956
3839
  "warn",
2957
3840
  `heartbeat failed: ${error instanceof Error ? error.message : String(error)}`
@@ -2959,6 +3842,7 @@ async function runOnboard(launchConfig) {
2959
3842
  });
2960
3843
  }, HEARTBEAT_INTERVAL_MS);
2961
3844
  const shutdown = async () => {
3845
+ dashboard?.stop();
2962
3846
  clearInterval(interval);
2963
3847
  relay.stop();
2964
3848
  await unregisterConnector(config, launchConfig);
@@ -2971,7 +3855,7 @@ async function runOnboard(launchConfig) {
2971
3855
 
2972
3856
  // src/runtime.ts
2973
3857
  import { mkdir as mkdir9 } from "fs/promises";
2974
- import path9 from "path";
3858
+ import path11 from "path";
2975
3859
  async function runNewConnector(launchConfig) {
2976
3860
  const existing = await readDeviceConfig(launchConfig);
2977
3861
  const config = await ensureDeviceConfig(launchConfig);
@@ -2986,14 +3870,14 @@ async function runNewConnector(launchConfig) {
2986
3870
  await writeDeviceConfig(config);
2987
3871
  await writeLog("info", `wrote ${paths.configPath}`);
2988
3872
  await writeLog("info", `workdir root ${config.workdirRoot}`);
2989
- await writeLog("info", `log file ${paths.logPath}`);
3873
+ await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
2990
3874
  }
2991
3875
  async function runConnectorDaemon(launchConfig) {
2992
3876
  const existing = await readDeviceConfig(launchConfig);
2993
3877
  const config = await ensureDeviceConfig(launchConfig);
2994
3878
  const paths = getConnectorPaths(config.serverUrl);
2995
3879
  const daemonLockTarget = getConnectorDaemonLockTarget(config.serverUrl, config.connectorId);
2996
- await mkdir9(path9.dirname(daemonLockTarget), { recursive: true, mode: 448 });
3880
+ await mkdir9(path11.dirname(daemonLockTarget), { recursive: true, mode: 448 });
2997
3881
  let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
2998
3882
  throw new Error(
2999
3883
  `Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
@@ -3016,7 +3900,7 @@ async function runConnectorDaemon(launchConfig) {
3016
3900
  config.serverUrl,
3017
3901
  registration.connectorId
3018
3902
  );
3019
- await mkdir9(path9.dirname(registeredLockTarget), { recursive: true, mode: 448 });
3903
+ await mkdir9(path11.dirname(registeredLockTarget), { recursive: true, mode: 448 });
3020
3904
  const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
3021
3905
  timeoutMs: 0
3022
3906
  }).catch(() => {
@@ -3040,11 +3924,15 @@ async function runConnectorDaemon(launchConfig) {
3040
3924
  await writeLog("info", `server ${config.serverUrl}`);
3041
3925
  await writeLog("info", `config ${paths.configPath}`);
3042
3926
  await writeLog("info", `workdir root ${config.workdirRoot}`);
3043
- await writeLog("info", `log file ${paths.logPath}`);
3927
+ await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
3044
3928
  await writeLog("info", `bunny daemon ${activeBunnyDaemon.baseUrl}`);
3045
3929
  const interval = setInterval(() => {
3046
3930
  heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
3047
3931
  relay.updateRelayProtocol(nextRelayProtocol);
3932
+ relay.resumeAfterAccessRestore();
3933
+ updateConnectorDashboardStatus({
3934
+ heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
3935
+ });
3048
3936
  }).catch((error) => {
3049
3937
  void writeLog(
3050
3938
  "warn",
@@ -3079,7 +3967,7 @@ async function runConnectorDaemon(launchConfig) {
3079
3967
  // src/cli.ts
3080
3968
  function printHelp() {
3081
3969
  console.log(
3082
- "Help: buda-connector [onboard | new | dev | daemon] [--token <token>] [--server-url <url>] [--workdir <path>] [--launch-config-stdin]\n pnpm --filter @buda-ai/connector dev\n\nCommands (default: onboard in TTY, daemon otherwise):\n onboard Interactive first-time setup + start daemon\n new Create/register a new connector identity\n dev Run the connector daemon in development\n daemon Run the connector daemon for sidecar/service usage"
3970
+ "Help: buda-connector [onboard | new | dev | daemon] [--token <token>] [--server-url <url>] [--workdir <path>] [--plain] [--launch-config-stdin]\n pnpm --filter @buda-ai/connector dev\n\nCommands (default: onboard in TTY, daemon otherwise):\n onboard Interactive first-time setup + start daemon\n new Create/register a new connector identity\n dev Run the connector daemon in development\n daemon Run the connector daemon for sidecar/service usage\n\nOptions:\n --plain Disable the interactive dashboard and print ordinary logs"
3083
3971
  );
3084
3972
  }
3085
3973
  function readFlagValue(name) {
@@ -3146,15 +4034,17 @@ async function main() {
3146
4034
  console.error("[buda-connector] uncaughtException:", error);
3147
4035
  });
3148
4036
  const launchConfig = await readLaunchConfig();
4037
+ const plain = process.argv.includes("--plain");
3149
4038
  if (command === "new") {
3150
4039
  await runNewConnector(launchConfig);
3151
4040
  return;
3152
4041
  }
3153
4042
  if (command === "onboard") {
3154
- await runOnboard(launchConfig);
4043
+ await runOnboard(launchConfig, { plain });
3155
4044
  return;
3156
4045
  }
3157
4046
  if (command === "dev" || command === "daemon") {
4047
+ startConnectorConsoleDashboard({ plain });
3158
4048
  await runConnectorDaemon(launchConfig);
3159
4049
  return;
3160
4050
  }