@buda-ai/connector 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,7 +73,7 @@ To show a file preview card (thumbnail + download button) inline in the current
73
73
 
74
74
  1. Call the `send-file` script with the file path (relative to `/agent`):
75
75
  ```bash
76
- node /system/.agents/skills/buda-machine-info/scripts/send-file.mjs uploads/photo.jpg
76
+ node "${BUDA_SYSTEM_ROOT:-/system}/.agents/skills/buda-machine-info/scripts/send-file.mjs" uploads/photo.jpg
77
77
  ```
78
78
  2. The script returns a `[send:uploads/photo.jpg]` marker if the file exists, or an empty string if it does not.
79
79
  3. Embed the returned marker directly in your reply — do not explain it to the user.
@@ -82,4 +82,3 @@ To show a file preview card (thumbnail + download button) inline in the current
82
82
  - Always call the script first to verify the file exists. Never emit `[send:…]` manually.
83
83
  - Path is relative to `/agent` (no leading slash): `uploads/photo.jpg` → `/agent/uploads/photo.jpg`.
84
84
  - You may also pass the full `/agent/…` path — the script normalises it automatically.
85
-
@@ -2,18 +2,20 @@
2
2
  /**
3
3
  * send-file.mjs
4
4
  *
5
- * Resolves a file path relative to /agent and returns a [send:] marker
5
+ * Resolves a file path relative to the Agent workspace and returns a [send:] marker
6
6
  * if the file exists, or an empty string if it does not.
7
7
  *
8
8
  * Usage:
9
9
  * node scripts/send-file.mjs <path>
10
10
  *
11
11
  * Path rules (mirrors the system prompt convention):
12
- * - Relative path (no leading slash): resolved against /agent
12
+ * - Relative path (no leading slash): resolved against the Agent workspace
13
13
  * e.g. "uploads/photo.jpg" → /agent/uploads/photo.jpg
14
- * - Absolute path starting with /agent: used as-is
14
+ * - Logical absolute path starting with /agent: resolved against the Agent workspace
15
15
  * e.g. "/agent/chart.png" → /agent/chart.png
16
- * - Any other absolute path: used as-is (escape hatch)
16
+ * - Connector physical absolute path: accepted only when it is inside BUDA_AGENT_ROOT
17
+ * - Connector paths outside the Agent workspace: rejected
18
+ * - Without BUDA_AGENT_ROOT, the existing Sandbox /agent behavior is preserved
17
19
  *
18
20
  * Output:
19
21
  * [send:uploads/photo.jpg] — file exists (path is relative to /agent)
@@ -21,42 +23,60 @@
21
23
  */
22
24
 
23
25
  import { existsSync } from "node:fs";
24
- import { isAbsolute, resolve } from "node:path";
26
+ import { isAbsolute, relative, resolve, sep } from "node:path";
27
+ import { fileURLToPath } from "node:url";
25
28
 
26
- const AGENT_ROOT = "/agent";
29
+ const LOGICAL_AGENT_ROOT = "/agent";
27
30
 
28
- function resolveAgentPath(input) {
31
+ function resolveSandboxPath(input) {
32
+ if (input.startsWith(`${LOGICAL_AGENT_ROOT}/`)) {
33
+ return { abs: input, rel: input.slice(`${LOGICAL_AGENT_ROOT}/`.length) };
34
+ }
35
+ if (isAbsolute(input)) {
36
+ return { abs: input, rel: input };
37
+ }
38
+ return { abs: resolve(LOGICAL_AGENT_ROOT, input), rel: input };
39
+ }
40
+
41
+ function resolveConnectorPath(input, agentRoot) {
29
42
  if (!input) return null;
43
+ const physicalRoot = resolve(agentRoot);
44
+ let absolutePath;
30
45
 
31
- // Strip a leading /agent prefix so the returned marker stays relative
32
- if (input.startsWith("/agent/")) {
33
- return { abs: input, rel: input.slice("/agent/".length) };
46
+ if (input === LOGICAL_AGENT_ROOT || input.startsWith(`${LOGICAL_AGENT_ROOT}/`)) {
47
+ const logicalRelative = input.slice(LOGICAL_AGENT_ROOT.length).replace(/^\/+/, "");
48
+ absolutePath = resolve(physicalRoot, logicalRelative);
49
+ } else {
50
+ absolutePath = isAbsolute(input) ? resolve(input) : resolve(physicalRoot, input);
34
51
  }
35
52
 
36
- // Other absolute paths — use as-is, keep the full path in the marker
37
- if (isAbsolute(input)) {
38
- return { abs: input, rel: input };
53
+ const relativePath = relative(physicalRoot, absolutePath);
54
+ if (
55
+ !relativePath ||
56
+ relativePath === ".." ||
57
+ relativePath.startsWith(`..${sep}`) ||
58
+ isAbsolute(relativePath)
59
+ ) {
60
+ return null;
39
61
  }
40
62
 
41
- // Relative path — resolve against /agent
42
- const abs = resolve(AGENT_ROOT, input);
43
- return { abs, rel: input };
63
+ return { abs: absolutePath, rel: relativePath.split(sep).join("/") };
44
64
  }
45
65
 
46
- const input = process.argv[2];
47
-
48
- if (!input) {
49
- // No argument — print nothing (empty result)
50
- process.exit(0);
66
+ export function resolveAgentPath(input, agentRoot = process.env.BUDA_AGENT_ROOT) {
67
+ if (!input) return null;
68
+ return agentRoot ? resolveConnectorPath(input, agentRoot) : resolveSandboxPath(input);
51
69
  }
52
70
 
53
- const resolved = resolveAgentPath(input);
71
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
54
72
 
55
- if (!resolved || !existsSync(resolved.abs)) {
56
- // File does not exist — return empty string
57
- process.stdout.write("");
58
- process.exit(0);
59
- }
73
+ if (isMain) {
74
+ const input = process.argv[2];
75
+ const resolved = resolveAgentPath(input);
60
76
 
61
- // File exists — emit the [send:] marker
62
- process.stdout.write(`[send:${resolved.rel}]`);
77
+ if (!resolved || !existsSync(resolved.abs)) {
78
+ process.stdout.write("");
79
+ } else {
80
+ process.stdout.write(`[send:${resolved.rel}]`);
81
+ }
82
+ }
package/dist/cli.js CHANGED
@@ -517,7 +517,7 @@ import path2 from "path";
517
517
  // package.json
518
518
  var package_default = {
519
519
  name: "@buda-ai/connector",
520
- version: "0.1.9",
520
+ version: "0.1.11",
521
521
  private: false,
522
522
  type: "module",
523
523
  publishConfig: {
@@ -584,6 +584,7 @@ var writeMetadata = async (metadataPath, metadata) => {
584
584
  };
585
585
  var publicStatus = (metadata) => ({
586
586
  connectorId: metadata.connectorId,
587
+ ...typeof metadata.heartbeatHealthy === "boolean" ? { heartbeatHealthy: metadata.heartbeatHealthy } : {},
587
588
  idle: metadata.idle,
588
589
  instanceId: metadata.instanceId,
589
590
  ...metadata.lastHeartbeatAt ? { lastHeartbeatAt: metadata.lastHeartbeatAt } : {},
@@ -640,6 +641,7 @@ async function startDaemonControlServer(options) {
640
641
  return;
641
642
  }
642
643
  metadata.idle = options.isIdle?.() ?? metadata.idle;
644
+ metadata.heartbeatHealthy = options.isHeartbeatHealthy?.() ?? metadata.heartbeatHealthy;
643
645
  metadata.lastHeartbeatAt = options.getLastHeartbeatAt?.() ?? metadata.lastHeartbeatAt;
644
646
  metadata.relayConnected = options.isRelayConnected?.() ?? metadata.relayConnected;
645
647
  if (request.command === "persist") {
@@ -682,6 +684,7 @@ async function startDaemonControlServer(options) {
682
684
  throw new Error("Failed to bind daemon control port");
683
685
  metadata = {
684
686
  connectorId: options.config.connectorId,
687
+ ...options.isHeartbeatHealthy ? { heartbeatHealthy: options.isHeartbeatHealthy() } : {},
685
688
  idle: options.isIdle?.() ?? true,
686
689
  instanceId: randomUUID3(),
687
690
  mode: options.mode,
@@ -2329,7 +2332,7 @@ async function warnIfStaleBundle(result) {
2329
2332
  if (!result.bundle?.staleClient) return;
2330
2333
  await writeLog(
2331
2334
  "warn",
2332
- `connector bundle out of date \u2014 server expects ${result.bundle.expectedVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
2335
+ `connector runtime out of date \u2014 latest published version is ${result.bundle.latestVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
2333
2336
  );
2334
2337
  }
2335
2338
  async function registerConnector(config, launchConfig, bunnyDaemon, options) {
@@ -2678,11 +2681,11 @@ var streamDaemonLogs = (options) => {
2678
2681
  var toDashboardConnector = (config, status) => ({
2679
2682
  agent: status ? status.idle ? "Ready" : "Busy" : "Stopped",
2680
2683
  connectorId: config.connectorId,
2681
- heartbeat: status?.lastHeartbeatAt ? `OK ${new Date(status.lastHeartbeatAt).toLocaleTimeString("en-GB", { hour12: false })}` : status ? "Waiting" : "Stopped",
2684
+ heartbeat: status?.heartbeatHealthy === false ? "Failed" : status?.lastHeartbeatAt ? `OK ${new Date(status.lastHeartbeatAt).toLocaleTimeString("en-GB", { hour12: false })}` : status ? "Waiting" : "Stopped",
2682
2685
  relay: status?.relayConnected ? "Connected" : "Disconnected",
2683
2686
  serverUrl: config.serverUrl,
2684
2687
  spaceId: config.spaceId ?? "Unknown",
2685
- status: status ? status.relayConnected ? "Online" : "Running" : "Stopped"
2688
+ status: status ? status.relayConnected && status.heartbeatHealthy !== false ? "Online" : "Running" : "Stopped"
2686
2689
  });
2687
2690
  async function readLocalConnectorDashboardRows(configRoot = getConfigRoot()) {
2688
2691
  const configs = await listDeviceConfigs(configRoot);
@@ -3857,7 +3860,8 @@ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT"
3857
3860
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
3858
3861
 
3859
3862
  // src/relay.ts
3860
- import path11 from "path";
3863
+ import { realpathSync } from "fs";
3864
+ import path12 from "path";
3861
3865
  import { createNextHandler } from "@bunny-agent/daemon/nextjs";
3862
3866
  import WebSocket2 from "ws";
3863
3867
 
@@ -4024,6 +4028,219 @@ function isNonEmptyString(value) {
4024
4028
  return typeof value === "string" && value.trim().length > 0;
4025
4029
  }
4026
4030
 
4031
+ // src/html-preview.ts
4032
+ import { readFile as readFile8, realpath as realpath2, stat as stat4 } from "fs/promises";
4033
+ import { createServer as createServer2 } from "http";
4034
+ import path10 from "path";
4035
+ var PREVIEW_HOST = "127.0.0.1";
4036
+ var MIN_PREVIEW_PORT = 3100;
4037
+ var MAX_PREVIEW_PORT = 3900;
4038
+ var MIME_TYPES = {
4039
+ ".avif": "image/avif",
4040
+ ".css": "text/css; charset=utf-8",
4041
+ ".gif": "image/gif",
4042
+ ".htm": "text/html; charset=utf-8",
4043
+ ".html": "text/html; charset=utf-8",
4044
+ ".ico": "image/x-icon",
4045
+ ".jpeg": "image/jpeg",
4046
+ ".jpg": "image/jpeg",
4047
+ ".js": "text/javascript; charset=utf-8",
4048
+ ".json": "application/json; charset=utf-8",
4049
+ ".mjs": "text/javascript; charset=utf-8",
4050
+ ".png": "image/png",
4051
+ ".svg": "image/svg+xml",
4052
+ ".txt": "text/plain; charset=utf-8",
4053
+ ".webp": "image/webp",
4054
+ ".woff": "font/woff",
4055
+ ".woff2": "font/woff2"
4056
+ };
4057
+ var HtmlPreviewServerRegistry = class {
4058
+ servers = /* @__PURE__ */ new Map();
4059
+ async open(input) {
4060
+ const htmlFile = await resolveWorkspaceFile(input);
4061
+ const folder = path10.dirname(htmlFile);
4062
+ let pending = this.servers.get(folder);
4063
+ if (!pending) {
4064
+ pending = startStaticServer(folder);
4065
+ this.servers.set(folder, pending);
4066
+ void pending.catch(() => {
4067
+ if (this.servers.get(folder) === pending) this.servers.delete(folder);
4068
+ });
4069
+ }
4070
+ const { port } = await pending;
4071
+ const previewUrl = new URL(`http://${PREVIEW_HOST}:${port}`);
4072
+ previewUrl.pathname = `/${path10.basename(htmlFile)}`;
4073
+ return {
4074
+ url: previewUrl.toString(),
4075
+ port,
4076
+ htmlPath: previewUrl.pathname
4077
+ };
4078
+ }
4079
+ async closeAll() {
4080
+ const pending = [...this.servers.values()];
4081
+ this.servers.clear();
4082
+ const entries = await Promise.all(pending.map((entry) => entry.catch(() => void 0)));
4083
+ await Promise.all(
4084
+ entries.map(
4085
+ (entry) => new Promise((resolve) => {
4086
+ if (!entry?.server.listening) return resolve();
4087
+ entry.server.close(() => resolve());
4088
+ })
4089
+ )
4090
+ );
4091
+ }
4092
+ };
4093
+ var handleHtmlPreviewRequest = async (request, mounts, registry) => {
4094
+ if (request.method !== "POST") {
4095
+ return Response.json({ error: "HTML preview requires POST." }, { status: 405 });
4096
+ }
4097
+ if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
4098
+ return Response.json({ error: "Invalid HTML preview request." }, { status: 400 });
4099
+ }
4100
+ const body = request.body;
4101
+ if (typeof body.filePath !== "string" || body.filePath.length === 0) {
4102
+ return Response.json({ error: "HTML preview requires a file path." }, { status: 400 });
4103
+ }
4104
+ if (body.rootVolume !== "agent" && body.rootVolume !== "space") {
4105
+ return Response.json({ error: "HTML preview requires a valid root volume." }, { status: 400 });
4106
+ }
4107
+ try {
4108
+ return Response.json(
4109
+ await registry.open({ filePath: body.filePath, rootVolume: body.rootVolume, mounts })
4110
+ );
4111
+ } catch (error) {
4112
+ return Response.json(
4113
+ { error: error instanceof Error ? error.message : "Unable to open HTML preview." },
4114
+ { status: 400 }
4115
+ );
4116
+ }
4117
+ };
4118
+ var resolveWorkspaceFile = async (input) => {
4119
+ const virtualPath = input.filePath.replaceAll("\\", "/");
4120
+ if (virtualPath.includes("\0")) throw new Error("HTML preview path contains invalid data.");
4121
+ const mountPath = path10.resolve(input.mounts[input.rootVolume]);
4122
+ const root = await realpath2(mountPath);
4123
+ let relativePath = virtualPath;
4124
+ let explicitVolume;
4125
+ for (const volume of ["agent", "space"]) {
4126
+ const prefix = `/${volume}`;
4127
+ if (virtualPath === prefix || virtualPath.startsWith(`${prefix}/`)) {
4128
+ explicitVolume = volume;
4129
+ relativePath = virtualPath.slice(prefix.length).replace(/^\/+/, "");
4130
+ break;
4131
+ }
4132
+ }
4133
+ if (explicitVolume && explicitVolume !== input.rootVolume) {
4134
+ throw new Error("HTML preview path does not match the requested root volume.");
4135
+ }
4136
+ if (!explicitVolume && (path10.isAbsolute(virtualPath) || /^[A-Za-z]:\//.test(virtualPath))) {
4137
+ const physicalPath = path10.normalize(virtualPath);
4138
+ const physicalRoot = [mountPath, root].find(
4139
+ (candidateRoot) => isContained(candidateRoot, physicalPath)
4140
+ );
4141
+ if (!physicalRoot) {
4142
+ throw new Error("HTML preview path must stay inside the requested workspace volume.");
4143
+ }
4144
+ relativePath = path10.relative(physicalRoot, physicalPath).split(path10.sep).join("/");
4145
+ }
4146
+ const segments = relativePath.split("/").filter(Boolean);
4147
+ if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
4148
+ throw new Error("HTML preview path must not contain traversal segments.");
4149
+ }
4150
+ const candidate = path10.resolve(root, ...segments);
4151
+ assertContained(root, candidate);
4152
+ const canonicalFile = await realpath2(candidate);
4153
+ assertContained(root, canonicalFile);
4154
+ const fileStat = await stat4(canonicalFile);
4155
+ if (!fileStat.isFile()) throw new Error("HTML preview path must identify a file.");
4156
+ if (![".htm", ".html"].includes(path10.extname(canonicalFile).toLowerCase())) {
4157
+ throw new Error("HTML preview path must identify an HTML file.");
4158
+ }
4159
+ return canonicalFile;
4160
+ };
4161
+ var startStaticServer = async (folder) => {
4162
+ for (let port = MIN_PREVIEW_PORT; port <= MAX_PREVIEW_PORT; port += 1) {
4163
+ const server = createServer2((request, response) => {
4164
+ void serveStaticFile(folder, request.method ?? "GET", request.url ?? "/").then((result) => {
4165
+ response.writeHead(result.status, result.headers);
4166
+ response.end(request.method === "HEAD" ? void 0 : result.body);
4167
+ }).catch(() => {
4168
+ response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
4169
+ response.end("HTML preview failed.");
4170
+ });
4171
+ });
4172
+ const listening = await listen(server, port);
4173
+ if (listening) {
4174
+ server.unref();
4175
+ return { server, port };
4176
+ }
4177
+ }
4178
+ throw new Error(
4179
+ `No free HTML preview port is available (${MIN_PREVIEW_PORT}-${MAX_PREVIEW_PORT}).`
4180
+ );
4181
+ };
4182
+ var listen = (server, port) => new Promise((resolve, reject) => {
4183
+ const onError = (error) => {
4184
+ server.removeListener("listening", onListening);
4185
+ if (error.code === "EADDRINUSE") resolve(false);
4186
+ else reject(error);
4187
+ };
4188
+ const onListening = () => {
4189
+ server.removeListener("error", onError);
4190
+ resolve(true);
4191
+ };
4192
+ server.once("error", onError);
4193
+ server.once("listening", onListening);
4194
+ server.listen(port, PREVIEW_HOST);
4195
+ });
4196
+ var serveStaticFile = async (folder, method, requestUrl) => {
4197
+ if (method !== "GET" && method !== "HEAD") {
4198
+ return { status: 405, headers: { Allow: "GET, HEAD" } };
4199
+ }
4200
+ let decodedPath;
4201
+ try {
4202
+ decodedPath = decodeURIComponent(requestUrl.split("?", 1)[0] ?? "/").replaceAll("\\", "/");
4203
+ } catch {
4204
+ return { status: 400, headers: {} };
4205
+ }
4206
+ if (decodedPath.includes("\0")) return { status: 400, headers: {} };
4207
+ const segments = decodedPath.split("/").filter(Boolean);
4208
+ if (segments.some((segment) => segment === "." || segment === "..")) {
4209
+ return { status: 403, headers: {} };
4210
+ }
4211
+ try {
4212
+ const candidate = path10.resolve(folder, ...segments);
4213
+ assertContained(folder, candidate);
4214
+ let canonicalFile = await realpath2(candidate);
4215
+ assertContained(folder, canonicalFile);
4216
+ if ((await stat4(canonicalFile)).isDirectory()) {
4217
+ canonicalFile = await realpath2(path10.join(canonicalFile, "index.html"));
4218
+ assertContained(folder, canonicalFile);
4219
+ }
4220
+ if (!(await stat4(canonicalFile)).isFile()) throw new Error("Not a file");
4221
+ const body = method === "HEAD" ? void 0 : await readFile8(canonicalFile);
4222
+ return {
4223
+ status: 200,
4224
+ headers: {
4225
+ "Cache-Control": "no-store",
4226
+ "Content-Type": MIME_TYPES[path10.extname(canonicalFile).toLowerCase()] ?? "application/octet-stream",
4227
+ "X-Content-Type-Options": "nosniff"
4228
+ },
4229
+ body
4230
+ };
4231
+ } catch {
4232
+ return { status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" } };
4233
+ }
4234
+ };
4235
+ var assertContained = (root, candidate) => {
4236
+ if (isContained(root, candidate)) return;
4237
+ throw new Error("HTML preview path escapes its allowed root.");
4238
+ };
4239
+ var isContained = (root, candidate) => {
4240
+ const relative = path10.relative(root, candidate);
4241
+ return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
4242
+ };
4243
+
4027
4244
  // src/workspace-volume-lifecycle.ts
4028
4245
  import { constants as constants3 } from "fs";
4029
4246
  import {
@@ -4031,13 +4248,13 @@ import {
4031
4248
  lstat as lstat2,
4032
4249
  mkdir as mkdir9,
4033
4250
  readdir as readdir5,
4034
- readFile as readFile8,
4035
- realpath as realpath2,
4251
+ readFile as readFile9,
4252
+ realpath as realpath3,
4036
4253
  rm as rm3,
4037
4254
  symlink as symlink2,
4038
4255
  writeFile as writeFile4
4039
4256
  } from "fs/promises";
4040
- import path10 from "path";
4257
+ import path11 from "path";
4041
4258
  var MOUNT_DIRECTORY = "mount";
4042
4259
  var ARCHIVE_MARKER = "archived.json";
4043
4260
  var AGENT_CONTEXT_TEMPLATE_FILES = [
@@ -4049,8 +4266,8 @@ var AGENT_CONTEXT_TEMPLATE_FILES = [
4049
4266
  "BOOTSTRAP.md",
4050
4267
  "MEMORY.md"
4051
4268
  ];
4052
- var deriveSystemVolume = (descriptor) => path10.join(
4053
- path10.dirname(path10.dirname(path10.dirname(path10.dirname(descriptor.sharedVolume)))),
4269
+ var deriveSystemVolume = (descriptor) => path11.join(
4270
+ path11.dirname(path11.dirname(path11.dirname(path11.dirname(descriptor.sharedVolume)))),
4054
4271
  "system"
4055
4272
  );
4056
4273
  var WorkspaceVolumeLifecycle = class {
@@ -4062,14 +4279,14 @@ var WorkspaceVolumeLifecycle = class {
4062
4279
  async mount() {
4063
4280
  await this.ensureVolumes();
4064
4281
  await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
4065
- const mountRoot = path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
4282
+ const mountRoot = path11.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
4066
4283
  await mkdir9(mountRoot, { recursive: true, mode: 448 });
4067
4284
  const mounts = {
4068
4285
  mountRoot,
4069
- agent: path10.join(mountRoot, "agent"),
4070
- space: path10.join(mountRoot, "space"),
4071
- system: path10.join(mountRoot, "system"),
4072
- temporary: path10.join(mountRoot, "tmp")
4286
+ agent: path11.join(mountRoot, "agent"),
4287
+ space: path11.join(mountRoot, "space"),
4288
+ system: path11.join(mountRoot, "system"),
4289
+ temporary: path11.join(mountRoot, "tmp")
4073
4290
  };
4074
4291
  await Promise.all([
4075
4292
  ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
@@ -4077,18 +4294,18 @@ var WorkspaceVolumeLifecycle = class {
4077
4294
  ensureVolumeLink(this.systemVolume, mounts.system),
4078
4295
  ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
4079
4296
  ]);
4080
- const daemonVolumes = path10.join(mountRoot, "volumes");
4297
+ const daemonVolumes = path11.join(mountRoot, "volumes");
4081
4298
  await mkdir9(daemonVolumes, { recursive: true, mode: 448 });
4082
4299
  await Promise.all([
4083
- ensureVolumeLink(this.descriptor.privateVolume, path10.join(daemonVolumes, "agent")),
4084
- ensureVolumeLink(this.descriptor.sharedVolume, path10.join(daemonVolumes, "space")),
4085
- ensureVolumeLink(this.systemVolume, path10.join(daemonVolumes, "system")),
4086
- ensureVolumeLink(this.descriptor.temporaryVolume, path10.join(daemonVolumes, "tmp"))
4300
+ ensureVolumeLink(this.descriptor.privateVolume, path11.join(daemonVolumes, "agent")),
4301
+ ensureVolumeLink(this.descriptor.sharedVolume, path11.join(daemonVolumes, "space")),
4302
+ ensureVolumeLink(this.systemVolume, path11.join(daemonVolumes, "system")),
4303
+ ensureVolumeLink(this.descriptor.temporaryVolume, path11.join(daemonVolumes, "tmp"))
4087
4304
  ]);
4088
4305
  return mounts;
4089
4306
  }
4090
4307
  async unmount() {
4091
- await rm3(path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
4308
+ await rm3(path11.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
4092
4309
  recursive: true,
4093
4310
  force: true
4094
4311
  });
@@ -4097,33 +4314,33 @@ var WorkspaceVolumeLifecycle = class {
4097
4314
  await this.unmount();
4098
4315
  await mkdir9(this.descriptor.stateVolume, { recursive: true, mode: 448 });
4099
4316
  await writeFile4(
4100
- path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
4317
+ path11.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
4101
4318
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
4102
4319
  `,
4103
4320
  { encoding: "utf8", mode: 384 }
4104
4321
  );
4105
4322
  }
4106
4323
  async restoreAgent() {
4107
- await rm3(path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
4324
+ await rm3(path11.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
4108
4325
  return this.mount();
4109
4326
  }
4110
4327
  async deleteAgent(options) {
4111
4328
  requireConfirmation(options);
4112
4329
  await this.unmount();
4113
- const agentRoot = path10.dirname(this.descriptor.privateVolume);
4330
+ const agentRoot = path11.dirname(this.descriptor.privateVolume);
4114
4331
  await rm3(agentRoot, { recursive: true, force: true });
4115
4332
  }
4116
4333
  async archiveSpace() {
4117
4334
  await mkdir9(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
4118
4335
  await writeFile4(
4119
- path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
4336
+ path11.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
4120
4337
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
4121
4338
  `,
4122
4339
  { encoding: "utf8", mode: 384 }
4123
4340
  );
4124
4341
  }
4125
4342
  async restoreSpace() {
4126
- await rm3(path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
4343
+ await rm3(path11.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
4127
4344
  }
4128
4345
  async deleteSpace(options) {
4129
4346
  requireConfirmation(options);
@@ -4139,7 +4356,7 @@ var WorkspaceVolumeLifecycle = class {
4139
4356
  );
4140
4357
  for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
4141
4358
  if (!entries.has(filename)) continue;
4142
- const destination = path10.join(this.descriptor.privateVolume, filename);
4359
+ const destination = path11.join(this.descriptor.privateVolume, filename);
4143
4360
  try {
4144
4361
  await lstat2(destination);
4145
4362
  skipped.push(filename);
@@ -4147,7 +4364,7 @@ var WorkspaceVolumeLifecycle = class {
4147
4364
  if (!isMissingFile(error)) throw error;
4148
4365
  try {
4149
4366
  await copyFile2(
4150
- path10.join(templateDirectory, filename),
4367
+ path11.join(templateDirectory, filename),
4151
4368
  destination,
4152
4369
  constants3.COPYFILE_EXCL
4153
4370
  );
@@ -4170,7 +4387,7 @@ var WorkspaceVolumeLifecycle = class {
4170
4387
  ]) {
4171
4388
  await mkdir9(volume, { recursive: true, mode: 448 });
4172
4389
  const info = await lstat2(volume);
4173
- if (info.isSymbolicLink() || !info.isDirectory() || await realpath2(volume) !== volume) {
4390
+ if (info.isSymbolicLink() || !info.isDirectory() || await realpath3(volume) !== volume) {
4174
4391
  throw new Error("Workspace volume must be a canonical real directory");
4175
4392
  }
4176
4393
  }
@@ -4183,7 +4400,7 @@ var isMissingFile = (error) => error instanceof Error && "code" in error && erro
4183
4400
  var ensureVolumeLink = async (target, linkPath) => {
4184
4401
  try {
4185
4402
  const info = await lstat2(linkPath);
4186
- if (!info.isSymbolicLink() || await realpath2(linkPath) !== target) {
4403
+ if (!info.isSymbolicLink() || await realpath3(linkPath) !== target) {
4187
4404
  throw new Error("Workspace mount link does not match its canonical volume");
4188
4405
  }
4189
4406
  } catch (error) {
@@ -4193,7 +4410,7 @@ var ensureVolumeLink = async (target, linkPath) => {
4193
4410
  } catch (createError) {
4194
4411
  if (!isAlreadyExists3(createError)) throw createError;
4195
4412
  const info = await lstat2(linkPath);
4196
- if (!info.isSymbolicLink() || await realpath2(linkPath) !== target) {
4413
+ if (!info.isSymbolicLink() || await realpath3(linkPath) !== target) {
4197
4414
  throw new Error("Workspace mount link raced with an invalid target");
4198
4415
  }
4199
4416
  }
@@ -4255,6 +4472,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4255
4472
  let currentInflight = /* @__PURE__ */ new Map();
4256
4473
  let activeTerminalTunnels = 0;
4257
4474
  const replayGuard = new ConnectorExecutionReplayGuard();
4475
+ const htmlPreviews = new HtmlPreviewServerRegistry();
4258
4476
  const clearTimers = () => {
4259
4477
  if (pingTimer) {
4260
4478
  clearInterval(pingTimer);
@@ -4361,13 +4579,15 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4361
4579
  onTerminalClose: () => {
4362
4580
  activeTerminalTunnels = Math.max(0, activeTerminalTunnels - 1);
4363
4581
  }
4364
- }
4582
+ },
4583
+ htmlPreviews
4365
4584
  );
4366
4585
  });
4367
4586
  ws.on("close", (code) => {
4368
4587
  detach();
4369
4588
  if (!shouldReconnectConnectorRelay(code)) {
4370
4589
  clearTimers();
4590
+ void htmlPreviews.closeAll();
4371
4591
  if (code === CONNECTOR_SOCKET_REPLACED_CLOSE_CODE) {
4372
4592
  stopped = true;
4373
4593
  void writeLog("warn", "relay replaced by another process for this connector");
@@ -4427,10 +4647,11 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4427
4647
  }
4428
4648
  clearTimers();
4429
4649
  socket?.close();
4650
+ void htmlPreviews.closeAll();
4430
4651
  }
4431
4652
  };
4432
4653
  }
4433
- async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity) {
4654
+ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity, htmlPreviews) {
4434
4655
  if (!socket || socket.readyState !== WebSocket2.OPEN) return;
4435
4656
  let message;
4436
4657
  try {
@@ -4534,6 +4755,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4534
4755
  const requestPath = message.request.path;
4535
4756
  const isBrowserRequest = requestPath.startsWith("/browser/") || requestPath === "/browser";
4536
4757
  const isWorkspaceDescriptorRequest = requestPath === "/workspace/descriptor";
4758
+ const isHtmlPreviewRequest = new URL(requestPath, "http://connector.workspace").pathname === "/api/preview/html";
4537
4759
  const isAgentRequest = isBrowserRequest || requestPath.startsWith("/api/") || isWorkspaceDescriptorRequest;
4538
4760
  if (isAgentRequest && (!executionIdentity || !workspaceRegistry)) {
4539
4761
  throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
@@ -4566,6 +4788,8 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4566
4788
  workdirRoot: mounts?.mountRoot
4567
4789
  }
4568
4790
  );
4791
+ } else if (isHtmlPreviewRequest && mounts && htmlPreviews) {
4792
+ response = await handleHtmlPreviewRequest(message.request, mounts, htmlPreviews);
4569
4793
  } else if (descriptor && mounts) {
4570
4794
  response = await handleWorkspaceDaemonRequest(message.request, mounts, controller.signal);
4571
4795
  } else {
@@ -4606,7 +4830,8 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4606
4830
  });
4607
4831
  if (response.body) {
4608
4832
  const reader = response.body.getReader();
4609
- const decoder = new TextDecoder();
4833
+ const binaryResponse = getHeaderValue(message.request.headers, "x-buda-relay-response-encoding") === "base64";
4834
+ const decoder = binaryResponse ? null : new TextDecoder();
4610
4835
  const cancelReader = () => {
4611
4836
  void reader.cancel().catch(() => {
4612
4837
  });
@@ -4620,10 +4845,11 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4620
4845
  sendRelayMessage(socket, {
4621
4846
  type: "chunk",
4622
4847
  requestId: message.requestId,
4623
- data: decoder.decode(value, { stream: true })
4848
+ data: binaryResponse ? Buffer.from(value).toString("base64") : decoder?.decode(value, { stream: true }) ?? "",
4849
+ ...binaryResponse ? { encoding: "base64" } : {}
4624
4850
  });
4625
4851
  }
4626
- const tail = decoder.decode();
4852
+ const tail = decoder?.decode() ?? "";
4627
4853
  if (tail) {
4628
4854
  sendRelayMessage(socket, {
4629
4855
  type: "chunk",
@@ -4663,6 +4889,29 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4663
4889
  var handleWorkspaceDaemonRequest = async (request, mounts, signal) => {
4664
4890
  const normalized = normalizeWorkspaceRequest(request, mounts);
4665
4891
  const handler = createNextHandler({ root: mounts.mountRoot });
4892
+ if (getHeaderValue(normalized.headers, "x-buda-relay-body-encoding") === "base64") {
4893
+ if (new URL(normalized.path, "http://connector.workspace").pathname !== "/api/fs/write-stream") {
4894
+ throw new Error("Base64 relay bodies are only supported for filesystem write streams.");
4895
+ }
4896
+ if (typeof normalized.body !== "string") {
4897
+ throw new Error("Invalid base64 filesystem write body.");
4898
+ }
4899
+ const content = Buffer.from(normalized.body, "base64");
4900
+ if (content.toString("base64") !== normalized.body) {
4901
+ throw new Error("Invalid base64 filesystem write content.");
4902
+ }
4903
+ const headers = new Headers(normalized.headers);
4904
+ headers.delete("x-buda-relay-body-encoding");
4905
+ headers.set("Content-Type", "application/octet-stream");
4906
+ return handler(
4907
+ new Request(new URL(normalized.path, "http://connector.workspace"), {
4908
+ method: normalized.method,
4909
+ headers,
4910
+ body: content,
4911
+ signal
4912
+ })
4913
+ );
4914
+ }
4666
4915
  return handler(
4667
4916
  new Request(new URL(normalized.path, "http://connector.workspace"), {
4668
4917
  method: normalized.method,
@@ -4673,18 +4922,60 @@ var handleWorkspaceDaemonRequest = async (request, mounts, signal) => {
4673
4922
  );
4674
4923
  };
4675
4924
  var normalizeWorkspaceRequest = (request, mounts) => {
4676
- if (request.path !== "/api/coding/run") return request;
4925
+ if (request.path !== "/api/coding/run") {
4926
+ return normalizeConnectorFilesystemRequest(request, mounts);
4927
+ }
4677
4928
  if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
4678
4929
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
4679
4930
  }
4680
4931
  const body = request.body;
4681
4932
  const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
4682
- const normalized = path11.posix.normalize(cwd);
4933
+ const normalized = path12.posix.normalize(cwd);
4683
4934
  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)] : [];
4684
4935
  if (!mount || relative === void 0) {
4685
4936
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
4686
4937
  }
4687
- return { ...request, body: { ...body, cwd: path11.join(mount, relative) } };
4938
+ const env = body.env && typeof body.env === "object" && !Array.isArray(body.env) ? body.env : {};
4939
+ return {
4940
+ ...request,
4941
+ body: {
4942
+ ...body,
4943
+ cwd: path12.join(mount, relative),
4944
+ env: {
4945
+ ...env,
4946
+ BUDA_AGENT_ROOT: mounts.agent,
4947
+ BUDA_SPACE_ROOT: mounts.space,
4948
+ BUDA_SYSTEM_ROOT: mounts.system
4949
+ }
4950
+ }
4951
+ };
4952
+ };
4953
+ var normalizeConnectorFilesystemRequest = (request, mounts) => {
4954
+ const url = new URL(request.path, "http://connector.workspace");
4955
+ if (!url.pathname.startsWith("/api/fs/")) return request;
4956
+ const filePath = url.searchParams.get("path");
4957
+ if (!filePath) return request;
4958
+ const mount = url.searchParams.get("volume") === "space" ? mounts.space : mounts.agent;
4959
+ const roots = [mount];
4960
+ try {
4961
+ const canonicalRoot = realpathSync(mount);
4962
+ if (canonicalRoot !== mount) roots.push(canonicalRoot);
4963
+ } catch {
4964
+ }
4965
+ const normalizedFilePath = path12.normalize(filePath);
4966
+ for (const root of roots) {
4967
+ const normalizedRoot = path12.normalize(root);
4968
+ const candidates = [normalizedRoot, normalizedRoot.replace(/^[/\\]+/, "")];
4969
+ for (const candidate of candidates) {
4970
+ if (normalizedFilePath !== candidate && !normalizedFilePath.startsWith(`${candidate}${path12.sep}`)) {
4971
+ continue;
4972
+ }
4973
+ const relativePath = path12.relative(candidate, normalizedFilePath).split(path12.sep).join("/");
4974
+ url.searchParams.set("path", relativePath);
4975
+ return { ...request, path: `${url.pathname}${url.search}` };
4976
+ }
4977
+ }
4978
+ return request;
4688
4979
  };
4689
4980
  function sendRelayError(socket, requestId, error) {
4690
4981
  sendRelayMessage(socket, {
@@ -4716,13 +5007,13 @@ function getHeaderValue(headers, name) {
4716
5007
 
4717
5008
  // src/workdir-layout.ts
4718
5009
  import { lstat as lstat3, mkdir as mkdir10, symlink as symlink3, unlink as unlink6 } from "fs/promises";
4719
- import path12 from "path";
5010
+ import path13 from "path";
4720
5011
  async function provisionWorkdirLayout(workdirRoot) {
4721
5012
  const manifest = await readBundledManifest();
4722
5013
  await mkdir10(workdirRoot, { recursive: true });
4723
- await mkdir10(path12.join(workdirRoot, "system", ".agents"), { recursive: true });
4724
- const systemSkillsLink = path12.join(workdirRoot, "system", ".agents", "skills");
4725
- const systemScriptsLink = path12.join(workdirRoot, "system", "scripts");
5014
+ await mkdir10(path13.join(workdirRoot, "system", ".agents"), { recursive: true });
5015
+ const systemSkillsLink = path13.join(workdirRoot, "system", ".agents", "skills");
5016
+ const systemScriptsLink = path13.join(workdirRoot, "system", "scripts");
4726
5017
  await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
4727
5018
  await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
4728
5019
  await writeLog("info", "workdir layout provisioned", {
@@ -4754,7 +5045,7 @@ async function ensureSymlink(target, linkPath) {
4754
5045
  }
4755
5046
  } catch {
4756
5047
  }
4757
- await mkdir10(path12.dirname(linkPath), { recursive: true });
5048
+ await mkdir10(path13.dirname(linkPath), { recursive: true });
4758
5049
  await symlink3(target, linkPath, "dir");
4759
5050
  }
4760
5051
 
@@ -4765,13 +5056,13 @@ import {
4765
5056
  lstat as lstat4,
4766
5057
  mkdir as mkdir11,
4767
5058
  open as open3,
4768
- readFile as readFile9,
4769
- realpath as realpath3,
5059
+ readFile as readFile10,
5060
+ realpath as realpath4,
4770
5061
  rename as rename6,
4771
5062
  statfs,
4772
5063
  unlink as unlink7
4773
5064
  } from "fs/promises";
4774
- import path13 from "path";
5065
+ import path14 from "path";
4775
5066
  var REGISTRY_VERSION = 1;
4776
5067
  var REGISTRY_DIRECTORY = ".buda";
4777
5068
  var REGISTRY_FILENAME = "workspace-registry.json";
@@ -4795,13 +5086,13 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
4795
5086
  await mkdir11(storageRoot, { recursive: true, mode: 448 });
4796
5087
  await rejectUnsafeFilesystem(storageRoot);
4797
5088
  const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
4798
- const registryDirectory = path13.join(canonicalRoot, REGISTRY_DIRECTORY);
5089
+ const registryDirectory = path14.join(canonicalRoot, REGISTRY_DIRECTORY);
4799
5090
  await ensurePrivateDirectory2(registryDirectory);
4800
- const registryPath = path13.join(registryDirectory, REGISTRY_FILENAME);
5091
+ const registryPath = path14.join(registryDirectory, REGISTRY_FILENAME);
4801
5092
  const document = await withExclusiveFileLock(registryPath, async () => {
4802
5093
  let current;
4803
5094
  try {
4804
- current = parseRegistry(await readFile9(registryPath, "utf8"));
5095
+ current = parseRegistry(await readFile10(registryPath, "utf8"));
4805
5096
  } catch (error) {
4806
5097
  if (isMissingFile2(error)) {
4807
5098
  current = { version: REGISTRY_VERSION, storageRoot: canonicalRoot, workspaces: {} };
@@ -4866,23 +5157,23 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
4866
5157
  };
4867
5158
  var readRegistryDocument = async (registryPath) => {
4868
5159
  try {
4869
- return parseRegistry(await readFile9(registryPath, "utf8"));
5160
+ return parseRegistry(await readFile10(registryPath, "utf8"));
4870
5161
  } catch (error) {
4871
5162
  if (error instanceof WorkspaceRegistryError) throw error;
4872
5163
  throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
4873
5164
  }
4874
5165
  };
4875
5166
  var deriveWorkspace = (storageRoot, spaceId, agentId) => {
4876
- const spaceRoot = path13.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
4877
- const agentRoot = path13.join(spaceRoot, "agents", agentId);
5167
+ const spaceRoot = path14.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
5168
+ const agentRoot = path14.join(spaceRoot, "agents", agentId);
4878
5169
  return {
4879
5170
  workspaceKey: `${spaceId}:${agentId}`,
4880
5171
  spaceId,
4881
5172
  agentId,
4882
- privateVolume: path13.join(agentRoot, "workspace"),
4883
- sharedVolume: path13.join(spaceRoot, "shared"),
4884
- stateVolume: path13.join(agentRoot, "state"),
4885
- temporaryVolume: path13.join(agentRoot, "tmp")
5173
+ privateVolume: path14.join(agentRoot, "workspace"),
5174
+ sharedVolume: path14.join(spaceRoot, "shared"),
5175
+ stateVolume: path14.join(agentRoot, "state"),
5176
+ temporaryVolume: path14.join(agentRoot, "tmp")
4886
5177
  };
4887
5178
  };
4888
5179
  var parseRegistry = (raw) => {
@@ -4928,9 +5219,9 @@ var reconcileWorkspace = async (workspace, storageRoot) => {
4928
5219
  workspace.temporaryVolume
4929
5220
  ];
4930
5221
  for (const volume of volumes) {
4931
- assertContained(storageRoot, volume);
5222
+ assertContained2(storageRoot, volume);
4932
5223
  await ensurePrivateDirectory2(volume);
4933
- const canonical = await realpath3(volume);
5224
+ const canonical = await realpath4(volume);
4934
5225
  if (canonical !== volume) throw corrupt2("Workspace path resolves through a symbolic link");
4935
5226
  }
4936
5227
  };
@@ -4976,7 +5267,7 @@ var canonicalDirectory = async (directory, label) => {
4976
5267
  if (info.isSymbolicLink() || !info.isDirectory()) {
4977
5268
  throw new WorkspaceRegistryError("STORAGE_UNSAFE", `${label} must be a real directory`);
4978
5269
  }
4979
- return realpath3(directory);
5270
+ return realpath4(directory);
4980
5271
  };
4981
5272
  var rejectUnsafeFilesystem = async (storageRoot) => {
4982
5273
  const stats = await statfs(storageRoot, { bigint: true });
@@ -5025,12 +5316,12 @@ var validateStoredId = (label, value) => {
5025
5316
  throw corrupt2(errorMessage2(error));
5026
5317
  }
5027
5318
  };
5028
- var assertContained = (root, candidate) => {
5319
+ var assertContained2 = (root, candidate) => {
5029
5320
  if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
5030
5321
  };
5031
5322
  var isPathInside2 = (parent, child) => {
5032
- const relative = path13.relative(parent, child);
5033
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path13.sep}`) && !path13.isAbsolute(relative);
5323
+ const relative = path14.relative(parent, child);
5324
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path14.sep}`) && !path14.isAbsolute(relative);
5034
5325
  };
5035
5326
  var toDescriptor = (workspace) => ({
5036
5327
  workspaceKey: workspace.workspaceKey,
@@ -5202,7 +5493,7 @@ async function runOnboard(launchConfig, options = {}) {
5202
5493
 
5203
5494
  // src/runtime.ts
5204
5495
  import { mkdir as mkdir12 } from "fs/promises";
5205
- import path14 from "path";
5496
+ import path15 from "path";
5206
5497
  var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
5207
5498
  var RECOVERED_DAEMON_STOP_POLL_MS = 100;
5208
5499
  var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
@@ -5258,7 +5549,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5258
5549
  config.connectorId,
5259
5550
  configRoot
5260
5551
  );
5261
- await mkdir12(path14.dirname(daemonLockTarget), { recursive: true, mode: 448 });
5552
+ await mkdir12(path15.dirname(daemonLockTarget), { recursive: true, mode: 448 });
5262
5553
  let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
5263
5554
  throw new Error(
5264
5555
  `Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
@@ -5268,6 +5559,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5268
5559
  let bunnyDaemon;
5269
5560
  let relayProtocol;
5270
5561
  let control;
5562
+ let heartbeatHealthy = true;
5271
5563
  let lastHeartbeatAt;
5272
5564
  try {
5273
5565
  await migrateLegacyWorkspaceRoot(config.workdirRoot);
@@ -5291,7 +5583,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5291
5583
  registration.connectorId,
5292
5584
  configRoot
5293
5585
  );
5294
- await mkdir12(path14.dirname(registeredLockTarget), { recursive: true, mode: 448 });
5586
+ await mkdir12(path15.dirname(registeredLockTarget), { recursive: true, mode: 448 });
5295
5587
  const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
5296
5588
  timeoutMs: 0
5297
5589
  }).catch(() => {
@@ -5346,6 +5638,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5346
5638
  };
5347
5639
  interval = setInterval(() => {
5348
5640
  heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
5641
+ heartbeatHealthy = true;
5349
5642
  lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
5350
5643
  relay.updateRelayProtocol(nextRelayProtocol);
5351
5644
  relay.resumeAfterAccessRestore();
@@ -5353,6 +5646,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5353
5646
  heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
5354
5647
  });
5355
5648
  }).catch((error) => {
5649
+ heartbeatHealthy = false;
5356
5650
  if (error instanceof ConnectorDeletedError) {
5357
5651
  void writeLog("warn", "connector deleted remotely; stopping daemon");
5358
5652
  void shutdown({ unregister: false });
@@ -5368,6 +5662,7 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5368
5662
  config,
5369
5663
  configRoot,
5370
5664
  getLastHeartbeatAt: () => lastHeartbeatAt,
5665
+ isHeartbeatHealthy: () => heartbeatHealthy,
5371
5666
  isIdle: () => relay.isIdle(),
5372
5667
  isRelayConnected: () => relay.isConnected(),
5373
5668
  mode: options.mode ?? "persistent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buda-ai/connector",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "publishConfig": {