@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/README.md +3 -1
- package/agent-templates/shared/AGENTS.md +1 -1
- package/dist/chunk-MCKGQKYU.js +15 -0
- package/dist/cli.js +1052 -162
- package/dist/lib-UX6C35VI.js +1307 -0
- package/package.json +4 -1
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.
|
|
425
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1390
|
+
sessions2.set(sessionId, nextSession);
|
|
628
1391
|
browser.on("disconnected", () => {
|
|
629
|
-
if (
|
|
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 =
|
|
1437
|
+
const activeSession = sessions2.get(sessionId) ?? await sessionPromises.get(sessionId)?.catch(() => null);
|
|
675
1438
|
if (!activeSession) return;
|
|
676
|
-
if (
|
|
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([...
|
|
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 =
|
|
1557
|
+
const agentRoot = path4.resolve(workdirRoot, "agent");
|
|
795
1558
|
const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
|
|
796
|
-
const absolutePath =
|
|
797
|
-
if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${
|
|
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:
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 (
|
|
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
|
|
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 =
|
|
1006
|
-
var BUNDLED_DIR =
|
|
1007
|
-
var SYSTEM_SKILLS_DIR =
|
|
1008
|
-
var SCRIPTS_DIR =
|
|
1009
|
-
var SHARED_DIR =
|
|
1010
|
-
var MONOREPO_SRC =
|
|
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(
|
|
1782
|
+
filter: (src) => !src.split(path5.sep).includes("extensions")
|
|
1020
1783
|
});
|
|
1021
1784
|
}
|
|
1022
1785
|
ensureBundledDir();
|
|
1023
1786
|
function ensureBundledDir() {
|
|
1024
|
-
const missingRequiredTemplate = !existsSync(
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
2005
|
+
readdir as readdir3,
|
|
1243
2006
|
readFile as readFile4,
|
|
1244
2007
|
readlink,
|
|
1245
2008
|
realpath,
|
|
1246
|
-
rename as
|
|
2009
|
+
rename as rename3,
|
|
1247
2010
|
rm,
|
|
1248
2011
|
symlink,
|
|
1249
|
-
unlink as
|
|
2012
|
+
unlink as unlink4
|
|
1250
2013
|
} from "fs/promises";
|
|
1251
|
-
import
|
|
2014
|
+
import path6 from "path";
|
|
1252
2015
|
var MIGRATION_VERSION = 1;
|
|
1253
|
-
var MIGRATION_DIRECTORY =
|
|
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 =
|
|
2061
|
+
const migrationDirectory = path6.join(canonicalRoot, MIGRATION_DIRECTORY);
|
|
1299
2062
|
await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
|
|
1300
|
-
const journalPath =
|
|
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,
|
|
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,
|
|
1373
|
-
await
|
|
1374
|
-
await syncDirectory(
|
|
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 =
|
|
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:
|
|
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
|
|
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:
|
|
1434
|
-
{ id: "space", source:
|
|
1435
|
-
{ id: "logs", source:
|
|
1436
|
-
{ id: "connector-log", source:
|
|
1437
|
-
{ id: "runtime-config", source:
|
|
1438
|
-
{ id: "runtime-config-hidden", source:
|
|
1439
|
-
{ id: "runtime-config-file", source:
|
|
1440
|
-
{ id: "runtime-config-hidden-file", source:
|
|
1441
|
-
{ id: "bunny-runtime", source:
|
|
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:
|
|
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
|
|
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
|
|
1464
|
-
const spaceRoot =
|
|
1465
|
-
const agentRoot =
|
|
1466
|
-
if (id === "agent") return
|
|
1467
|
-
if (id === "space") return
|
|
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
|
|
2232
|
+
return path6.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
|
|
1470
2233
|
}
|
|
1471
|
-
return
|
|
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
|
|
2240
|
+
const entries = (await readdir3(source)).sort();
|
|
1478
2241
|
for (const entry of entries) {
|
|
1479
|
-
await copyTree(
|
|
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
|
-
|
|
1515
|
-
|
|
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(
|
|
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(
|
|
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
|
|
2348
|
+
await rename3(temporaryPath, journalPath);
|
|
1586
2349
|
await chmod2(journalPath, 384);
|
|
1587
|
-
await syncDirectory(
|
|
2350
|
+
await syncDirectory(path6.dirname(journalPath));
|
|
1588
2351
|
} finally {
|
|
1589
|
-
await
|
|
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 =
|
|
2380
|
+
const relative = path6.relative(storageRoot, directory);
|
|
1618
2381
|
let current = storageRoot;
|
|
1619
|
-
for (const segment of relative.split(
|
|
1620
|
-
current =
|
|
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 !==
|
|
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 =
|
|
1655
|
-
if (
|
|
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") &&
|
|
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 =
|
|
1759
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${
|
|
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
|
|
2534
|
+
import path8 from "path";
|
|
1772
2535
|
import { createNextHandler } from "@bunny-agent/daemon/nextjs";
|
|
1773
|
-
import
|
|
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
|
|
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
|
|
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) =>
|
|
1964
|
-
|
|
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 =
|
|
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:
|
|
1981
|
-
space:
|
|
1982
|
-
system:
|
|
1983
|
-
temporary:
|
|
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 =
|
|
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,
|
|
1995
|
-
ensureVolumeLink(this.descriptor.sharedVolume,
|
|
1996
|
-
ensureVolumeLink(this.systemVolume,
|
|
1997
|
-
ensureVolumeLink(this.descriptor.temporaryVolume,
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 !==
|
|
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
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
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 !==
|
|
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 =
|
|
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:
|
|
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 !==
|
|
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
|
|
2501
|
-
import
|
|
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(
|
|
2506
|
-
const systemSkillsLink =
|
|
2507
|
-
const systemScriptsLink =
|
|
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
|
|
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
|
|
3408
|
+
await unlink5(linkPath);
|
|
2536
3409
|
}
|
|
2537
3410
|
} catch {
|
|
2538
3411
|
}
|
|
2539
|
-
await mkdir7(
|
|
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
|
|
3425
|
+
rename as rename4,
|
|
2553
3426
|
statfs,
|
|
2554
|
-
unlink as
|
|
3427
|
+
unlink as unlink6
|
|
2555
3428
|
} from "fs/promises";
|
|
2556
|
-
import
|
|
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 =
|
|
3453
|
+
const registryDirectory = path10.join(canonicalRoot, REGISTRY_DIRECTORY);
|
|
2581
3454
|
await ensurePrivateDirectory2(registryDirectory);
|
|
2582
|
-
const registryPath =
|
|
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 =
|
|
2659
|
-
const agentRoot =
|
|
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:
|
|
2665
|
-
sharedVolume:
|
|
2666
|
-
stateVolume:
|
|
2667
|
-
temporaryVolume:
|
|
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
|
|
3661
|
+
await rename4(temporaryPath, registryPath);
|
|
2789
3662
|
await chmod3(registryPath, 384);
|
|
2790
3663
|
} catch (error) {
|
|
2791
|
-
await
|
|
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 =
|
|
2815
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${
|
|
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).
|
|
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
|
|
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(
|
|
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(
|
|
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
|
}
|