@buda-ai/connector 0.1.6 → 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) => {
@@ -2196,7 +2973,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2196
2973
  if (token) headers["X-Buda-Connector-Token"] = token;
2197
2974
  headers["X-Buda-Connector-Id"] = config.connectorId;
2198
2975
  if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
2199
- const ws = new WebSocket(relayUrl, {
2976
+ const ws = new WebSocket2(relayUrl, {
2200
2977
  headers: Object.keys(headers).length > 0 ? headers : void 0,
2201
2978
  handshakeTimeout: RELAY_HANDSHAKE_TIMEOUT_MS
2202
2979
  });
@@ -2215,7 +2992,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2215
2992
  hadOpenConnection = true;
2216
2993
  reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
2217
2994
  pingTimer = setInterval(() => {
2218
- if (ws.readyState !== WebSocket.OPEN) return;
2995
+ if (ws.readyState !== WebSocket2.OPEN) return;
2219
2996
  try {
2220
2997
  ws.ping();
2221
2998
  } catch (error) {
@@ -2245,7 +3022,9 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2245
3022
  config.connectorId,
2246
3023
  activeRelayProtocol,
2247
3024
  replayGuard,
2248
- workspaceRegistry
3025
+ workspaceRegistry,
3026
+ config.serverUrl,
3027
+ bunnyDaemon.baseUrl
2249
3028
  );
2250
3029
  });
2251
3030
  ws.on("close", (code) => {
@@ -2308,8 +3087,8 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2308
3087
  }
2309
3088
  };
2310
3089
  }
2311
- async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry) {
2312
- 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;
2313
3092
  let message;
2314
3093
  try {
2315
3094
  message = JSON.parse(raw);
@@ -2326,6 +3105,52 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2326
3105
  return;
2327
3106
  }
2328
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
+ }
2329
3154
  await writeLog("warn", "relay rejected unsigned browser input event");
2330
3155
  return;
2331
3156
  }
@@ -2351,6 +3176,15 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2351
3176
  }
2352
3177
  const controller = new AbortController();
2353
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;
2354
3188
  try {
2355
3189
  const requestPath = message.request.path;
2356
3190
  const isBrowserRequest = requestPath.startsWith("/browser/") || requestPath === "/browser";
@@ -2411,6 +3245,14 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2411
3245
  signal: controller.signal
2412
3246
  });
2413
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
+ }
2414
3256
  sendRelayMessage(socket, {
2415
3257
  type: "start",
2416
3258
  requestId: message.requestId,
@@ -2453,7 +3295,21 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2453
3295
  type: "end",
2454
3296
  requestId: message.requestId
2455
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
+ }
2456
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
+ }
2457
3313
  sendRelayError(socket, message.requestId, error);
2458
3314
  } finally {
2459
3315
  inflight.delete(message.requestId);
@@ -2478,12 +3334,12 @@ var normalizeWorkspaceRequest = (request, mounts) => {
2478
3334
  }
2479
3335
  const body = request.body;
2480
3336
  const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
2481
- const normalized = path6.posix.normalize(cwd);
3337
+ const normalized = path8.posix.normalize(cwd);
2482
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)] : [];
2483
3339
  if (!mount || relative === void 0) {
2484
3340
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
2485
3341
  }
2486
- return { ...request, body: { ...body, cwd: path6.join(mount, relative) } };
3342
+ return { ...request, body: { ...body, cwd: path8.join(mount, relative) } };
2487
3343
  };
2488
3344
  function sendRelayError(socket, requestId, error) {
2489
3345
  sendRelayMessage(socket, {
@@ -2494,7 +3350,7 @@ function sendRelayError(socket, requestId, error) {
2494
3350
  });
2495
3351
  }
2496
3352
  function sendRelayMessage(socket, message) {
2497
- if (socket.readyState !== WebSocket.OPEN) return;
3353
+ if (socket.readyState !== WebSocket2.OPEN) return;
2498
3354
  socket.send(JSON.stringify(message));
2499
3355
  }
2500
3356
  function headersToRecord(headers) {
@@ -2514,14 +3370,14 @@ function getHeaderValue(headers, name) {
2514
3370
  }
2515
3371
 
2516
3372
  // src/workdir-layout.ts
2517
- import { lstat as lstat3, mkdir as mkdir7, symlink as symlink3, unlink as unlink4 } from "fs/promises";
2518
- 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";
2519
3375
  async function provisionWorkdirLayout(workdirRoot) {
2520
3376
  const manifest = await readBundledManifest();
2521
3377
  await mkdir7(workdirRoot, { recursive: true });
2522
- await mkdir7(path7.join(workdirRoot, "system", ".agents"), { recursive: true });
2523
- const systemSkillsLink = path7.join(workdirRoot, "system", ".agents", "skills");
2524
- 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");
2525
3381
  await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
2526
3382
  await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
2527
3383
  await writeLog("info", "workdir layout provisioned", {
@@ -2541,7 +3397,7 @@ async function ensureSymlink(target, linkPath) {
2541
3397
  try {
2542
3398
  const existing = await lstat3(linkPath);
2543
3399
  if (existing.isSymbolicLink()) {
2544
- await unlink4(linkPath);
3400
+ await unlink5(linkPath);
2545
3401
  } else if (existing.isDirectory()) {
2546
3402
  await writeLog(
2547
3403
  "warn",
@@ -2549,11 +3405,11 @@ async function ensureSymlink(target, linkPath) {
2549
3405
  );
2550
3406
  return;
2551
3407
  } else {
2552
- await unlink4(linkPath);
3408
+ await unlink5(linkPath);
2553
3409
  }
2554
3410
  } catch {
2555
3411
  }
2556
- await mkdir7(path7.dirname(linkPath), { recursive: true });
3412
+ await mkdir7(path9.dirname(linkPath), { recursive: true });
2557
3413
  await symlink3(target, linkPath, "dir");
2558
3414
  }
2559
3415
 
@@ -2566,11 +3422,11 @@ import {
2566
3422
  open as open3,
2567
3423
  readFile as readFile6,
2568
3424
  realpath as realpath3,
2569
- rename as rename3,
3425
+ rename as rename4,
2570
3426
  statfs,
2571
- unlink as unlink5
3427
+ unlink as unlink6
2572
3428
  } from "fs/promises";
2573
- import path8 from "path";
3429
+ import path10 from "path";
2574
3430
  var REGISTRY_VERSION = 1;
2575
3431
  var REGISTRY_DIRECTORY = ".buda";
2576
3432
  var REGISTRY_FILENAME = "workspace-registry.json";
@@ -2594,9 +3450,9 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
2594
3450
  await mkdir8(storageRoot, { recursive: true, mode: 448 });
2595
3451
  await rejectUnsafeFilesystem(storageRoot);
2596
3452
  const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
2597
- const registryDirectory = path8.join(canonicalRoot, REGISTRY_DIRECTORY);
3453
+ const registryDirectory = path10.join(canonicalRoot, REGISTRY_DIRECTORY);
2598
3454
  await ensurePrivateDirectory2(registryDirectory);
2599
- const registryPath = path8.join(registryDirectory, REGISTRY_FILENAME);
3455
+ const registryPath = path10.join(registryDirectory, REGISTRY_FILENAME);
2600
3456
  const document = await withExclusiveFileLock(registryPath, async () => {
2601
3457
  let current;
2602
3458
  try {
@@ -2672,16 +3528,16 @@ var readRegistryDocument = async (registryPath) => {
2672
3528
  }
2673
3529
  };
2674
3530
  var deriveWorkspace = (storageRoot, spaceId, agentId) => {
2675
- const spaceRoot = path8.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
2676
- 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);
2677
3533
  return {
2678
3534
  workspaceKey: `${spaceId}:${agentId}`,
2679
3535
  spaceId,
2680
3536
  agentId,
2681
- privateVolume: path8.join(agentRoot, "workspace"),
2682
- sharedVolume: path8.join(spaceRoot, "shared"),
2683
- stateVolume: path8.join(agentRoot, "state"),
2684
- 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")
2685
3541
  };
2686
3542
  };
2687
3543
  var parseRegistry = (raw) => {
@@ -2802,10 +3658,10 @@ var writeRegistryAtomically = async (registryPath, document) => {
2802
3658
  await handle.close();
2803
3659
  }
2804
3660
  try {
2805
- await rename3(temporaryPath, registryPath);
3661
+ await rename4(temporaryPath, registryPath);
2806
3662
  await chmod3(registryPath, 384);
2807
3663
  } catch (error) {
2808
- await unlink5(temporaryPath).catch(() => void 0);
3664
+ await unlink6(temporaryPath).catch(() => void 0);
2809
3665
  throw error;
2810
3666
  }
2811
3667
  };
@@ -2828,8 +3684,8 @@ var assertContained = (root, candidate) => {
2828
3684
  if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
2829
3685
  };
2830
3686
  var isPathInside2 = (parent, child) => {
2831
- const relative = path8.relative(parent, child);
2832
- 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);
2833
3689
  };
2834
3690
  var toDescriptor = (workspace) => ({
2835
3691
  workspaceKey: workspace.workspaceKey,
@@ -2859,7 +3715,7 @@ var corrupt2 = (message) => new WorkspaceRegistryError("REGISTRY_CORRUPT", messa
2859
3715
  var collision = (message) => new WorkspaceRegistryError("WORKSPACE_COLLISION", message);
2860
3716
 
2861
3717
  // src/onboard.ts
2862
- async function runOnboard(launchConfig) {
3718
+ async function runOnboard(launchConfig, options = {}) {
2863
3719
  intro("Welcome to Buda Connector");
2864
3720
  const existing = await readDeviceConfig(launchConfig);
2865
3721
  let relayProtocol;
@@ -2952,23 +3808,33 @@ async function runOnboard(launchConfig) {
2952
3808
  launchConfig
2953
3809
  );
2954
3810
  s.stop("Daemon running");
2955
- await writeLog("info", `server ${config.serverUrl}`);
2956
- await writeLog("info", `config ${paths.configPath}`);
2957
- await writeLog("info", `workdir root ${config.workdirRoot}`);
2958
- await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
2959
- await writeLog("info", `log file ${paths.logPath}`);
2960
3811
  note(
2961
3812
  [
2962
3813
  `Server: ${config.serverUrl}`,
2963
3814
  `Bunny daemon: ${bunnyDaemon.baseUrl}`,
2964
3815
  `Relay: connected`,
2965
- `Logs: ${paths.logPath}`
3816
+ `Logs: ${getDatedConnectorLogPath(paths.logPath)}`
2966
3817
  ].join("\n"),
2967
3818
  "Status"
2968
3819
  );
2969
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)}`);
2970
3832
  const interval = setInterval(() => {
2971
- 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) => {
2972
3838
  void writeLog(
2973
3839
  "warn",
2974
3840
  `heartbeat failed: ${error instanceof Error ? error.message : String(error)}`
@@ -2976,6 +3842,7 @@ async function runOnboard(launchConfig) {
2976
3842
  });
2977
3843
  }, HEARTBEAT_INTERVAL_MS);
2978
3844
  const shutdown = async () => {
3845
+ dashboard?.stop();
2979
3846
  clearInterval(interval);
2980
3847
  relay.stop();
2981
3848
  await unregisterConnector(config, launchConfig);
@@ -2988,7 +3855,7 @@ async function runOnboard(launchConfig) {
2988
3855
 
2989
3856
  // src/runtime.ts
2990
3857
  import { mkdir as mkdir9 } from "fs/promises";
2991
- import path9 from "path";
3858
+ import path11 from "path";
2992
3859
  async function runNewConnector(launchConfig) {
2993
3860
  const existing = await readDeviceConfig(launchConfig);
2994
3861
  const config = await ensureDeviceConfig(launchConfig);
@@ -3003,14 +3870,14 @@ async function runNewConnector(launchConfig) {
3003
3870
  await writeDeviceConfig(config);
3004
3871
  await writeLog("info", `wrote ${paths.configPath}`);
3005
3872
  await writeLog("info", `workdir root ${config.workdirRoot}`);
3006
- await writeLog("info", `log file ${paths.logPath}`);
3873
+ await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
3007
3874
  }
3008
3875
  async function runConnectorDaemon(launchConfig) {
3009
3876
  const existing = await readDeviceConfig(launchConfig);
3010
3877
  const config = await ensureDeviceConfig(launchConfig);
3011
3878
  const paths = getConnectorPaths(config.serverUrl);
3012
3879
  const daemonLockTarget = getConnectorDaemonLockTarget(config.serverUrl, config.connectorId);
3013
- await mkdir9(path9.dirname(daemonLockTarget), { recursive: true, mode: 448 });
3880
+ await mkdir9(path11.dirname(daemonLockTarget), { recursive: true, mode: 448 });
3014
3881
  let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
3015
3882
  throw new Error(
3016
3883
  `Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
@@ -3033,7 +3900,7 @@ async function runConnectorDaemon(launchConfig) {
3033
3900
  config.serverUrl,
3034
3901
  registration.connectorId
3035
3902
  );
3036
- await mkdir9(path9.dirname(registeredLockTarget), { recursive: true, mode: 448 });
3903
+ await mkdir9(path11.dirname(registeredLockTarget), { recursive: true, mode: 448 });
3037
3904
  const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
3038
3905
  timeoutMs: 0
3039
3906
  }).catch(() => {
@@ -3057,12 +3924,15 @@ async function runConnectorDaemon(launchConfig) {
3057
3924
  await writeLog("info", `server ${config.serverUrl}`);
3058
3925
  await writeLog("info", `config ${paths.configPath}`);
3059
3926
  await writeLog("info", `workdir root ${config.workdirRoot}`);
3060
- await writeLog("info", `log file ${paths.logPath}`);
3927
+ await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
3061
3928
  await writeLog("info", `bunny daemon ${activeBunnyDaemon.baseUrl}`);
3062
3929
  const interval = setInterval(() => {
3063
3930
  heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
3064
3931
  relay.updateRelayProtocol(nextRelayProtocol);
3065
3932
  relay.resumeAfterAccessRestore();
3933
+ updateConnectorDashboardStatus({
3934
+ heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
3935
+ });
3066
3936
  }).catch((error) => {
3067
3937
  void writeLog(
3068
3938
  "warn",
@@ -3097,7 +3967,7 @@ async function runConnectorDaemon(launchConfig) {
3097
3967
  // src/cli.ts
3098
3968
  function printHelp() {
3099
3969
  console.log(
3100
- "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"
3101
3971
  );
3102
3972
  }
3103
3973
  function readFlagValue(name) {
@@ -3164,15 +4034,17 @@ async function main() {
3164
4034
  console.error("[buda-connector] uncaughtException:", error);
3165
4035
  });
3166
4036
  const launchConfig = await readLaunchConfig();
4037
+ const plain = process.argv.includes("--plain");
3167
4038
  if (command === "new") {
3168
4039
  await runNewConnector(launchConfig);
3169
4040
  return;
3170
4041
  }
3171
4042
  if (command === "onboard") {
3172
- await runOnboard(launchConfig);
4043
+ await runOnboard(launchConfig, { plain });
3173
4044
  return;
3174
4045
  }
3175
4046
  if (command === "dev" || command === "daemon") {
4047
+ startConnectorConsoleDashboard({ plain });
3176
4048
  await runConnectorDaemon(launchConfig);
3177
4049
  return;
3178
4050
  }