@buda-ai/connector 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config.ts
4
- import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
4
+ import { createHash as createHash2, randomBytes, randomUUID as randomUUID2 } from "crypto";
5
5
  import { chmod, mkdir, readFile as readFile2, rename, unlink as unlink2, writeFile } from "fs/promises";
6
6
  import { homedir, hostname, platform as platform2 } from "os";
7
7
  import path from "path";
8
8
 
9
9
  // src/exclusive-file-lock.ts
10
+ import { randomUUID } from "crypto";
10
11
  import { open, readFile, stat, unlink } from "fs/promises";
11
12
  var DEFAULT_TIMEOUT_MS = 1e4;
12
13
  var DEFAULT_STALE_MS = 12e4;
@@ -33,8 +34,11 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
33
34
  await wait(pollMs);
34
35
  continue;
35
36
  }
36
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, createdAt: Date.now() })}
37
- `);
37
+ const ownerToken = randomUUID();
38
+ await handle.writeFile(
39
+ `${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
40
+ `
41
+ );
38
42
  await handle.sync();
39
43
  let released = false;
40
44
  return {
@@ -42,7 +46,8 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
42
46
  if (released) return;
43
47
  released = true;
44
48
  await handle.close();
45
- await unlink(lockPath).catch(() => void 0);
49
+ const currentOwner = await readFile(lockPath, "utf8").then((raw) => JSON.parse(raw).ownerToken).catch(() => void 0);
50
+ if (currentOwner === ownerToken) await unlink(lockPath).catch(() => void 0);
46
51
  }
47
52
  };
48
53
  }
@@ -154,12 +159,23 @@ function getConnectorProfileKey(serverUrl, token) {
154
159
  const discriminator = token?.trim() ? getConnectorTokenFingerprint(token) : "legacy-no-token";
155
160
  return `${getServerOriginKey(serverUrl)}--${discriminator}`;
156
161
  }
162
+ function getConnectorIdentityKey(deviceId, serverUrl, spaceId) {
163
+ const normalizedSpaceId = spaceId.trim();
164
+ if (!deviceId.trim() || !normalizedSpaceId) {
165
+ throw new Error("Connector identity requires deviceId and spaceId");
166
+ }
167
+ const digest = createHash2("sha256").update(`${deviceId.trim()}
168
+ ${normalizeServerOrigin(serverUrl)}
169
+ ${normalizedSpaceId}`, "utf8").digest("hex").slice(0, 24);
170
+ const spaceKey = normalizedSpaceId.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase();
171
+ return `${getServerOriginKey(serverUrl)}--${spaceKey}--${digest}`;
172
+ }
157
173
  function getConnectorPaths(serverUrl, configRoot = path.join(homedir(), ".buda")) {
158
174
  const scopedLegacyDir = path.join(configRoot, "servers", getServerOriginKey(serverUrl));
159
175
  return {
160
176
  configDir: configRoot,
161
177
  configPath: path.join(configRoot, "device.json"),
162
- logPath: path.join(configRoot, "connector.log"),
178
+ logPath: path.join(configRoot, "logs", "connector.log"),
163
179
  workdirRoot: path.join(configRoot, "agents"),
164
180
  scopedLegacyConfigPath: path.join(scopedLegacyDir, "device.json"),
165
181
  scopedLegacyWorkdirRoot: path.join(scopedLegacyDir, "agents")
@@ -175,6 +191,17 @@ function getConnectorDaemonLockTarget(serverUrl, connectorId, configRoot = path.
175
191
  "connector-daemon"
176
192
  );
177
193
  }
194
+ function getConnectorBootstrapLockTarget(launchConfig, configRoot = path.join(homedir(), ".buda")) {
195
+ const serverUrl = getServerUrl(launchConfig);
196
+ const scope = launchConfig?.spaceId?.trim() ? `space-${launchConfig.spaceId.trim()}` : getConnectorToken(launchConfig) ? `token-${getConnectorTokenFingerprint(getConnectorToken(launchConfig)).slice(0, 24)}` : "oauth-unresolved";
197
+ return path.join(
198
+ configRoot,
199
+ "daemons",
200
+ getServerOriginKey(serverUrl),
201
+ scope.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase(),
202
+ "bootstrap"
203
+ );
204
+ }
178
205
  function getServerUrl(launchConfig) {
179
206
  return (launchConfig?.serverUrl || process.env.BUDA_CONNECTOR_SERVER_URL || process.env.BUDA_SERVER_URL || process.env.NEXT_PUBLIC_APP_URL || DEFAULT_SERVER_URL).replace(/\/$/, "");
180
207
  }
@@ -184,6 +211,13 @@ function getConnectorRelayUrl(serverUrl, connectorId) {
184
211
  url.searchParams.set("connectorId", connectorId);
185
212
  return url.toString();
186
213
  }
214
+ function getConnectorTerminalUrl(serverUrl, connectorId, sessionId) {
215
+ const url = new URL("/api/connectors/terminal", serverUrl);
216
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
217
+ url.searchParams.set("connectorId", connectorId);
218
+ url.searchParams.set("sessionId", sessionId);
219
+ return url.toString();
220
+ }
187
221
  function getWorkdirRoot(launchConfig) {
188
222
  return launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || getConnectorPaths(getServerUrl(launchConfig)).workdirRoot;
189
223
  }
@@ -200,15 +234,16 @@ function getBunnyDaemonPort(launchConfig) {
200
234
  function getConnectorToken(launchConfig) {
201
235
  return launchConfig?.token?.trim() || process.env.BUDA_CONNECTOR_TOKEN?.trim() || void 0;
202
236
  }
203
- function buildDeviceConfig(launchConfig, deviceId = `dvc_${randomUUID()}`) {
237
+ function buildDeviceConfig(launchConfig, deviceId = `dvc_${randomUUID2()}`) {
204
238
  const host = hostname();
205
239
  const token = getConnectorToken(launchConfig);
206
240
  return {
207
- connectorId: `cnr_${randomUUID()}`,
241
+ connectorId: `cnr_${randomUUID2()}`,
208
242
  deviceId,
209
243
  deviceName: host,
210
244
  hostLabel: `${host} (${platform2()})`,
211
245
  serverUrl: getServerUrl(launchConfig),
246
+ ...launchConfig?.spaceId?.trim() ? { spaceId: launchConfig.spaceId.trim() } : {},
212
247
  workdirRoot: getWorkdirRoot(launchConfig),
213
248
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
214
249
  deviceToken: randomBytes(32).toString("base64url"),
@@ -220,12 +255,18 @@ function isDeviceConfig(value) {
220
255
  const config = value;
221
256
  return typeof config.connectorId === "string" && typeof config.deviceId === "string" && typeof config.serverUrl === "string";
222
257
  }
258
+ function isCanonicalDeviceConfig(value) {
259
+ return isDeviceConfig(value) && typeof value.spaceId === "string" && value.spaceId.trim().length > 0;
260
+ }
223
261
  async function readJsonFile(configPath) {
224
262
  try {
225
263
  const raw = await readFile2(configPath, "utf8");
226
264
  return JSON.parse(raw);
227
- } catch {
228
- return null;
265
+ } catch (error) {
266
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
267
+ return null;
268
+ }
269
+ throw new Error(`Invalid Connector config ${configPath}`, { cause: error });
229
270
  }
230
271
  }
231
272
  function migrateLegacyProfiles(values) {
@@ -242,36 +283,68 @@ function migrateLegacyProfiles(values) {
242
283
  async function loadDeviceConfigStore(configPath) {
243
284
  const parsed = await readJsonFile(configPath);
244
285
  const candidate = parsed;
286
+ if (candidate?.version === 4 && candidate.identities && typeof candidate.identities === "object" && candidate.aliases && typeof candidate.aliases === "object") {
287
+ const identities = candidate.identities;
288
+ for (const [identityKey, value] of Object.entries(identities)) {
289
+ if (!isCanonicalDeviceConfig(value)) {
290
+ throw new Error(`Invalid canonical Connector identity ${identityKey}`);
291
+ }
292
+ const expected = getConnectorIdentityKey(value.deviceId, value.serverUrl, value.spaceId);
293
+ if (identityKey !== expected) {
294
+ throw new Error(`Connector identity key does not match ${value.deviceId}/${value.spaceId}`);
295
+ }
296
+ }
297
+ return {
298
+ store: {
299
+ version: 4,
300
+ ...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
301
+ identities,
302
+ aliases: candidate.aliases
303
+ },
304
+ legacyProfiles: {},
305
+ needsWrite: false
306
+ };
307
+ }
245
308
  if (candidate?.version === 3 && candidate.profiles && typeof candidate.profiles === "object") {
246
309
  return {
247
310
  store: {
248
- version: 3,
311
+ version: 4,
249
312
  ...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
250
- profiles: candidate.profiles
313
+ identities: {},
314
+ aliases: {}
251
315
  },
316
+ legacyProfiles: candidate.profiles,
252
317
  needsWrite: false
253
318
  };
254
319
  }
255
320
  if (candidate && (candidate.version === 1 || candidate.version === 2) && candidate.servers && typeof candidate.servers === "object") {
256
321
  return {
257
322
  store: {
258
- version: 3,
323
+ version: 4,
259
324
  ...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
260
- profiles: migrateLegacyProfiles(Object.values(candidate.servers))
325
+ identities: {},
326
+ aliases: {}
261
327
  },
262
- needsWrite: true
328
+ legacyProfiles: migrateLegacyProfiles(Object.values(candidate.servers)),
329
+ needsWrite: false
263
330
  };
264
331
  }
265
332
  if (isDeviceConfig(parsed)) {
266
333
  return {
267
334
  store: {
268
- version: 3,
269
- profiles: migrateLegacyProfiles([parsed])
335
+ version: 4,
336
+ identities: {},
337
+ aliases: {}
270
338
  },
271
- needsWrite: true
339
+ legacyProfiles: migrateLegacyProfiles([parsed]),
340
+ needsWrite: false
272
341
  };
273
342
  }
274
- return { store: { version: 3, profiles: {} }, needsWrite: false };
343
+ return {
344
+ store: { version: 4, identities: {}, aliases: {} },
345
+ legacyProfiles: {},
346
+ needsWrite: false
347
+ };
275
348
  }
276
349
  function normalizeStoredWorkdirRoot(config, paths) {
277
350
  const configuredRoot = config.workdirRoot?.trim();
@@ -289,7 +362,7 @@ function normalizeStoredWorkdirRoot(config, paths) {
289
362
  }
290
363
  async function writeDeviceConfigStore(configPath, store) {
291
364
  await mkdir(path.dirname(configPath), { recursive: true });
292
- const temporaryPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
365
+ const temporaryPath = `${configPath}.${process.pid}.${randomUUID2()}.tmp`;
293
366
  try {
294
367
  await writeFile(temporaryPath, `${JSON.stringify(store, null, 2)}
295
368
  `, { mode: 384 });
@@ -299,36 +372,70 @@ async function writeDeviceConfigStore(configPath, store) {
299
372
  await unlink2(temporaryPath).catch(() => void 0);
300
373
  }
301
374
  }
375
+ function findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths) {
376
+ const aliases = [
377
+ ...launchConfig?.spaceId?.trim() ? [`${getServerOriginKey(serverUrl)}--space--${launchConfig.spaceId.trim()}`] : [],
378
+ ...getConnectorToken(launchConfig) ? [getConnectorProfileKey(serverUrl, getConnectorToken(launchConfig))] : []
379
+ ];
380
+ const identityKey = aliases.map((alias) => store.aliases[alias]).find(Boolean);
381
+ let storedConfig = identityKey ? store.identities[identityKey] : void 0;
382
+ if (!storedConfig && getConnectorToken(launchConfig)) {
383
+ const legacyKey = getConnectorProfileKey(serverUrl, getConnectorToken(launchConfig));
384
+ storedConfig = legacyProfiles[legacyKey];
385
+ }
386
+ if (!isDeviceConfig(storedConfig)) return null;
387
+ return normalizeStoredWorkdirRoot(storedConfig, paths).config;
388
+ }
389
+ function putCanonicalDeviceConfig(store, config) {
390
+ if (!config.spaceId?.trim()) {
391
+ throw new Error("Cannot persist Connector identity before the server resolves spaceId");
392
+ }
393
+ const spaceId = config.spaceId.trim();
394
+ const identityKey = getConnectorIdentityKey(config.deviceId, config.serverUrl, spaceId);
395
+ store.identities[identityKey] = { ...config, spaceId };
396
+ store.aliases[`${getServerOriginKey(config.serverUrl)}--space--${spaceId}`] = identityKey;
397
+ if (config.token?.trim()) {
398
+ store.aliases[getConnectorProfileKey(config.serverUrl, config.token)] = identityKey;
399
+ }
400
+ }
302
401
  async function readDeviceConfig(launchConfig, configRoot) {
303
402
  const serverUrl = getServerUrl(launchConfig);
304
403
  const paths = getConnectorPaths(serverUrl, configRoot);
305
- const launchToken = getConnectorToken(launchConfig);
306
404
  await mkdir(paths.configDir, { recursive: true });
307
405
  return withExclusiveFileLock(paths.configPath, async () => {
308
- const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
309
- const profileKey = getConnectorProfileKey(serverUrl, launchToken);
310
- const storedConfig = store.profiles[profileKey];
311
- let config = isDeviceConfig(storedConfig) ? storedConfig : null;
312
- let normalizedWorkdirRoot = false;
313
- if (config) {
314
- const normalized = normalizeStoredWorkdirRoot(config, paths);
315
- config = normalized.config;
316
- normalizedWorkdirRoot = normalized.changed;
317
- store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
318
- }
319
- if (needsWrite || normalizedWorkdirRoot) {
320
- await writeDeviceConfigStore(paths.configPath, store);
406
+ const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
407
+ return findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths);
408
+ });
409
+ }
410
+ async function listDeviceConfigs(configRoot) {
411
+ const paths = getConnectorPaths(DEFAULT_SERVER_URL, configRoot);
412
+ await mkdir(paths.configDir, { recursive: true });
413
+ return withExclusiveFileLock(paths.configPath, async () => {
414
+ const { legacyProfiles, store } = await loadDeviceConfigStore(paths.configPath);
415
+ const identities = [...Object.values(store.identities), ...Object.values(legacyProfiles)].filter(isCanonicalDeviceConfig).map(
416
+ (config) => normalizeStoredWorkdirRoot(config, getConnectorPaths(config.serverUrl, configRoot)).config
417
+ );
418
+ const unique = /* @__PURE__ */ new Map();
419
+ for (const config of identities) {
420
+ unique.set(`${normalizeServerOrigin(config.serverUrl)}
421
+ ${config.connectorId}`, config);
321
422
  }
322
- return config;
423
+ return [...unique.values()].sort(
424
+ (left, right) => left.createdAt.localeCompare(right.createdAt) || left.connectorId.localeCompare(right.connectorId)
425
+ );
323
426
  });
324
427
  }
325
428
  async function writeDeviceConfig(config, configRoot) {
429
+ if (!config.spaceId?.trim()) {
430
+ throw new Error("Cannot persist Connector identity before the server resolves spaceId");
431
+ }
432
+ const spaceId = config.spaceId.trim();
326
433
  const paths = getConnectorPaths(config.serverUrl, configRoot);
327
434
  await mkdir(config.workdirRoot, { recursive: true });
328
435
  await mkdir(paths.configDir, { recursive: true });
329
436
  await withExclusiveFileLock(paths.configPath, async () => {
330
437
  const { store } = await loadDeviceConfigStore(paths.configPath);
331
- store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
438
+ putCanonicalDeviceConfig(store, { ...config, spaceId });
332
439
  await writeDeviceConfigStore(paths.configPath, store);
333
440
  });
334
441
  }
@@ -336,13 +443,12 @@ async function getOrCreateSystemDeviceId(configRoot, resolver = resolveSystemDev
336
443
  const paths = getConnectorPaths(DEFAULT_SERVER_URL, configRoot);
337
444
  await mkdir(paths.configDir, { recursive: true });
338
445
  return withExclusiveFileLock(paths.configPath, async () => {
339
- const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
446
+ const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
340
447
  if (store.systemDeviceId) {
341
- if (needsWrite) await writeDeviceConfigStore(paths.configPath, store);
342
448
  return store.systemDeviceId;
343
449
  }
344
- const existingDeviceId = Object.values(store.profiles).find(isDeviceConfig)?.deviceId;
345
- store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID()}`;
450
+ const existingDeviceId = Object.values(store.identities).find(isDeviceConfig)?.deviceId ?? Object.values(legacyProfiles).find(isDeviceConfig)?.deviceId;
451
+ store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID2()}`;
346
452
  await writeDeviceConfigStore(paths.configPath, store);
347
453
  return store.systemDeviceId;
348
454
  });
@@ -354,33 +460,28 @@ async function createDeviceConfig(launchConfig, configRoot, resolver) {
354
460
  async function ensureDeviceConfig(launchConfig, configRoot, resolver = resolveSystemDeviceId) {
355
461
  const serverUrl = getServerUrl(launchConfig);
356
462
  const paths = getConnectorPaths(serverUrl, configRoot);
357
- const token = getConnectorToken(launchConfig);
358
- const profileKey = getConnectorProfileKey(serverUrl, token);
359
463
  await mkdir(paths.configDir, { recursive: true });
360
464
  const config = await withExclusiveFileLock(paths.configPath, async () => {
361
- const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
362
- const storedConfig = store.profiles[profileKey];
363
- if (isDeviceConfig(storedConfig)) {
364
- const normalized = normalizeStoredWorkdirRoot(storedConfig, paths);
365
- if (needsWrite || normalized.changed) {
366
- store.profiles[profileKey] = normalized.config;
367
- await writeDeviceConfigStore(paths.configPath, store);
368
- }
369
- const existing = normalized.config;
370
- const overrideToken = getConnectorToken(launchConfig);
371
- return {
465
+ const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
466
+ const existing = findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths);
467
+ if (existing) {
468
+ const resolved = {
372
469
  ...existing,
373
470
  serverUrl,
374
471
  workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig),
375
- ...overrideToken ? { token: overrideToken } : {}
472
+ ...getConnectorToken(launchConfig) ? { token: getConnectorToken(launchConfig) } : {}
376
473
  };
474
+ if (isCanonicalDeviceConfig(resolved)) {
475
+ putCanonicalDeviceConfig(store, resolved);
476
+ await writeDeviceConfigStore(paths.configPath, store);
477
+ }
478
+ return resolved;
377
479
  }
378
- if (!store.systemDeviceId) {
379
- const existingDeviceId = Object.values(store.profiles).find(isDeviceConfig)?.deviceId;
380
- store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID()}`;
381
- }
382
- const created = buildDeviceConfig(launchConfig, store.systemDeviceId);
383
- store.profiles[profileKey] = created;
480
+ const existingDeviceId = store.systemDeviceId ?? Object.values(store.identities).find(isDeviceConfig)?.deviceId ?? Object.values(legacyProfiles).find(isDeviceConfig)?.deviceId;
481
+ const deviceId = existingDeviceId || resolver() || `dvc_${randomUUID2()}`;
482
+ const created = buildDeviceConfig(launchConfig, deviceId);
483
+ store.systemDeviceId = deviceId;
484
+ if (isCanonicalDeviceConfig(created)) putCanonicalDeviceConfig(store, created);
384
485
  await writeDeviceConfigStore(paths.configPath, store);
385
486
  return created;
386
487
  });
@@ -393,6 +494,7 @@ function parseLaunchConfig(raw) {
393
494
  const bunnyDaemonPort = typeof parsed.bunnyDaemonPort === "number" && Number.isInteger(parsed.bunnyDaemonPort) ? parsed.bunnyDaemonPort : void 0;
394
495
  return {
395
496
  serverUrl: typeof parsed.serverUrl === "string" ? parsed.serverUrl : void 0,
497
+ spaceId: typeof parsed.spaceId === "string" ? parsed.spaceId : void 0,
396
498
  workdirRoot: typeof parsed.workdirRoot === "string" ? parsed.workdirRoot : void 0,
397
499
  oauthToken: typeof parsed.oauthToken === "string" ? parsed.oauthToken : void 0,
398
500
  token: typeof parsed.token === "string" ? parsed.token : void 0,
@@ -401,29 +503,1064 @@ function parseLaunchConfig(raw) {
401
503
  };
402
504
  }
403
505
 
404
- // src/onboard.ts
405
- import { cancel, intro, isCancel, log, note, outro, spinner, text } from "@clack/prompts";
506
+ // src/console-dashboard.ts
507
+ import { closeSync, openSync } from "fs";
508
+ import { ReadStream, WriteStream } from "tty";
509
+ import { format, stripVTControlCharacters } from "util";
406
510
 
407
- // src/bunny-daemon.ts
408
- import { mkdir as mkdir3 } from "fs/promises";
409
- import { createDaemon } from "@bunny-agent/daemon";
511
+ // src/daemon-control.ts
512
+ import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
513
+ import { chmod as chmod2, mkdir as mkdir2, readdir, readFile as readFile3, rename as rename2, unlink as unlink3, writeFile as writeFile2 } from "fs/promises";
514
+ import { createConnection, createServer } from "net";
515
+ import path2 from "path";
516
+
517
+ // package.json
518
+ var package_default = {
519
+ name: "@buda-ai/connector",
520
+ version: "0.1.8",
521
+ private: false,
522
+ type: "module",
523
+ publishConfig: {
524
+ access: "public"
525
+ },
526
+ files: [
527
+ "dist",
528
+ "agent-templates"
529
+ ],
530
+ bin: {
531
+ "buda-connector": "./dist/cli.js",
532
+ connector: "./dist/cli.js"
533
+ },
534
+ scripts: {
535
+ dev: "tsx src/cli.ts dev",
536
+ daemon: "tsx src/cli.ts daemon",
537
+ new: "tsx src/cli.ts new",
538
+ start: "node dist/cli.js daemon",
539
+ build: "tsup",
540
+ test: "vitest run",
541
+ typecheck: "tsc --noEmit",
542
+ lint: "biome format . --write && biome check .",
543
+ "lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
544
+ },
545
+ dependencies: {
546
+ "@bunny-agent/daemon": "0.9.51",
547
+ "@clack/prompts": "^1.0.0",
548
+ "playwright-core": "^1.52.0",
549
+ ws: "^8.21.0"
550
+ },
551
+ optionalDependencies: {
552
+ "node-pty": "^1.1.0"
553
+ },
554
+ devDependencies: {
555
+ "@types/node": "^24.13.2",
556
+ "@types/ws": "^8.18.1",
557
+ relaylib: "workspace:*",
558
+ tsup: "^8.5.0",
559
+ tsx: "^4.20.5",
560
+ typescript: "^5.9.3",
561
+ vitest: "^2.1.8"
562
+ },
563
+ engines: {
564
+ node: ">=24.18.0"
565
+ }
566
+ };
567
+
568
+ // src/daemon-control.ts
569
+ var CONNECTOR_CONTROL_PROTOCOL_VERSION = 1;
570
+ var CONNECTOR_RUNTIME_VERSION = package_default.version;
571
+ var DEFAULT_CONTROL_REQUEST_TIMEOUT_MS = 3e3;
572
+ var getDaemonControlMetadataPath = (config, configRoot) => `${getConnectorDaemonLockTarget(config.serverUrl, config.connectorId, configRoot)}.control.json`;
573
+ var writeMetadata = async (metadataPath, metadata) => {
574
+ await mkdir2(path2.dirname(metadataPath), { recursive: true, mode: 448 });
575
+ const temporaryPath = `${metadataPath}.${process.pid}.${randomUUID3()}.tmp`;
576
+ try {
577
+ await writeFile2(temporaryPath, `${JSON.stringify(metadata)}
578
+ `, { mode: 384 });
579
+ await rename2(temporaryPath, metadataPath);
580
+ await chmod2(metadataPath, 384);
581
+ } finally {
582
+ await unlink3(temporaryPath).catch(() => void 0);
583
+ }
584
+ };
585
+ var publicStatus = (metadata) => ({
586
+ connectorId: metadata.connectorId,
587
+ idle: metadata.idle,
588
+ instanceId: metadata.instanceId,
589
+ ...metadata.lastHeartbeatAt ? { lastHeartbeatAt: metadata.lastHeartbeatAt } : {},
590
+ mode: metadata.mode,
591
+ pid: metadata.pid,
592
+ protocolVersion: metadata.protocolVersion,
593
+ relayConnected: metadata.relayConnected,
594
+ serverUrl: metadata.serverUrl,
595
+ ...metadata.spaceId ? { spaceId: metadata.spaceId } : {},
596
+ startedAt: metadata.startedAt,
597
+ version: metadata.version
598
+ });
599
+ var parseRequest = (raw) => {
600
+ try {
601
+ const value = JSON.parse(raw);
602
+ if (typeof value.secret !== "string" || !["hold", "persist", "release", "status", "stop", "stop-if-idle"].includes(
603
+ value.command ?? ""
604
+ )) {
605
+ return null;
606
+ }
607
+ return value;
608
+ } catch {
609
+ return null;
610
+ }
611
+ };
612
+ async function startDaemonControlServer(options) {
613
+ const metadataPath = getDaemonControlMetadataPath(options.config, options.configRoot);
614
+ let stopping = false;
615
+ let desktopHolds = 0;
616
+ let metadata;
617
+ let startupGrace;
618
+ const sockets = /* @__PURE__ */ new Set();
619
+ const requestStop = () => {
620
+ if (stopping) return;
621
+ stopping = true;
622
+ void Promise.resolve(options.onStop());
623
+ };
624
+ const server = createServer((socket) => {
625
+ sockets.add(socket);
626
+ socket.setEncoding("utf8");
627
+ let buffer = "";
628
+ let holding = false;
629
+ socket.on("data", (chunk) => {
630
+ buffer += chunk;
631
+ const newline = buffer.indexOf("\n");
632
+ if (newline < 0) return;
633
+ const request = parseRequest(buffer.slice(0, newline));
634
+ buffer = buffer.slice(newline + 1);
635
+ if (!request || request.secret !== metadata.secret) {
636
+ socket.end(
637
+ `${JSON.stringify({ ok: false, error: "unauthorized" })}
638
+ `
639
+ );
640
+ return;
641
+ }
642
+ metadata.idle = options.isIdle?.() ?? metadata.idle;
643
+ metadata.lastHeartbeatAt = options.getLastHeartbeatAt?.() ?? metadata.lastHeartbeatAt;
644
+ metadata.relayConnected = options.isRelayConnected?.() ?? metadata.relayConnected;
645
+ if (request.command === "persist") {
646
+ if (startupGrace) clearTimeout(startupGrace);
647
+ metadata.mode = "persistent";
648
+ void writeMetadata(metadataPath, metadata);
649
+ } else if (request.command === "hold") {
650
+ if (startupGrace) clearTimeout(startupGrace);
651
+ holding = true;
652
+ desktopHolds += 1;
653
+ } else if (request.command === "release") {
654
+ if (metadata.mode === "transient" && desktopHolds === 0) requestStop();
655
+ } else if (request.command === "stop") {
656
+ requestStop();
657
+ } else if (request.command === "stop-if-idle" && metadata.idle) {
658
+ requestStop();
659
+ }
660
+ socket.write(
661
+ `${JSON.stringify({ ok: true, status: publicStatus(metadata) })}
662
+ `
663
+ );
664
+ if (request.command !== "hold") socket.end();
665
+ });
666
+ socket.on("close", () => {
667
+ sockets.delete(socket);
668
+ if (!holding) return;
669
+ desktopHolds = Math.max(0, desktopHolds - 1);
670
+ if (metadata.mode === "transient" && desktopHolds === 0) requestStop();
671
+ });
672
+ });
673
+ await new Promise((resolve, reject) => {
674
+ server.once("error", reject);
675
+ server.listen(0, "127.0.0.1", () => {
676
+ server.off("error", reject);
677
+ resolve();
678
+ });
679
+ });
680
+ const address = server.address();
681
+ if (!address || typeof address === "string")
682
+ throw new Error("Failed to bind daemon control port");
683
+ metadata = {
684
+ connectorId: options.config.connectorId,
685
+ idle: options.isIdle?.() ?? true,
686
+ instanceId: randomUUID3(),
687
+ mode: options.mode,
688
+ pid: process.pid,
689
+ port: address.port,
690
+ protocolVersion: CONNECTOR_CONTROL_PROTOCOL_VERSION,
691
+ relayConnected: options.isRelayConnected?.() ?? false,
692
+ secret: randomBytes2(32).toString("base64url"),
693
+ serverUrl: options.config.serverUrl,
694
+ ...options.config.spaceId ? { spaceId: options.config.spaceId } : {},
695
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
696
+ version: options.version ?? CONNECTOR_RUNTIME_VERSION
697
+ };
698
+ await writeMetadata(metadataPath, metadata);
699
+ if (metadata.mode === "transient") {
700
+ startupGrace = setTimeout(requestStop, options.transientGraceMs ?? 3e4);
701
+ }
702
+ return {
703
+ close: async () => {
704
+ if (startupGrace) clearTimeout(startupGrace);
705
+ for (const socket of sockets) socket.destroy();
706
+ await new Promise((resolve) => server.close(() => resolve()));
707
+ const activeInstanceId = await readFile3(metadataPath, "utf8").then((raw) => JSON.parse(raw).instanceId).catch(() => void 0);
708
+ if (activeInstanceId === metadata.instanceId) {
709
+ await unlink3(metadataPath).catch(() => void 0);
710
+ }
711
+ },
712
+ setIdle: (idle) => {
713
+ metadata.idle = idle;
714
+ },
715
+ status: () => publicStatus(metadata)
716
+ };
717
+ }
718
+ var parseMetadata = (raw, expectedConnectorId) => {
719
+ const metadata = JSON.parse(raw);
720
+ if (metadata.protocolVersion !== CONNECTOR_CONTROL_PROTOCOL_VERSION || typeof metadata.port !== "number" || typeof metadata.secret !== "string" || typeof metadata.connectorId !== "string" || typeof metadata.serverUrl !== "string" || expectedConnectorId && metadata.connectorId !== expectedConnectorId) {
721
+ throw new Error("Invalid Connector daemon control metadata");
722
+ }
723
+ return metadata;
724
+ };
725
+ var readMetadata = async (config, configRoot) => {
726
+ const metadataPath = getDaemonControlMetadataPath(config, configRoot);
727
+ return parseMetadata(await readFile3(metadataPath, "utf8"), config.connectorId);
728
+ };
729
+ async function listPersistedDaemonStatuses(configRoot) {
730
+ const daemonRoot = path2.join(
731
+ getConnectorPaths(DEFAULT_SERVER_URL, configRoot).configDir,
732
+ "daemons"
733
+ );
734
+ const serverDirectories = await readdir(daemonRoot, { withFileTypes: true }).catch(() => []);
735
+ const statuses = await Promise.all(
736
+ serverDirectories.filter((entry) => entry.isDirectory()).map(async (serverEntry) => {
737
+ const serverDirectory = path2.join(daemonRoot, serverEntry.name);
738
+ const connectorEntries = await readdir(serverDirectory, { withFileTypes: true }).catch(
739
+ () => []
740
+ );
741
+ return Promise.all(
742
+ connectorEntries.filter((entry) => entry.isDirectory()).map(
743
+ (entry) => readFile3(
744
+ path2.join(serverDirectory, entry.name, "connector-daemon.control.json"),
745
+ "utf8"
746
+ ).then((raw) => publicStatus(parseMetadata(raw))).catch(() => null)
747
+ )
748
+ );
749
+ })
750
+ );
751
+ const unique = /* @__PURE__ */ new Map();
752
+ for (const status of statuses.flat()) {
753
+ if (!status) continue;
754
+ unique.set(`${status.serverUrl}
755
+ ${status.connectorId}`, status);
756
+ }
757
+ return [...unique.values()];
758
+ }
759
+ var connectControl = async (metadata, command, timeoutMs = DEFAULT_CONTROL_REQUEST_TIMEOUT_MS) => new Promise((resolve, reject) => {
760
+ const socket = createConnection({ host: "127.0.0.1", port: metadata.port });
761
+ let buffer = "";
762
+ let settled = false;
763
+ const cleanup = () => {
764
+ socket.setTimeout(0);
765
+ socket.off("data", onData);
766
+ socket.off("error", fail);
767
+ socket.off("timeout", onTimeout);
768
+ };
769
+ const fail = (error) => {
770
+ if (settled) return;
771
+ settled = true;
772
+ cleanup();
773
+ socket.destroy();
774
+ reject(error);
775
+ };
776
+ const onTimeout = () => {
777
+ fail(new Error(`Connector daemon control timed out after ${timeoutMs}ms`));
778
+ };
779
+ const onData = (chunk) => {
780
+ buffer += chunk;
781
+ const newline = buffer.indexOf("\n");
782
+ if (newline < 0) return;
783
+ try {
784
+ const response = JSON.parse(buffer.slice(0, newline));
785
+ if (!response.ok) return fail(new Error(response.error ?? "Daemon control request failed"));
786
+ settled = true;
787
+ cleanup();
788
+ resolve({ response, socket });
789
+ } catch (error) {
790
+ fail(error instanceof Error ? error : new Error(String(error)));
791
+ }
792
+ };
793
+ socket.once("error", fail);
794
+ socket.once("timeout", onTimeout);
795
+ socket.setTimeout(timeoutMs);
796
+ socket.once("connect", () => {
797
+ socket.write(
798
+ `${JSON.stringify({ command, secret: metadata.secret })}
799
+ `
800
+ );
801
+ });
802
+ socket.setEncoding("utf8");
803
+ socket.on("data", onData);
804
+ });
805
+ async function readPersistedDaemonStatus(config, configRoot) {
806
+ return publicStatus(await readMetadata(config, configRoot));
807
+ }
808
+ async function requestDaemonControl(config, command, configRoot, options = {}) {
809
+ const metadata = await readMetadata(config, configRoot);
810
+ const { response, socket } = await connectControl(metadata, command, options.timeoutMs);
811
+ socket.end();
812
+ if (!response.status) throw new Error("Daemon control response did not include status");
813
+ return response.status;
814
+ }
815
+ async function holdDaemonControl(config, configRoot) {
816
+ const metadata = await readMetadata(config, configRoot);
817
+ const { response, socket } = await connectControl(metadata, "hold");
818
+ if (!response.status) {
819
+ socket.destroy();
820
+ throw new Error("Daemon hold response did not include status");
821
+ }
822
+ const closed = new Promise((resolve) => socket.once("close", () => resolve()));
823
+ return { close: () => socket.end(), closed, status: response.status };
824
+ }
825
+
826
+ // src/console-dashboard.ts
827
+ var MAX_LINES_PER_PANE = 500;
828
+ var MIN_DASHBOARD_WIDTH = 60;
829
+ var MIN_DASHBOARD_HEIGHT = 18;
830
+ var DASHBOARD_TITLE = `Buda Connector v${CONNECTOR_RUNTIME_VERSION}`;
831
+ var colors = {
832
+ cyan: "\x1B[36m",
833
+ dim: "\x1B[2m",
834
+ red: "\x1B[31m",
835
+ reset: "\x1B[0m",
836
+ reverse: "\x1B[7m",
837
+ yellow: "\x1B[33m"
838
+ };
839
+ var openControllingTerminal = () => {
840
+ const inputPath = process.platform === "win32" ? "CONIN$" : "/dev/tty";
841
+ const outputPath = process.platform === "win32" ? "CONOUT$" : "/dev/tty";
842
+ let inputFd;
843
+ let outputFd;
844
+ try {
845
+ inputFd = openSync(inputPath, "r");
846
+ outputFd = openSync(outputPath, "w");
847
+ const input = new ReadStream(inputFd);
848
+ const output = new WriteStream(outputFd);
849
+ return {
850
+ input,
851
+ output,
852
+ close: () => {
853
+ input.destroy();
854
+ output.destroy();
855
+ }
856
+ };
857
+ } catch {
858
+ if (inputFd !== void 0) closeSync(inputFd);
859
+ if (outputFd !== void 0) closeSync(outputFd);
860
+ return void 0;
861
+ }
862
+ };
863
+ var resolveDashboardStreams = () => {
864
+ if (process.stdin.isTTY && process.stdout.isTTY) {
865
+ return { input: process.stdin, output: process.stdout };
866
+ }
867
+ if (process.stdin.isTTY && process.stderr.isTTY) {
868
+ return { input: process.stdin, output: process.stderr };
869
+ }
870
+ return openControllingTerminal();
871
+ };
872
+ var sanitize = (value) => Array.from(stripVTControlCharacters(value).replaceAll("\r", "")).filter((character) => {
873
+ const code = character.charCodeAt(0);
874
+ return code === 9 || code >= 32;
875
+ }).join("");
876
+ var truncate = (value, width) => {
877
+ if (width <= 0) return "";
878
+ if (value.length <= width) return value.padEnd(width);
879
+ if (width === 1) return value.slice(0, 1);
880
+ return `${value.slice(0, width - 1)}~`;
881
+ };
882
+ var formatDuration = (durationMs) => {
883
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1e3));
884
+ const hours = Math.floor(totalSeconds / 3600);
885
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
886
+ const seconds = totalSeconds % 60;
887
+ return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
888
+ };
889
+ var createDashboardState = (startedAt = Date.now()) => ({
890
+ activeSection: "activity",
891
+ activePane: "activity",
892
+ activity: [],
893
+ agentDebug: [],
894
+ connectorLogs: {},
895
+ connectors: [],
896
+ heartbeat: "Waiting",
897
+ relay: "Starting",
898
+ agent: "Starting",
899
+ server: "Unknown",
900
+ startedAt,
901
+ scroll: { activity: 0, agentDebug: 0 }
902
+ });
903
+ var classifyDashboardLine = (rawLine) => {
904
+ const line = sanitize(rawLine).trimEnd();
905
+ const prefix = "[buda-connector] ";
906
+ if (line.startsWith(prefix)) {
907
+ const message = line.slice(prefix.length);
908
+ const routineHealthCheck = /^relay request GET \/healthz\b/i.test(message);
909
+ return { pane: routineHealthCheck ? "agentDebug" : "activity", message };
910
+ }
911
+ return { pane: "agentDebug", message: line };
912
+ };
913
+ var classifyDashboardLevel = (fallback, formattedMessage, rawLine) => {
914
+ const normalizedLine = sanitize(rawLine).toLowerCase();
915
+ if (normalizedLine.includes("[error]") || normalizedLine.includes("[fatal]")) return "error";
916
+ if (normalizedLine.includes("[warning]") || normalizedLine.includes("[warn]")) return "warn";
917
+ const benignBunnyDiagnostic = [
918
+ "[bunny-agent:pi] getappendsystemprompt:",
919
+ "[bunny-agent:pi] loadskills",
920
+ "[bunny-agent:pi] resume:",
921
+ "[bunny-agent:pi] runner:"
922
+ ].some((prefix) => formattedMessage.toLowerCase().startsWith(prefix));
923
+ return benignBunnyDiagnostic ? "info" : fallback;
924
+ };
925
+ var updateStatusFromActivity = (state, message) => {
926
+ const normalized = message.toLowerCase();
927
+ if (normalized.startsWith("server ")) return { ...state, server: message.slice(7) };
928
+ if (normalized.startsWith("relay connected")) return { ...state, relay: "Connected" };
929
+ if (normalized.startsWith("relay reconnecting")) return { ...state, relay: "Retrying" };
930
+ if (normalized.startsWith("relay access revoked"))
931
+ return { ...state, relay: "Waiting for access" };
932
+ if (normalized.startsWith("relay disconnected") || normalized.startsWith("relay error") || normalized.startsWith("relay ping failed")) {
933
+ return { ...state, relay: "Disconnected" };
934
+ }
935
+ if (normalized.startsWith("bunny daemon listening") || normalized.startsWith("bunny daemon http")) {
936
+ return { ...state, agent: "Ready" };
937
+ }
938
+ if (normalized.startsWith("bunny daemon stopped")) return { ...state, agent: "Stopped" };
939
+ if (normalized.startsWith("heartbeat failed")) return { ...state, heartbeat: "Failed" };
940
+ return state;
941
+ };
942
+ var appendDashboardLine = (state, pane, level, message, time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })) => {
943
+ const current = state[pane];
944
+ const last = current.at(-1);
945
+ const nextLines = last?.message === message && last.level === level ? [...current.slice(0, -1), { ...last, count: last.count + 1, time }] : [...current, { count: 1, level, message, time }];
946
+ const trimmed = nextLines.slice(-MAX_LINES_PER_PANE);
947
+ const scroll = state.scroll[pane] > 0 ? { ...state.scroll, [pane]: Math.min(trimmed.length - 1, state.scroll[pane] + 1) } : state.scroll;
948
+ const next = { ...state, [pane]: trimmed, scroll };
949
+ return pane === "activity" ? updateStatusFromActivity(next, message) : next;
950
+ };
951
+ var scrollDashboardPane = (state, pane, delta) => {
952
+ const maxScroll = Math.max(0, state[pane].length - 1);
953
+ const nextScroll = delta === "follow" ? 0 : delta === "oldest" ? maxScroll : Math.max(0, Math.min(maxScroll, state.scroll[pane] + delta));
954
+ return { ...state, scroll: { ...state.scroll, [pane]: nextScroll } };
955
+ };
956
+ var selectedConnector = (state) => state.connectors.find((connector) => connector.connectorId === state.selectedConnectorId);
957
+ var applySelectedConnector = (state, resetScroll = true) => {
958
+ const connector = selectedConnector(state);
959
+ const logs = connector ? state.connectorLogs[connector.connectorId] : void 0;
960
+ return {
961
+ ...state,
962
+ activity: logs?.activity ?? [],
963
+ agent: connector?.agent ?? "Stopped",
964
+ agentDebug: logs?.agentDebug ?? [],
965
+ heartbeat: connector?.heartbeat ?? "Stopped",
966
+ relay: connector?.relay ?? "Disconnected",
967
+ server: connector?.serverUrl ?? "Unknown",
968
+ scroll: resetScroll ? { activity: 0, agentDebug: 0 } : state.scroll
969
+ };
970
+ };
971
+ var updateDashboardConnectors = (state, connectors, preferredConnectorId) => {
972
+ const selectedStillExists = connectors.some(
973
+ (connector) => connector.connectorId === state.selectedConnectorId
974
+ );
975
+ const selectedConnectorId = selectedStillExists ? state.selectedConnectorId : connectors.find((connector) => connector.connectorId === preferredConnectorId)?.connectorId ?? connectors[0]?.connectorId;
976
+ return applySelectedConnector(
977
+ {
978
+ ...state,
979
+ activeSection: connectors.length > 0 && state.connectors.length === 0 ? "connectors" : state.activeSection,
980
+ connectors,
981
+ selectedConnectorId
982
+ },
983
+ selectedConnectorId !== state.selectedConnectorId
984
+ );
985
+ };
986
+ var selectConnectorByOffset = (state, offset) => {
987
+ if (state.connectors.length === 0) return state;
988
+ const currentIndex = Math.max(
989
+ 0,
990
+ state.connectors.findIndex((connector) => connector.connectorId === state.selectedConnectorId)
991
+ );
992
+ const nextIndex = Math.max(0, Math.min(state.connectors.length - 1, currentIndex + offset));
993
+ return applySelectedConnector({
994
+ ...state,
995
+ selectedConnectorId: state.connectors[nextIndex]?.connectorId
996
+ });
997
+ };
998
+ var promptOptionCount = (prompt) => prompt.kind === "exit-choice" ? 3 : 2;
999
+ var isUpKey = (key) => ["\x1B[A", "\x1BOA", "k", "K"].includes(key);
1000
+ var isDownKey = (key) => ["\x1B[B", "\x1BOB", "j", "J"].includes(key);
1001
+ var getConnectorSectionHeight = (state, height) => state.connectors.length ? Math.min(
1002
+ Math.max(4, state.connectors.length + 2),
1003
+ Math.max(4, Math.min(Math.floor(height / 3), height - 13))
1004
+ ) : 0;
1005
+ var parseDashboardMouseWheel = (input, state, size) => {
1006
+ const event = input.split("\x1B[<").at(-1);
1007
+ const match = event?.match(/^(64|65);\d+;(\d+)[Mm]/);
1008
+ if (!match) return void 0;
1009
+ const row = Number(match[2]);
1010
+ const connectorHeight = getConnectorSectionHeight(state, size.height);
1011
+ const availablePaneRows = size.height - 7 - connectorHeight;
1012
+ const activityHeight = Math.floor(availablePaneRows / 2);
1013
+ const activityStartRow = 7 + connectorHeight;
1014
+ const debugStartRow = activityStartRow + activityHeight;
1015
+ const pane = row >= activityStartRow && row < debugStartRow ? "activity" : row >= debugStartRow && row < size.height ? "agentDebug" : void 0;
1016
+ if (!pane) return void 0;
1017
+ return { delta: match[1] === "64" ? 3 : -3, pane };
1018
+ };
1019
+ var reduceDashboardInput = (state, key, managerEnabled) => {
1020
+ if (state.prompt) {
1021
+ if (key === "\x1B") return { state: { ...state, prompt: void 0 } };
1022
+ if (isUpKey(key) || isDownKey(key)) {
1023
+ const delta = isUpKey(key) ? -1 : 1;
1024
+ const optionCount = promptOptionCount(state.prompt);
1025
+ const option = (state.prompt.option + delta + optionCount) % optionCount;
1026
+ return { state: { ...state, prompt: { ...state.prompt, option } } };
1027
+ }
1028
+ if (key !== "\r" && key !== "\n") return { state };
1029
+ if (state.prompt.kind === "stop-selected") {
1030
+ const connector = selectedConnector(state);
1031
+ return state.prompt.option === 1 && connector ? {
1032
+ command: {
1033
+ connectorId: connector.connectorId,
1034
+ serverUrl: connector.serverUrl,
1035
+ type: "stop-selected"
1036
+ },
1037
+ state: { ...state, prompt: void 0 }
1038
+ } : { state: { ...state, prompt: void 0 } };
1039
+ }
1040
+ if (state.prompt.kind === "stop-all-confirm") {
1041
+ return state.prompt.option === 1 ? { command: { type: "stop-all" }, state: { ...state, prompt: void 0 } } : { state: { ...state, prompt: void 0 } };
1042
+ }
1043
+ if (state.prompt.option === 0) {
1044
+ return { state: { ...state, prompt: { kind: "stop-all-confirm", option: 0 } } };
1045
+ }
1046
+ if (state.prompt.option === 1) {
1047
+ return { command: { type: "exit-dashboard" }, state: { ...state, prompt: void 0 } };
1048
+ }
1049
+ return { state: { ...state, prompt: void 0 } };
1050
+ }
1051
+ if (managerEnabled && key === "") {
1052
+ return { state: { ...state, prompt: { kind: "exit-choice", option: 1 } } };
1053
+ }
1054
+ if (managerEnabled && key === "\x1B") {
1055
+ return { command: { type: "exit-dashboard" }, state };
1056
+ }
1057
+ if (managerEnabled && (key === "s" || key === "S") && ["Online", "Running"].includes(selectedConnector(state)?.status ?? "")) {
1058
+ return { state: { ...state, prompt: { kind: "stop-selected", option: 0 } } };
1059
+ }
1060
+ if (key === " ") {
1061
+ const sections = managerEnabled ? ["connectors", "activity", "agentDebug"] : ["activity", "agentDebug"];
1062
+ const index = sections.indexOf(state.activeSection);
1063
+ const activeSection = sections[(index + 1) % sections.length] ?? sections[0];
1064
+ return {
1065
+ state: {
1066
+ ...state,
1067
+ activePane: activeSection === "connectors" ? state.activePane : activeSection,
1068
+ activeSection
1069
+ }
1070
+ };
1071
+ }
1072
+ if (state.activeSection === "connectors" && (isUpKey(key) || isDownKey(key))) {
1073
+ return { state: selectConnectorByOffset(state, isUpKey(key) ? -1 : 1) };
1074
+ }
1075
+ const pane = state.activePane;
1076
+ if (key === "f" || key === "F" || key === "\x1B[F") {
1077
+ return { state: scrollDashboardPane(state, pane, "follow") };
1078
+ }
1079
+ if (key === "\x1B[H") return { state: scrollDashboardPane(state, pane, "oldest") };
1080
+ if (isUpKey(key)) return { state: scrollDashboardPane(state, pane, 1) };
1081
+ if (isDownKey(key)) return { state: scrollDashboardPane(state, pane, -1) };
1082
+ if (key === "\x1B[5~") return { state: scrollDashboardPane(state, pane, 8) };
1083
+ if (key === "\x1B[6~") return { state: scrollDashboardPane(state, pane, -8) };
1084
+ return { state };
1085
+ };
1086
+ var renderStatus = (state, width, now) => {
1087
+ const innerWidth = width - 2;
1088
+ const status = (value) => value;
1089
+ const connector = selectedConnector(state);
1090
+ return [
1091
+ `+${"-".repeat(innerWidth)}+`,
1092
+ `|${colors.cyan}${truncate(` ${DASHBOARD_TITLE}`, innerWidth)}${colors.reset}|`,
1093
+ `|${truncate(` Connector ${connector?.label ?? connector?.connectorId ?? "None"} Space ${connector?.spaceId ?? "Unknown"}`, innerWidth)}|`,
1094
+ `|${truncate(` Relay ${status(state.relay)} Agent ${status(state.agent)} Heartbeat ${status(state.heartbeat)}`, innerWidth)}|`,
1095
+ `|${truncate(` Server ${state.server} Uptime ${formatDuration(now - state.startedAt)}`, innerWidth)}|`,
1096
+ `+${"-".repeat(innerWidth)}+`
1097
+ ];
1098
+ };
1099
+ var renderConnectors = (state, height, width) => {
1100
+ const innerWidth = width - 2;
1101
+ const contentHeight = Math.max(1, height - 2);
1102
+ const selectedIndex = Math.max(
1103
+ 0,
1104
+ state.connectors.findIndex((connector) => connector.connectorId === state.selectedConnectorId)
1105
+ );
1106
+ const start = Math.max(
1107
+ 0,
1108
+ Math.min(
1109
+ selectedIndex - Math.floor(contentHeight / 2),
1110
+ state.connectors.length - contentHeight
1111
+ )
1112
+ );
1113
+ const visible = state.connectors.slice(start, start + contentHeight);
1114
+ const range = state.connectors.length ? `${start + 1}-${start + visible.length}/${state.connectors.length}` : "0/0";
1115
+ const title = ` Connectors ${state.activeSection === "connectors" ? "[active]" : ""}`;
1116
+ const headerFill = Math.max(1, innerWidth - title.length - range.length);
1117
+ const output = [
1118
+ `${state.activeSection === "connectors" ? colors.reverse : ""}+${title}${"-".repeat(headerFill)}${range}+${colors.reset}`
1119
+ ];
1120
+ for (let index = 0; index < contentHeight; index += 1) {
1121
+ const connector = visible[index];
1122
+ if (!connector) {
1123
+ output.push(`|${" ".repeat(innerWidth)}|`);
1124
+ continue;
1125
+ }
1126
+ const marker = connector.connectorId === state.selectedConnectorId ? ">" : " ";
1127
+ const statusText = connector.status.padEnd(8);
1128
+ const availableFieldWidth = Math.max(24, innerWidth - 18);
1129
+ const connectorWidth = Math.max(10, Math.floor(availableFieldWidth * 0.4));
1130
+ const spaceWidth = Math.max(8, Math.floor(availableFieldWidth * 0.25));
1131
+ const serverWidth = Math.max(8, availableFieldWidth - connectorWidth - spaceWidth);
1132
+ const content = `${marker} ${statusText} ${truncate(connector.label ?? connector.connectorId, connectorWidth)} | ${truncate(connector.spaceId, spaceWidth)} | ${truncate(connector.serverUrl, serverWidth)}`;
1133
+ const color = connector.status === "Stopped" ? colors.dim : connector.status === "Online" ? colors.cyan : connector.status === "Connecting" || connector.status === "Stopping" ? colors.yellow : connector.status === "Error" || connector.status === "Unresponsive" ? colors.red : "";
1134
+ output.push(`|${color}${truncate(content, innerWidth)}${colors.reset}|`);
1135
+ }
1136
+ output.push(`+${"-".repeat(innerWidth)}+`);
1137
+ return output;
1138
+ };
1139
+ var renderPane = (title, lines, height, width, active, scroll) => {
1140
+ const innerWidth = width - 2;
1141
+ const contentHeight = Math.max(1, height - 2);
1142
+ const end = Math.max(0, lines.length - scroll);
1143
+ const visible = lines.slice(Math.max(0, end - contentHeight), end);
1144
+ const headerSuffix = scroll === 0 ? "following" : `${scroll} lines behind`;
1145
+ const titleText = ` ${title} ${active ? "[active]" : ""}`;
1146
+ const headerFill = Math.max(1, innerWidth - titleText.length - headerSuffix.length);
1147
+ const output = [
1148
+ `${active ? colors.reverse : ""}+${titleText}${"-".repeat(headerFill)}${headerSuffix}+${colors.reset}`
1149
+ ];
1150
+ for (let index = 0; index < contentHeight; index += 1) {
1151
+ const line = visible[index];
1152
+ if (!line) {
1153
+ output.push(`|${" ".repeat(innerWidth)}|`);
1154
+ continue;
1155
+ }
1156
+ const levelColor = line.level === "error" ? colors.red : line.level === "warn" ? colors.yellow : colors.dim;
1157
+ const count = line.count > 1 ? ` x${line.count}` : "";
1158
+ const content = `${line.time} ${line.level.toUpperCase().padEnd(5)} ${line.message}${count}`;
1159
+ output.push(`|${levelColor}${truncate(content, innerWidth)}${colors.reset}|`);
1160
+ }
1161
+ output.push(`+${"-".repeat(innerWidth)}+`);
1162
+ return output;
1163
+ };
1164
+ var renderFooter = (state, width) => {
1165
+ const managerKeys = state.connectors.length ? "Up/Down select | S stop | Esc exit | " : "";
1166
+ return `${colors.dim}${truncate(` ${managerKeys}Tab switch pane | F follow`, width)}${colors.reset}`;
1167
+ };
1168
+ var renderPrompt = (state, size) => {
1169
+ const width = Math.max(10, size.width);
1170
+ const height = Math.max(4, size.height);
1171
+ const connector = selectedConnector(state);
1172
+ const prompt = state.prompt;
1173
+ if (!prompt) return "";
1174
+ const content = prompt.kind === "exit-choice" ? {
1175
+ options: ["Stop all local Connectors and exit", "Exit Dashboard only", "Cancel"],
1176
+ title: "Exit Buda Connector Dashboard?",
1177
+ warning: "Dashboard exit does not stop background Connector processes."
1178
+ } : prompt.kind === "stop-all-confirm" ? {
1179
+ options: ["Cancel", "Stop all local Connectors and exit"],
1180
+ title: "Stop every local Connector?",
1181
+ warning: "Running agent tasks will be interrupted."
1182
+ } : {
1183
+ options: ["Cancel", "Stop selected Connector"],
1184
+ title: `Stop ${connector?.connectorId ?? "selected Connector"}?`,
1185
+ warning: "The Dashboard will remain open."
1186
+ };
1187
+ const lines = [
1188
+ content.title,
1189
+ "",
1190
+ content.warning,
1191
+ "",
1192
+ ...content.options.map((option, index) => `${index === prompt.option ? ">" : " "} ${option}`),
1193
+ "",
1194
+ "Up/Down choose | Enter confirm | Esc cancel"
1195
+ ];
1196
+ const visible = lines.slice(0, height);
1197
+ while (visible.length < height) visible.push("");
1198
+ return visible.map((line) => truncate(line, width)).join("\n");
1199
+ };
1200
+ var renderDashboard = (state, size, now = Date.now()) => {
1201
+ const width = Math.max(10, size.width);
1202
+ const height = Math.max(4, size.height);
1203
+ if (width < MIN_DASHBOARD_WIDTH || height < MIN_DASHBOARD_HEIGHT) {
1204
+ return [
1205
+ truncate(DASHBOARD_TITLE, width),
1206
+ truncate("Terminal too small for dashboard. Resize or restart with --plain.", width),
1207
+ truncate(`Relay ${state.relay} | Agent ${state.agent}`, width),
1208
+ truncate(`Heartbeat ${state.heartbeat}`, width)
1209
+ ].slice(0, height).join("\n");
1210
+ }
1211
+ if (state.prompt) return renderPrompt(state, size);
1212
+ const connectorHeight = getConnectorSectionHeight(state, height);
1213
+ const availablePaneRows = height - 7 - connectorHeight;
1214
+ const activityHeight = Math.floor(availablePaneRows / 2);
1215
+ const debugHeight = availablePaneRows - activityHeight;
1216
+ return [
1217
+ ...renderStatus(state, width, now),
1218
+ ...connectorHeight ? renderConnectors(state, connectorHeight, width) : [],
1219
+ ...renderPane(
1220
+ "Activity",
1221
+ state.activity,
1222
+ activityHeight,
1223
+ width,
1224
+ state.activeSection === "activity",
1225
+ state.scroll.activity
1226
+ ),
1227
+ ...renderPane(
1228
+ "Agent Debug",
1229
+ state.agentDebug,
1230
+ debugHeight,
1231
+ width,
1232
+ state.activeSection === "agentDebug",
1233
+ state.scroll.agentDebug
1234
+ ),
1235
+ renderFooter(state, width)
1236
+ ].join("\n");
1237
+ };
1238
+ var ConnectorConsoleDashboard = class {
1239
+ constructor(streams, manager) {
1240
+ this.streams = streams;
1241
+ this.manager = manager;
1242
+ }
1243
+ state = createDashboardState();
1244
+ stopped = false;
1245
+ renderQueued = false;
1246
+ originalConsole = {
1247
+ error: console.error,
1248
+ log: console.log,
1249
+ warn: console.warn
1250
+ };
1251
+ start() {
1252
+ console.log = (...args) => this.capture("info", args);
1253
+ console.warn = (...args) => this.capture("warn", args);
1254
+ console.error = (...args) => this.capture("error", args);
1255
+ this.streams.output.write("\x1B[?1049h\x1B[?25l\x1B[?1000h\x1B[?1006h");
1256
+ if (this.streams.input.isTTY) this.streams.input.setRawMode(true);
1257
+ this.streams.input.resume();
1258
+ this.streams.input.on("data", this.handleInput);
1259
+ this.streams.output.on("resize", this.scheduleRender);
1260
+ process.once("exit", this.stop);
1261
+ this.scheduleRender();
1262
+ }
1263
+ updateStatus(update) {
1264
+ this.state = { ...this.state, ...update };
1265
+ this.scheduleRender();
1266
+ }
1267
+ updateConnectors(connectors, preferredConnectorId) {
1268
+ this.state = updateDashboardConnectors(this.state, connectors, preferredConnectorId);
1269
+ this.scheduleRender();
1270
+ }
1271
+ appendConnectorLog(connectorId, level, raw) {
1272
+ const current = this.state.connectorLogs[connectorId] ?? { activity: [], agentDebug: [] };
1273
+ let connectorState = { ...this.state, ...current };
1274
+ const formatted = raw;
1275
+ for (const rawLine of formatted.split("\n")) {
1276
+ const { pane, message } = classifyDashboardLine(rawLine);
1277
+ if (!message) continue;
1278
+ connectorState = appendDashboardLine(
1279
+ connectorState,
1280
+ pane,
1281
+ classifyDashboardLevel(level, formatted, rawLine),
1282
+ message
1283
+ );
1284
+ }
1285
+ const connectorLogs = {
1286
+ ...this.state.connectorLogs,
1287
+ [connectorId]: {
1288
+ activity: connectorState.activity,
1289
+ agentDebug: connectorState.agentDebug
1290
+ }
1291
+ };
1292
+ this.state = {
1293
+ ...this.state,
1294
+ connectorLogs,
1295
+ ...connectorId === this.state.selectedConnectorId ? { activity: connectorState.activity, agentDebug: connectorState.agentDebug } : {}
1296
+ };
1297
+ this.scheduleRender();
1298
+ }
1299
+ stop = () => {
1300
+ if (this.stopped) return;
1301
+ this.stopped = true;
1302
+ console.log = this.originalConsole.log;
1303
+ console.warn = this.originalConsole.warn;
1304
+ console.error = this.originalConsole.error;
1305
+ this.streams.input.off("data", this.handleInput);
1306
+ this.streams.output.off("resize", this.scheduleRender);
1307
+ process.off("exit", this.stop);
1308
+ if (this.streams.input.isTTY) this.streams.input.setRawMode(false);
1309
+ this.streams.input.pause();
1310
+ this.streams.output.write("\x1B[?1006l\x1B[?1000l\x1B[?25h\x1B[?1049l");
1311
+ this.streams.close?.();
1312
+ if (activeDashboard === this) activeDashboard = void 0;
1313
+ };
1314
+ capture(level, args) {
1315
+ const formatted = format(...args);
1316
+ for (const rawLine of formatted.split("\n")) {
1317
+ const { pane, message } = classifyDashboardLine(rawLine);
1318
+ if (!message) continue;
1319
+ this.state = appendDashboardLine(
1320
+ this.state,
1321
+ pane,
1322
+ classifyDashboardLevel(level, formatted, rawLine),
1323
+ message
1324
+ );
1325
+ }
1326
+ this.scheduleRender();
1327
+ }
1328
+ handleInput = (chunk) => {
1329
+ const key = chunk.toString();
1330
+ if (key === "" && !this.manager) {
1331
+ this.stop();
1332
+ process.kill(process.pid, "SIGINT");
1333
+ return;
1334
+ }
1335
+ const mouseWheel = parseDashboardMouseWheel(key, this.state, {
1336
+ height: this.streams.output.rows || 24,
1337
+ width: this.streams.output.columns || 80
1338
+ });
1339
+ if (mouseWheel) {
1340
+ this.state = scrollDashboardPane(
1341
+ {
1342
+ ...this.state,
1343
+ activePane: mouseWheel.pane,
1344
+ activeSection: mouseWheel.pane
1345
+ },
1346
+ mouseWheel.pane,
1347
+ mouseWheel.delta
1348
+ );
1349
+ this.scheduleRender();
1350
+ return;
1351
+ }
1352
+ const result = reduceDashboardInput(this.state, key, Boolean(this.manager));
1353
+ this.state = result.state;
1354
+ this.scheduleRender();
1355
+ if (result.command) void this.runCommand(result.command);
1356
+ };
1357
+ async runCommand(command) {
1358
+ if (!this.manager) return;
1359
+ if (command.type === "exit-dashboard") {
1360
+ this.stop();
1361
+ this.manager.onExit();
1362
+ return;
1363
+ }
1364
+ try {
1365
+ if (command.type === "stop-selected") {
1366
+ this.state = {
1367
+ ...this.state,
1368
+ connectors: this.state.connectors.map(
1369
+ (connector) => connector.connectorId === command.connectorId ? { ...connector, status: "Stopping" } : connector
1370
+ )
1371
+ };
1372
+ this.scheduleRender();
1373
+ await this.manager.onStopConnector(command.connectorId, command.serverUrl);
1374
+ return;
1375
+ }
1376
+ await this.manager.onStopAll();
1377
+ this.stop();
1378
+ this.manager.onExit();
1379
+ } catch (error) {
1380
+ this.capture("error", [error instanceof Error ? error.message : String(error)]);
1381
+ }
1382
+ }
1383
+ scheduleRender = () => {
1384
+ if (this.stopped || this.renderQueued) return;
1385
+ this.renderQueued = true;
1386
+ setImmediate(() => {
1387
+ this.renderQueued = false;
1388
+ if (this.stopped) return;
1389
+ const width = this.streams.output.columns || 80;
1390
+ const height = this.streams.output.rows || 24;
1391
+ this.streams.output.write(`\x1B[H\x1B[2J${renderDashboard(this.state, { width, height })}`);
1392
+ });
1393
+ };
1394
+ };
1395
+ var activeDashboard;
1396
+ var startConnectorConsoleDashboard = (options = {}) => {
1397
+ if (options.plain) return void 0;
1398
+ const streams = options.streams ?? resolveDashboardStreams();
1399
+ if (!streams?.input.isTTY || !streams.output.isTTY) return void 0;
1400
+ if (activeDashboard) return activeDashboard;
1401
+ activeDashboard = new ConnectorConsoleDashboard(streams, options.manager);
1402
+ activeDashboard.start();
1403
+ return activeDashboard;
1404
+ };
1405
+ var updateConnectorDashboardStatus = (update) => {
1406
+ activeDashboard?.updateStatus(update);
1407
+ };
1408
+ var updateConnectorDashboardConnectors = (connectors, preferredConnectorId) => {
1409
+ activeDashboard?.updateConnectors(connectors, preferredConnectorId);
1410
+ };
1411
+ var appendConnectorDashboardLog = (connectorId, level, raw) => {
1412
+ activeDashboard?.appendConnectorLog(connectorId, level, raw);
1413
+ };
1414
+
1415
+ // src/daemon-client.ts
1416
+ import { spawn } from "child_process";
1417
+ import { mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
1418
+ import path7 from "path";
1419
+
1420
+ // src/cloud-client.ts
1421
+ import { createHash as createHash4 } from "crypto";
1422
+ import { platform as platform3, release } from "os";
1423
+
1424
+ // src/browser.ts
1425
+ import { accessSync, constants, statSync } from "fs";
1426
+ import { mkdir as mkdir4 } from "fs/promises";
1427
+ import { platform as getPlatform, homedir as homedir2 } from "os";
1428
+ import path4 from "path";
410
1429
 
411
1430
  // src/logger.ts
412
- import { appendFile, mkdir as mkdir2 } from "fs/promises";
1431
+ import { appendFile, mkdir as mkdir3, readdir as readdir2, rename as rename3, stat as stat2, unlink as unlink4 } from "fs/promises";
1432
+ import path3 from "path";
1433
+ import { format as format2 } from "util";
1434
+ var CONNECTOR_LOG_MAX_BYTES = 2 * 1024 * 1024;
1435
+ var CONNECTOR_LOG_BACKUP_COUNT = 2;
1436
+ var CONNECTOR_LOG_RETENTION_DAYS = 7;
413
1437
  var pendingLogs = [];
1438
+ var fileWriteQueue = Promise.resolve();
1439
+ var lastPrunedDate = "";
1440
+ var activeLogContext;
1441
+ var setConnectorLogContext = (context) => {
1442
+ activeLogContext = { ...activeLogContext, ...context };
1443
+ };
1444
+ var ignoreMissingFile = (error) => {
1445
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
1446
+ return;
1447
+ }
1448
+ throw error;
1449
+ };
1450
+ var formatConnectorLogDate = (date) => {
1451
+ const year = date.getFullYear();
1452
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1453
+ const day = String(date.getDate()).padStart(2, "0");
1454
+ return `${year}-${month}-${day}`;
1455
+ };
1456
+ var getDatedConnectorLogPath = (baseLogPath, date = /* @__PURE__ */ new Date()) => {
1457
+ const parsed = path3.parse(baseLogPath);
1458
+ return path3.join(parsed.dir, `${parsed.name}-${formatConnectorLogDate(date)}${parsed.ext}`);
1459
+ };
1460
+ async function pruneConnectorLogs(baseLogPath, now = /* @__PURE__ */ new Date(), retentionDays = CONNECTOR_LOG_RETENTION_DAYS) {
1461
+ const parsed = path3.parse(baseLogPath);
1462
+ const prefix = `${parsed.name}-`;
1463
+ const cutoff = new Date(now);
1464
+ cutoff.setHours(0, 0, 0, 0);
1465
+ cutoff.setDate(cutoff.getDate() - Math.max(0, retentionDays - 1));
1466
+ const cutoffDate = formatConnectorLogDate(cutoff);
1467
+ const files = await readdir2(parsed.dir).catch(() => []);
1468
+ const expired = files.filter((fileName) => {
1469
+ if (!fileName.startsWith(prefix)) return false;
1470
+ const date = fileName.slice(prefix.length, prefix.length + 10);
1471
+ const suffix = fileName.slice(prefix.length + 10);
1472
+ const isDatedLog = /^\d{4}-\d{2}-\d{2}$/.test(date);
1473
+ const segment = suffix.startsWith(`${parsed.ext}.`) ? suffix.slice(parsed.ext.length + 1) : "";
1474
+ const isLogSegment = suffix === parsed.ext || /^\d+$/.test(segment);
1475
+ return isDatedLog && isLogSegment && date < cutoffDate;
1476
+ });
1477
+ await Promise.all(expired.map((fileName) => unlink4(path3.join(parsed.dir, fileName))));
1478
+ return expired.length;
1479
+ }
1480
+ async function rotateConnectorLogIfNeeded(logPath, maxBytes = CONNECTOR_LOG_MAX_BYTES, backupCount = CONNECTOR_LOG_BACKUP_COUNT) {
1481
+ const currentSize = await stat2(logPath).then((value) => value.size).catch((error) => {
1482
+ ignoreMissingFile(error);
1483
+ return 0;
1484
+ });
1485
+ if (currentSize < maxBytes) return false;
1486
+ await unlink4(`${logPath}.${backupCount}`).catch(ignoreMissingFile);
1487
+ for (let index = backupCount - 1; index >= 1; index -= 1) {
1488
+ await rename3(`${logPath}.${index}`, `${logPath}.${index + 1}`).catch(ignoreMissingFile);
1489
+ }
1490
+ await rename3(logPath, `${logPath}.1`).catch(ignoreMissingFile);
1491
+ return true;
1492
+ }
1493
+ var appendConnectorLog = (baseLogPath, line, now = /* @__PURE__ */ new Date()) => {
1494
+ const date = formatConnectorLogDate(now);
1495
+ const logPath = getDatedConnectorLogPath(baseLogPath, now);
1496
+ const write = async () => {
1497
+ if (lastPrunedDate !== date) {
1498
+ await pruneConnectorLogs(baseLogPath, now).catch(() => void 0);
1499
+ lastPrunedDate = date;
1500
+ }
1501
+ await rotateConnectorLogIfNeeded(logPath);
1502
+ await appendFile(logPath, line);
1503
+ };
1504
+ const queuedWrite = fileWriteQueue.then(write, write);
1505
+ fileWriteQueue = queuedWrite.catch(() => void 0);
1506
+ return queuedWrite;
1507
+ };
1508
+ var persistLocalEntry = async (entry) => {
1509
+ const paths = getConnectorPaths(
1510
+ activeLogContext?.serverUrl ?? getServerUrl(),
1511
+ activeLogContext?.configRoot
1512
+ );
1513
+ await mkdir3(path3.dirname(paths.logPath), { recursive: true }).catch(() => void 0);
1514
+ await appendConnectorLog(paths.logPath, `${JSON.stringify(entry)}
1515
+ `).catch(() => void 0);
1516
+ };
1517
+ var installConnectorConsoleCapture = () => {
1518
+ const writes = [];
1519
+ const original = {
1520
+ error: console.error,
1521
+ log: console.log,
1522
+ warn: console.warn
1523
+ };
1524
+ const capture = (level, output) => (...args) => {
1525
+ const message = format2(...args);
1526
+ if (!message.startsWith("[buda-connector]")) {
1527
+ writes.push(
1528
+ persistLocalEntry({
1529
+ level,
1530
+ message,
1531
+ metadata: {
1532
+ source: "daemon-console",
1533
+ ...activeLogContext?.connectorId ? { connectorId: activeLogContext.connectorId } : {}
1534
+ },
1535
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1536
+ })
1537
+ );
1538
+ }
1539
+ output(...args);
1540
+ };
1541
+ console.log = capture("info", original.log);
1542
+ console.warn = capture("warn", original.warn);
1543
+ console.error = capture("error", original.error);
1544
+ return async () => {
1545
+ console.log = original.log;
1546
+ console.warn = original.warn;
1547
+ console.error = original.error;
1548
+ await Promise.all(writes);
1549
+ };
1550
+ };
414
1551
  async function writeLog(level, message, metadata) {
415
1552
  const entry = {
416
1553
  level,
417
1554
  message,
418
- metadata,
1555
+ metadata: {
1556
+ ...metadata ?? {},
1557
+ ...activeLogContext?.connectorId ? { connectorId: activeLogContext.connectorId } : {}
1558
+ },
419
1559
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
420
1560
  };
421
1561
  pendingLogs.push(entry);
422
1562
  if (pendingLogs.length > 100) pendingLogs.splice(0, pendingLogs.length - 100);
423
- const paths = getConnectorPaths(getServerUrl());
424
- await mkdir2(paths.configDir, { recursive: true }).catch(() => void 0);
425
- await appendFile(paths.logPath, `${JSON.stringify(entry)}
426
- `).catch(() => void 0);
1563
+ await persistLocalEntry(entry);
427
1564
  const line = `[buda-connector] ${message}`;
428
1565
  if (level === "error") console.error(line);
429
1566
  else if (level === "warn") console.warn(line);
@@ -434,58 +1571,9 @@ function takePendingLogs() {
434
1571
  }
435
1572
  function restorePendingLogs(logs) {
436
1573
  pendingLogs.unshift(...logs);
437
- }
438
-
439
- // src/bunny-daemon.ts
440
- async function startEmbeddedBunnyDaemon(config, launchConfig) {
441
- const host = getBunnyDaemonHost(launchConfig);
442
- const port = getBunnyDaemonPort(launchConfig);
443
- const root = config.workdirRoot;
444
- await mkdir3(root, { recursive: true });
445
- const server = createDaemon({ host, port, root });
446
- await new Promise((resolve, reject) => {
447
- const onError = (error) => {
448
- server.off("listening", onListening);
449
- reject(error);
450
- };
451
- const onListening = () => {
452
- server.off("error", onError);
453
- resolve();
454
- };
455
- server.once("error", onError);
456
- server.once("listening", onListening);
457
- server.listen(port, host);
458
- });
459
- const address = server.address();
460
- const actualPort = typeof address === "object" && address ? address.port : port;
461
- const baseUrl = `http://${host}:${actualPort}`;
462
- await writeLog("info", `bunny daemon listening ${baseUrl}`, {
463
- root
464
- });
465
- return {
466
- server,
467
- host,
468
- port: actualPort,
469
- baseUrl,
470
- root
471
- };
472
- }
473
- async function stopEmbeddedBunnyDaemon(daemon) {
474
- await new Promise((resolve) => {
475
- daemon.server.close(() => resolve());
476
- });
477
- await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
478
- }
479
-
480
- // src/cloud-client.ts
481
- import { createHash as createHash4 } from "crypto";
482
- import { platform as platform3, release } from "os";
1574
+ }
483
1575
 
484
1576
  // src/browser.ts
485
- import { accessSync, constants, statSync } from "fs";
486
- import { mkdir as mkdir4 } from "fs/promises";
487
- import { platform as getPlatform, homedir as homedir2 } from "os";
488
- import path2 from "path";
489
1577
  var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
490
1578
  var MAX_SCREENCAST_DIMENSION = 2560;
491
1579
  var SESSION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,320}$/;
@@ -791,15 +1879,15 @@ var resolveScreenshotPath = (workdirRoot, filePath) => {
791
1879
  if (!workdirRoot) throw new Error("Connector workdir is unavailable for screenshots.");
792
1880
  const raw = typeof filePath === "string" ? filePath.trim() : "";
793
1881
  if (!raw) throw new Error("file_path is required");
794
- const agentRoot = path2.resolve(workdirRoot, "agent");
1882
+ const agentRoot = path4.resolve(workdirRoot, "agent");
795
1883
  const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
796
- const absolutePath = path2.resolve(agentRoot, relativeInput);
797
- if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path2.sep}`)) {
1884
+ const absolutePath = path4.resolve(agentRoot, relativeInput);
1885
+ if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path4.sep}`)) {
798
1886
  throw new Error("file_path must stay under the connector agent drive");
799
1887
  }
800
1888
  return {
801
1889
  absolutePath,
802
- relativePath: path2.relative(agentRoot, absolutePath).split(path2.sep).join("/")
1890
+ relativePath: path4.relative(agentRoot, absolutePath).split(path4.sep).join("/")
803
1891
  };
804
1892
  };
805
1893
  async function handleBrowserRequest(requestPath, method, body, context = {}) {
@@ -915,7 +2003,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
915
2003
  const input = body ?? {};
916
2004
  const s = await ensureSession(sessionId);
917
2005
  const screenshotPath = resolveScreenshotPath(context.workdirRoot, input.file_path);
918
- await mkdir4(path2.dirname(screenshotPath.absolutePath), { recursive: true });
2006
+ await mkdir4(path4.dirname(screenshotPath.absolutePath), { recursive: true });
919
2007
  await s.page.screenshot({
920
2008
  path: screenshotPath.absolutePath,
921
2009
  fullPage: input.fullPage !== false
@@ -998,30 +2086,61 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
998
2086
 
999
2087
  // src/bundled.ts
1000
2088
  import { createHash as createHash3 } from "crypto";
1001
- import { cpSync, existsSync, rmSync } from "fs";
1002
- import { readdir, readFile as readFile3 } from "fs/promises";
1003
- import path3 from "path";
2089
+ import {
2090
+ copyFileSync,
2091
+ existsSync,
2092
+ mkdirSync,
2093
+ readdirSync,
2094
+ renameSync,
2095
+ rmSync,
2096
+ statSync as statSync2
2097
+ } from "fs";
2098
+ import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
2099
+ import path5 from "path";
1004
2100
  import { fileURLToPath } from "url";
1005
- var PKG_ROOT = path3.resolve(fileURLToPath(import.meta.url), "../..");
1006
- var BUNDLED_DIR = path3.join(PKG_ROOT, "agent-templates");
1007
- var SYSTEM_SKILLS_DIR = path3.join(BUNDLED_DIR, "system-skills");
1008
- var SCRIPTS_DIR = path3.join(BUNDLED_DIR, "scripts");
1009
- var SHARED_DIR = path3.join(BUNDLED_DIR, "shared");
1010
- var MONOREPO_SRC = path3.resolve(PKG_ROOT, "../buda/agent-templates");
1011
- function syncTemplatesFromMonorepo() {
2101
+ var PKG_ROOT = path5.resolve(fileURLToPath(import.meta.url), "../..");
2102
+ var BUNDLED_DIR = path5.join(PKG_ROOT, "agent-templates");
2103
+ var SYSTEM_SKILLS_DIR = path5.join(BUNDLED_DIR, "system-skills");
2104
+ var SCRIPTS_DIR = path5.join(BUNDLED_DIR, "scripts");
2105
+ var SHARED_DIR = path5.join(BUNDLED_DIR, "shared");
2106
+ var MONOREPO_SRC = path5.resolve(PKG_ROOT, "../buda/agent-templates");
2107
+ function syncTemplatesFromMonorepo(options = {}) {
1012
2108
  if (!existsSync(MONOREPO_SRC)) {
1013
2109
  throw new Error(`Cannot sync agent-templates: source missing at ${MONOREPO_SRC}`);
1014
2110
  }
1015
- rmSync(BUNDLED_DIR, { recursive: true, force: true });
1016
- cpSync(MONOREPO_SRC, BUNDLED_DIR, {
1017
- recursive: true,
1018
- dereference: true,
1019
- filter: (src) => !src.split(path3.sep).includes("extensions")
1020
- });
2111
+ mkdirSync(BUNDLED_DIR, { recursive: true });
2112
+ syncDirectory(MONOREPO_SRC, BUNDLED_DIR, Boolean(options.prune));
2113
+ }
2114
+ function syncDirectory(sourceDirectory, destinationDirectory, prune) {
2115
+ const sourceEntries = /* @__PURE__ */ new Set();
2116
+ for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) {
2117
+ if (entry.name === "extensions") continue;
2118
+ sourceEntries.add(entry.name);
2119
+ const source = path5.join(sourceDirectory, entry.name);
2120
+ const destination = path5.join(destinationDirectory, entry.name);
2121
+ if (statSync2(source).isDirectory()) {
2122
+ mkdirSync(destination, { recursive: true });
2123
+ syncDirectory(source, destination, prune);
2124
+ continue;
2125
+ }
2126
+ const temporary = `${destination}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
2127
+ copyFileSync(source, temporary);
2128
+ try {
2129
+ renameSync(temporary, destination);
2130
+ } catch {
2131
+ copyFileSync(temporary, destination);
2132
+ rmSync(temporary, { force: true });
2133
+ }
2134
+ }
2135
+ if (!prune) return;
2136
+ for (const entry of readdirSync(destinationDirectory, { withFileTypes: true })) {
2137
+ if (sourceEntries.has(entry.name)) continue;
2138
+ rmSync(path5.join(destinationDirectory, entry.name), { recursive: true, force: true });
2139
+ }
1021
2140
  }
1022
2141
  ensureBundledDir();
1023
2142
  function ensureBundledDir() {
1024
- const missingRequiredTemplate = !existsSync(path3.join(SHARED_DIR, "BOOTSTRAP.md"));
2143
+ const missingRequiredTemplate = !existsSync(path5.join(SHARED_DIR, "BOOTSTRAP.md"));
1025
2144
  if (existsSync(BUNDLED_DIR) && !missingRequiredTemplate) return;
1026
2145
  if (!existsSync(MONOREPO_SRC)) {
1027
2146
  console.warn(
@@ -1035,7 +2154,7 @@ function ensureBundledDir() {
1035
2154
  var cached = null;
1036
2155
  async function readBundledManifest() {
1037
2156
  if (cached) return cached;
1038
- const systemSkillNames = (await readdir(SYSTEM_SKILLS_DIR, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
2157
+ const systemSkillNames = (await readdir3(SYSTEM_SKILLS_DIR, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
1039
2158
  const version = await hashDir(BUNDLED_DIR);
1040
2159
  cached = {
1041
2160
  version,
@@ -1050,19 +2169,19 @@ async function readBundledManifest() {
1050
2169
  async function hashDir(dir) {
1051
2170
  const hash = createHash3("sha256");
1052
2171
  await walk(dir, async (relPath, abs) => {
1053
- hash.update(`${relPath}:`).update(await readFile3(abs));
2172
+ hash.update(`${relPath}:`).update(await readFile4(abs));
1054
2173
  });
1055
2174
  return hash.digest("hex");
1056
2175
  }
1057
2176
  async function walk(dir, visit, base = dir) {
1058
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
2177
+ const entries = await readdir3(dir, { withFileTypes: true }).catch(() => []);
1059
2178
  entries.sort((a, b) => a.name.localeCompare(b.name));
1060
2179
  for (const entry of entries) {
1061
- const abs = path3.join(dir, entry.name);
2180
+ const abs = path5.join(dir, entry.name);
1062
2181
  if (entry.isDirectory()) {
1063
2182
  await walk(abs, visit, base);
1064
2183
  } else if (entry.isFile()) {
1065
- await visit(path3.relative(base, abs), abs);
2184
+ await visit(path5.relative(base, abs), abs);
1066
2185
  }
1067
2186
  }
1068
2187
  }
@@ -1080,6 +2199,30 @@ var BASE_CONNECTOR_CAPABILITIES = [
1080
2199
  "shell",
1081
2200
  "git"
1082
2201
  ];
2202
+ async function resolveConnectorScope(deviceId, launchConfig) {
2203
+ const serverUrl = normalizeServerOrigin(getServerUrl(launchConfig));
2204
+ const headers = { "Content-Type": "application/json" };
2205
+ const oauthToken = launchConfig?.oauthToken?.trim();
2206
+ const token = getConnectorToken(launchConfig);
2207
+ if (oauthToken) headers.Authorization = `Bearer ${oauthToken}`;
2208
+ if (token) headers["X-Buda-Connector-Token"] = token;
2209
+ const response = await fetch(`${serverUrl}/api/connectors/scope`, {
2210
+ method: "POST",
2211
+ headers,
2212
+ body: JSON.stringify({ deviceId })
2213
+ });
2214
+ if (!response.ok) {
2215
+ throw new Error(`/api/connectors/scope failed: ${response.status} ${await response.text()}`);
2216
+ }
2217
+ const result = await response.json();
2218
+ if (typeof result.spaceId !== "string" || !result.spaceId.trim()) {
2219
+ throw new Error("Connector scope response did not include spaceId");
2220
+ }
2221
+ return {
2222
+ ...typeof result.connectorId === "string" ? { connectorId: result.connectorId } : {},
2223
+ spaceId: result.spaceId.trim()
2224
+ };
2225
+ }
1083
2226
  var browserAvailabilityWarningLogged = false;
1084
2227
  var getBrowserAvailabilityWarning = (browserAvailability) => browserAvailability.available ? null : `${browserAvailability.error} Browser automation is disabled, but shell, filesystem, Git, and coding capabilities remain available.`;
1085
2228
  var getAdvertisedCapabilities = (browserAvailability = resolveBrowserAvailability(), deviceToken) => [
@@ -1101,7 +2244,7 @@ async function buildPayload(config, extra) {
1101
2244
  deviceName: config.deviceName,
1102
2245
  hostLabel: config.hostLabel,
1103
2246
  platform: `${platform3()} ${release()}`,
1104
- version: "dev",
2247
+ version: CONNECTOR_RUNTIME_VERSION,
1105
2248
  pid: process.pid,
1106
2249
  workdirRoot: config.workdirRoot,
1107
2250
  baseUrl: extra?.bunnyDaemon?.baseUrl,
@@ -1136,6 +2279,8 @@ function buildAuthHeaders(config, launchConfig) {
1136
2279
  "X-Buda-Connector-Id": config.connectorId
1137
2280
  };
1138
2281
  const token = config.token?.trim() || getConnectorToken(launchConfig);
2282
+ const oauthToken = launchConfig?.oauthToken?.trim();
2283
+ if (oauthToken) headers.Authorization = `Bearer ${oauthToken}`;
1139
2284
  if (token) headers["X-Buda-Connector-Token"] = token;
1140
2285
  if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
1141
2286
  return headers;
@@ -1149,108 +2294,1043 @@ async function postJson(serverUrl, pathname, body, config, launchConfig) {
1149
2294
  },
1150
2295
  body: JSON.stringify(body)
1151
2296
  });
1152
- if (!response.ok) {
1153
- const text2 = await response.text().catch(() => "");
1154
- let errorCode;
1155
- let errorMessage3 = text2;
2297
+ if (!response.ok) {
2298
+ const text2 = await response.text().catch(() => "");
2299
+ let errorCode;
2300
+ let errorMessage3 = text2;
2301
+ try {
2302
+ const payload = JSON.parse(text2);
2303
+ errorCode = typeof payload.code === "string" ? payload.code : void 0;
2304
+ errorMessage3 = typeof payload.error === "string" ? payload.error : text2;
2305
+ } catch {
2306
+ }
2307
+ if (errorCode === "device_revoked") {
2308
+ throw new Error(
2309
+ `This device is prohibited/revoked and cannot reconnect. Contact support. (${errorMessage3})`
2310
+ );
2311
+ }
2312
+ throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
2313
+ }
2314
+ return await response.json();
2315
+ }
2316
+ function formatConnectorOwner(connector) {
2317
+ return connector.userEmail ?? connector.userId ?? "anonymous";
2318
+ }
2319
+ async function warnIfStaleBundle(result) {
2320
+ if (!result.bundle?.staleClient) return;
2321
+ await writeLog(
2322
+ "warn",
2323
+ `connector bundle out of date \u2014 server expects ${result.bundle.expectedVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
2324
+ );
2325
+ }
2326
+ async function registerConnector(config, launchConfig, bunnyDaemon, options) {
2327
+ const result = await postJson(
2328
+ config.serverUrl,
2329
+ "/api/connectors/register",
2330
+ await buildPayload(config, {
2331
+ bunnyDaemon,
2332
+ ...options?.recoverMissingProfile ? { recovery: "missing_local_profile" } : {}
2333
+ }),
2334
+ config,
2335
+ launchConfig
2336
+ );
2337
+ await writeLog(
2338
+ "info",
2339
+ `registered connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
2340
+ );
2341
+ await warnIfStaleBundle(result);
2342
+ return {
2343
+ connectorId: result.connector.connectorId,
2344
+ spaceId: result.connector.spaceId ?? result.spaceId,
2345
+ relayProtocol: result.relayProtocol
2346
+ };
2347
+ }
2348
+ async function heartbeatConnector(config, launchConfig, bunnyDaemon) {
2349
+ const logs = takePendingLogs();
2350
+ const result = await postJson(
2351
+ config.serverUrl,
2352
+ "/api/connectors/heartbeat",
2353
+ await buildPayload(config, {
2354
+ bunnyDaemon,
2355
+ logs
2356
+ }),
2357
+ config,
2358
+ launchConfig
2359
+ ).catch((error) => {
2360
+ restorePendingLogs(logs);
2361
+ throw error;
2362
+ });
2363
+ return { relayProtocol: result.relayProtocol };
2364
+ }
2365
+ async function unregisterConnector(config, launchConfig) {
2366
+ await postJson(
2367
+ config.serverUrl,
2368
+ "/api/connectors/unregister",
2369
+ {
2370
+ connectorId: config.connectorId
2371
+ },
2372
+ config,
2373
+ launchConfig
2374
+ ).catch((error) => {
2375
+ void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
2376
+ });
2377
+ }
2378
+
2379
+ // src/runtime-install.ts
2380
+ import { execFile } from "child_process";
2381
+ import { randomUUID as randomUUID4 } from "crypto";
2382
+ import { access, cp, mkdir as mkdir5, readFile as readFile5, rename as rename4, rm, writeFile as writeFile3 } from "fs/promises";
2383
+ import { homedir as homedir3 } from "os";
2384
+ import path6 from "path";
2385
+ import { promisify } from "util";
2386
+ var execFileAsync = promisify(execFile);
2387
+ var RUNTIME_INSTALL_TIMEOUT_MS = 5 * 6e4;
2388
+ var exists = async (filePath) => access(filePath).then(() => true).catch(() => false);
2389
+ var runtimeRoot = () => process.env.BUDA_CONNECTOR_RUNTIME_ROOT || path6.join(homedir3(), ".buda", "runtime");
2390
+ var runtimePlatformKey = `${process.platform}-${process.arch}-node${process.versions.modules}`;
2391
+ var versionDirectory = () => path6.join(runtimeRoot(), runtimePlatformKey, CONNECTOR_RUNTIME_VERSION);
2392
+ var manifestPath = () => path6.join(versionDirectory(), "runtime.json");
2393
+ var readRuntimeManifest = async () => {
2394
+ try {
2395
+ const parsed = JSON.parse(await readFile5(manifestPath(), "utf8"));
2396
+ if (parsed.version !== CONNECTOR_RUNTIME_VERSION || !await exists(parsed.entrypoint) || !await exists(parsed.nodeExecutable)) {
2397
+ return null;
2398
+ }
2399
+ return parsed;
2400
+ } catch {
2401
+ return null;
2402
+ }
2403
+ };
2404
+ var writeManifest = async (directory, manifest) => {
2405
+ await writeFile3(path6.join(directory, "runtime.json"), `${JSON.stringify(manifest, null, 2)}
2406
+ `, {
2407
+ mode: 384
2408
+ });
2409
+ };
2410
+ var validateRuntime = async (runtime) => {
2411
+ const { stdout } = await execFileAsync(
2412
+ runtime.nodeExecutable,
2413
+ [...runtime.nodeArgs ?? [], runtime.entrypoint, "--help"],
2414
+ { timeout: 15e3 }
2415
+ );
2416
+ if (!stdout.includes("Help: buda-connector")) {
2417
+ throw new Error("Staged Connector runtime failed its CLI validation");
2418
+ }
2419
+ };
2420
+ var installFromNpm = async (directory) => {
2421
+ await mkdir5(directory, { recursive: true, mode: 448 });
2422
+ await execFileAsync(
2423
+ process.platform === "win32" ? "npm.cmd" : "npm",
2424
+ [
2425
+ "install",
2426
+ "--ignore-scripts=false",
2427
+ "--no-audit",
2428
+ "--no-fund",
2429
+ "--omit=dev",
2430
+ "--prefix",
2431
+ directory,
2432
+ `@buda-ai/connector@${CONNECTOR_RUNTIME_VERSION}`
2433
+ ],
2434
+ { env: process.env, timeout: RUNTIME_INSTALL_TIMEOUT_MS }
2435
+ );
2436
+ const entrypoint = path6.join(
2437
+ directory,
2438
+ "node_modules",
2439
+ "@buda-ai",
2440
+ "connector",
2441
+ "dist",
2442
+ "cli.js"
2443
+ );
2444
+ const nodeExecutable = path6.join(directory, "node", path6.basename(process.execPath));
2445
+ await mkdir5(path6.dirname(nodeExecutable), { recursive: true });
2446
+ await cp(process.execPath, nodeExecutable, { force: true });
2447
+ if (!await exists(entrypoint)) throw new Error("Installed Connector runtime is incomplete");
2448
+ return { entrypoint, nodeExecutable, version: CONNECTOR_RUNTIME_VERSION };
2449
+ };
2450
+ var installFromBundledRuntime = async (directory, sourceRoot) => {
2451
+ const sourceNode = process.env.BUDA_CONNECTOR_RUNTIME_NODE || process.execPath;
2452
+ const sourceEntrypoint = process.env.BUDA_CONNECTOR_RUNTIME_ENTRYPOINT || process.argv[1];
2453
+ if (!sourceEntrypoint) throw new Error("Bundled Connector runtime entrypoint is missing");
2454
+ await cp(sourceRoot, directory, { recursive: true, force: true });
2455
+ const relativeEntrypoint = path6.relative(sourceRoot, sourceEntrypoint);
2456
+ if (relativeEntrypoint.startsWith("..") || path6.isAbsolute(relativeEntrypoint)) {
2457
+ throw new Error("Bundled Connector entrypoint must be inside its runtime root");
2458
+ }
2459
+ const relativeNode = path6.relative(sourceRoot, sourceNode);
2460
+ const entrypoint = path6.join(directory, relativeEntrypoint);
2461
+ const nodeExecutable = relativeNode.startsWith("..") ? path6.join(directory, "node", path6.basename(sourceNode)) : path6.join(directory, relativeNode);
2462
+ if (relativeNode.startsWith("..")) {
2463
+ await mkdir5(path6.dirname(nodeExecutable), { recursive: true });
2464
+ await cp(sourceNode, nodeExecutable, { force: true });
2465
+ }
2466
+ if (!await exists(entrypoint))
2467
+ throw new Error("Copied Connector runtime entrypoint is missing");
2468
+ return { entrypoint, nodeExecutable, version: CONNECTOR_RUNTIME_VERSION };
2469
+ };
2470
+ async function ensureStableConnectorRuntime() {
2471
+ if (process.env.BUDA_CONNECTOR_SKIP_RUNTIME_STAGE === "1" || process.argv[1]?.endsWith(".ts")) {
2472
+ const entrypoint = process.argv[1];
2473
+ if (!entrypoint) throw new Error("Connector CLI entrypoint is missing");
2474
+ return {
2475
+ entrypoint,
2476
+ ...entrypoint.endsWith(".ts") ? { nodeArgs: [...process.execArgv] } : {},
2477
+ nodeExecutable: process.execPath,
2478
+ version: CONNECTOR_RUNTIME_VERSION
2479
+ };
2480
+ }
2481
+ const existing = await readRuntimeManifest();
2482
+ if (existing) return existing;
2483
+ const platformRoot = path6.join(runtimeRoot(), runtimePlatformKey);
2484
+ await mkdir5(platformRoot, { recursive: true, mode: 448 });
2485
+ return withExclusiveFileLock(
2486
+ path6.join(platformRoot, `install-${CONNECTOR_RUNTIME_VERSION}`),
2487
+ async () => {
2488
+ const installed = await readRuntimeManifest();
2489
+ if (installed) return installed;
2490
+ const temporary = path6.join(
2491
+ platformRoot,
2492
+ `.${CONNECTOR_RUNTIME_VERSION}.${randomUUID4()}.tmp`
2493
+ );
2494
+ await rm(temporary, { recursive: true, force: true });
2495
+ try {
2496
+ const bundledSource = process.env.BUDA_CONNECTOR_BUNDLED_RUNTIME_ROOT;
2497
+ const manifest = bundledSource ? await installFromBundledRuntime(temporary, bundledSource) : await installFromNpm(temporary);
2498
+ await validateRuntime(manifest);
2499
+ const finalDirectory = versionDirectory();
2500
+ const rebased = {
2501
+ ...manifest,
2502
+ entrypoint: path6.join(finalDirectory, path6.relative(temporary, manifest.entrypoint)),
2503
+ nodeExecutable: path6.join(
2504
+ finalDirectory,
2505
+ path6.relative(temporary, manifest.nodeExecutable)
2506
+ )
2507
+ };
2508
+ await writeManifest(temporary, rebased);
2509
+ await rm(finalDirectory, { recursive: true, force: true });
2510
+ await rename4(temporary, finalDirectory);
2511
+ return rebased;
2512
+ } catch (error) {
2513
+ await rm(temporary, { recursive: true, force: true });
2514
+ throw error;
2515
+ }
2516
+ },
2517
+ { timeoutMs: RUNTIME_INSTALL_TIMEOUT_MS }
2518
+ );
2519
+ }
2520
+
2521
+ // src/daemon-client.ts
2522
+ var START_TIMEOUT_MS = 45e3;
2523
+ var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
2524
+ var POLL_INTERVAL_MS = 250;
2525
+ var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
2526
+ var getConfigRoot = () => process.env.BUDA_CONNECTOR_CONFIG_ROOT;
2527
+ var readLiveStatus = async (config, configRoot = getConfigRoot(), timeoutMs) => requestDaemonControl(config, "status", configRoot, { timeoutMs }).catch(() => null);
2528
+ var isProcessAlive2 = (pid) => {
2529
+ try {
2530
+ process.kill(pid, 0);
2531
+ return true;
2532
+ } catch (error) {
2533
+ return !(typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH");
2534
+ }
2535
+ };
2536
+ var getRuntimeCommandArgs = (runtime, mode) => [
2537
+ ...runtime.nodeArgs ?? [],
2538
+ runtime.entrypoint,
2539
+ "daemon",
2540
+ "--internal-daemon",
2541
+ "--daemon-mode",
2542
+ mode,
2543
+ "--plain"
2544
+ ];
2545
+ var compareVersions = (left, right) => {
2546
+ const leftParts = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
2547
+ const rightParts = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
2548
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
2549
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
2550
+ if (difference !== 0) return difference;
2551
+ }
2552
+ return 0;
2553
+ };
2554
+ var shouldReplaceDaemonVersion = (runningVersion, candidateVersion) => compareVersions(candidateVersion, runningVersion) > 0;
2555
+ var waitForDaemon = async (launchConfig, deadline) => {
2556
+ let lastConfig = null;
2557
+ while (Date.now() < deadline) {
2558
+ lastConfig = await readDeviceConfig(launchConfig, getConfigRoot());
2559
+ if (lastConfig) {
2560
+ const status = await readLiveStatus(lastConfig);
2561
+ if (status) return { config: lastConfig, status };
2562
+ }
2563
+ await wait2(POLL_INTERVAL_MS);
2564
+ }
2565
+ throw new Error(
2566
+ lastConfig ? "Connector daemon did not expose its control channel in time" : "Connector registration did not create a canonical local identity in time"
2567
+ );
2568
+ };
2569
+ var spawnInternalDaemon = async (launchConfig, mode, preparedRuntime) => {
2570
+ const runtime = preparedRuntime ?? await ensureStableConnectorRuntime();
2571
+ const child = spawn(runtime.nodeExecutable, getRuntimeCommandArgs(runtime, mode), {
2572
+ detached: true,
2573
+ env: {
2574
+ ...process.env,
2575
+ BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG: JSON.stringify(launchConfig ?? {})
2576
+ },
2577
+ stdio: "ignore",
2578
+ windowsHide: true
2579
+ });
2580
+ child.unref();
2581
+ };
2582
+ var stopOlderIdleDaemon = async (config, status) => {
2583
+ if (!shouldReplaceDaemonVersion(status.version, CONNECTOR_RUNTIME_VERSION)) return false;
2584
+ if (!status.idle) return false;
2585
+ const stopStatus = await requestDaemonControl(config, "stop-if-idle", getConfigRoot());
2586
+ if (!stopStatus.idle) return false;
2587
+ const deadline = Date.now() + 1e4;
2588
+ while (Date.now() < deadline) {
2589
+ if (!await readLiveStatus(config)) return true;
2590
+ await wait2(POLL_INTERVAL_MS);
2591
+ }
2592
+ throw new Error("Older Connector daemon did not stop cleanly; existing identity was preserved");
2593
+ };
2594
+ async function ensureConnectorDaemon(launchConfig, mode) {
2595
+ const deviceId = await getOrCreateSystemDeviceId(getConfigRoot());
2596
+ const scope = await resolveConnectorScope(deviceId, launchConfig);
2597
+ launchConfig = { ...launchConfig, spaceId: scope.spaceId };
2598
+ const bootstrapTarget = getConnectorBootstrapLockTarget(launchConfig, getConfigRoot());
2599
+ await mkdir6(path7.dirname(bootstrapTarget), { recursive: true, mode: 448 });
2600
+ const bootstrapLock = await acquireExclusiveFileLock(bootstrapTarget, {
2601
+ timeoutMs: BOOTSTRAP_TIMEOUT_MS
2602
+ });
2603
+ try {
2604
+ const existingConfig = await readDeviceConfig(launchConfig, getConfigRoot());
2605
+ if (existingConfig) {
2606
+ const existingStatus = await readLiveStatus(existingConfig);
2607
+ if (existingStatus) {
2608
+ const preparedRuntime = !shouldReplaceDaemonVersion(existingStatus.version, CONNECTOR_RUNTIME_VERSION) || !existingStatus.idle ? void 0 : await ensureStableConnectorRuntime();
2609
+ const replaced = await stopOlderIdleDaemon(existingConfig, existingStatus);
2610
+ if (!replaced) {
2611
+ const status = mode === "persistent" ? await requestDaemonControl(existingConfig, "persist", getConfigRoot()) : existingStatus;
2612
+ return { config: existingConfig, status };
2613
+ }
2614
+ await spawnInternalDaemon(launchConfig, mode, preparedRuntime);
2615
+ return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
2616
+ }
2617
+ const persistedStatus = await readPersistedDaemonStatus(
2618
+ existingConfig,
2619
+ getConfigRoot()
2620
+ ).catch(() => null);
2621
+ if (persistedStatus && isProcessAlive2(persistedStatus.pid)) {
2622
+ throw new Error(
2623
+ `Connector daemon PID ${persistedStatus.pid} is running but its local control channel is not responding. Stop that process and retry.`
2624
+ );
2625
+ }
2626
+ }
2627
+ await spawnInternalDaemon(launchConfig, mode);
2628
+ return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
2629
+ } finally {
2630
+ await bootstrapLock.release();
2631
+ }
2632
+ }
2633
+ var streamDaemonLogs = (options) => {
2634
+ const logPath = getConnectorPaths(options.serverUrl, getConfigRoot()).logPath;
2635
+ let activePath = "";
2636
+ let seenLines = 0;
2637
+ const poll = async () => {
2638
+ const datedPath = getDatedConnectorLogPath(logPath);
2639
+ const raw = await readFile6(datedPath, "utf8").catch(() => "");
2640
+ const lines = raw.split("\n").filter(Boolean);
2641
+ if (datedPath !== activePath || lines.length < seenLines) {
2642
+ activePath = datedPath;
2643
+ seenLines = 0;
2644
+ }
2645
+ for (const line of lines.slice(seenLines)) {
2646
+ try {
2647
+ const entry = JSON.parse(line);
2648
+ const connectorId = entry.metadata?.connectorId;
2649
+ if (options.dashboardActive && typeof connectorId === "string") {
2650
+ const output2 = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
2651
+ appendConnectorDashboardLog(connectorId, entry.level, output2);
2652
+ continue;
2653
+ }
2654
+ if (connectorId !== options.connectorId) continue;
2655
+ const output = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
2656
+ if (entry.level === "error") console.error(output);
2657
+ else if (entry.level === "warn") console.warn(output);
2658
+ else console.log(output);
2659
+ } catch {
2660
+ if (options.plain) console.log(line);
2661
+ }
2662
+ }
2663
+ seenLines = lines.length;
2664
+ };
2665
+ void poll();
2666
+ const timer = setInterval(() => void poll(), 500);
2667
+ return () => clearInterval(timer);
2668
+ };
2669
+ var toDashboardConnector = (config, status) => ({
2670
+ agent: status ? status.idle ? "Ready" : "Busy" : "Stopped",
2671
+ connectorId: config.connectorId,
2672
+ heartbeat: status?.lastHeartbeatAt ? `OK ${new Date(status.lastHeartbeatAt).toLocaleTimeString("en-GB", { hour12: false })}` : status ? "Waiting" : "Stopped",
2673
+ relay: status?.relayConnected ? "Connected" : "Disconnected",
2674
+ serverUrl: config.serverUrl,
2675
+ spaceId: config.spaceId ?? "Unknown",
2676
+ status: status ? status.relayConnected ? "Online" : "Running" : "Stopped"
2677
+ });
2678
+ async function readLocalConnectorDashboardRows(configRoot = getConfigRoot()) {
2679
+ const configs = await listDeviceConfigs(configRoot);
2680
+ const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
2681
+ const persistedByIdentity = new Map(
2682
+ persistedStatuses.map((status) => [
2683
+ `${normalizeServerOrigin(status.serverUrl)}
2684
+ ${status.connectorId}`,
2685
+ status
2686
+ ])
2687
+ );
2688
+ const identities = /* @__PURE__ */ new Map();
2689
+ for (const config of configs) {
2690
+ identities.set(`${normalizeServerOrigin(config.serverUrl)}
2691
+ ${config.connectorId}`, {
2692
+ identity: config,
2693
+ stored: true
2694
+ });
2695
+ }
2696
+ for (const status of persistedStatuses) {
2697
+ const key = `${normalizeServerOrigin(status.serverUrl)}
2698
+ ${status.connectorId}`;
2699
+ if (!identities.has(key)) {
2700
+ identities.set(key, { identity: status, stored: false });
2701
+ }
2702
+ }
2703
+ const rows = await Promise.all(
2704
+ [...identities.entries()].map(async ([key, { identity, stored }]) => {
2705
+ const status = await readLiveStatus(identity, configRoot, 1500);
2706
+ if (status) return toDashboardConnector(identity, status);
2707
+ const persistedStatus = persistedByIdentity.get(key) ?? await readPersistedDaemonStatus(identity, configRoot).catch(() => null);
2708
+ const row = toDashboardConnector(identity, null);
2709
+ if (!stored && (!persistedStatus || !isProcessAlive2(persistedStatus.pid))) return null;
2710
+ return persistedStatus && isProcessAlive2(persistedStatus.pid) ? { ...row, status: "Unresponsive" } : row;
2711
+ })
2712
+ );
2713
+ return rows.filter((row) => row !== null);
2714
+ }
2715
+ async function stopLocalConnectorDaemon(connectorId, serverUrl, configRoot = getConfigRoot()) {
2716
+ const configs = await listDeviceConfigs(configRoot);
2717
+ const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
2718
+ const serverOrigin = normalizeServerOrigin(serverUrl);
2719
+ const config = [...configs, ...persistedStatuses].find(
2720
+ (candidate) => candidate.connectorId === connectorId && normalizeServerOrigin(candidate.serverUrl) === serverOrigin
2721
+ );
2722
+ if (!config) throw new Error(`Local Connector ${connectorId} was not found`);
2723
+ const status = await readLiveStatus(config, configRoot);
2724
+ if (!status) return;
2725
+ await requestDaemonControl(config, "stop", configRoot);
2726
+ }
2727
+ async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
2728
+ const configs = await listDeviceConfigs(configRoot);
2729
+ const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
2730
+ const identities = /* @__PURE__ */ new Map();
2731
+ for (const config of [...configs, ...persistedStatuses]) {
2732
+ identities.set(`${normalizeServerOrigin(config.serverUrl)}
2733
+ ${config.connectorId}`, config);
2734
+ }
2735
+ const results = await Promise.allSettled(
2736
+ [...identities.values()].map(async (config) => {
2737
+ const status = await readLiveStatus(config, configRoot);
2738
+ if (status) await requestDaemonControl(config, "stop", configRoot);
2739
+ })
2740
+ );
2741
+ const failures = results.filter((result) => result.status === "rejected");
2742
+ if (failures.length) {
2743
+ throw new Error(`Failed to stop ${failures.length} local Connector daemon(s)`);
2744
+ }
2745
+ }
2746
+ var waitForConnectorViewerExit = async (dashboardClosed) => {
2747
+ let finishSignal = () => void 0;
2748
+ const signal = new Promise((resolve) => {
2749
+ finishSignal = resolve;
2750
+ });
2751
+ process.once("SIGINT", finishSignal);
2752
+ process.once("SIGTERM", finishSignal);
2753
+ await (dashboardClosed ? Promise.race([dashboardClosed, signal]) : signal);
2754
+ process.off("SIGINT", finishSignal);
2755
+ process.off("SIGTERM", finishSignal);
2756
+ };
2757
+ async function runConnectorDashboard(launchConfig, options = {}) {
2758
+ let finishDashboard = () => void 0;
2759
+ const dashboardClosed = new Promise((resolve) => {
2760
+ finishDashboard = resolve;
2761
+ });
2762
+ const dashboard = startConnectorConsoleDashboard({
2763
+ manager: {
2764
+ onExit: finishDashboard,
2765
+ onStopAll: () => stopAllLocalConnectorDaemons(),
2766
+ onStopConnector: (connectorId, serverUrl) => stopLocalConnectorDaemon(connectorId, serverUrl)
2767
+ },
2768
+ plain: options.plain
2769
+ });
2770
+ if (!dashboard) {
2771
+ const { config } = await ensureConnectorDaemon(launchConfig, "persistent");
2772
+ const stopLogs2 = streamDaemonLogs({
2773
+ connectorId: config.connectorId,
2774
+ dashboardActive: false,
2775
+ plain: Boolean(options.plain),
2776
+ serverUrl: config.serverUrl
2777
+ });
2778
+ if (!process.stdin.isTTY) {
2779
+ stopLogs2();
2780
+ console.log(`Buda Connector ${config.connectorId} is running in the background.`);
2781
+ return;
2782
+ }
2783
+ await waitForConnectorViewerExit();
2784
+ stopLogs2();
2785
+ return;
2786
+ }
2787
+ const pendingConnectorId = `pending:${normalizeServerOrigin(getServerUrl(launchConfig))}`;
2788
+ let pendingConnector = {
2789
+ agent: "Starting",
2790
+ connectorId: pendingConnectorId,
2791
+ heartbeat: "Waiting",
2792
+ label: "New Connector",
2793
+ relay: "Connecting",
2794
+ serverUrl: getServerUrl(launchConfig),
2795
+ spaceId: "Resolving Space",
2796
+ status: "Connecting"
2797
+ };
2798
+ let preferredConnectorId = pendingConnectorId;
2799
+ let bootstrapSettled = false;
2800
+ const stopLogs = streamDaemonLogs({
2801
+ dashboardActive: true,
2802
+ plain: false,
2803
+ serverUrl: getServerUrl(launchConfig)
2804
+ });
2805
+ let refreshing = false;
2806
+ const refresh = async () => {
2807
+ if (refreshing) return;
2808
+ refreshing = true;
2809
+ try {
2810
+ const connectors = await readLocalConnectorDashboardRows();
2811
+ updateConnectorDashboardConnectors(
2812
+ pendingConnector ? [...connectors, pendingConnector] : connectors,
2813
+ preferredConnectorId
2814
+ );
2815
+ } finally {
2816
+ refreshing = false;
2817
+ }
2818
+ };
2819
+ updateConnectorDashboardConnectors([pendingConnector], pendingConnectorId);
2820
+ void readDeviceConfig(launchConfig, getConfigRoot()).then((config) => {
2821
+ if (!config) return;
2822
+ pendingConnector = void 0;
2823
+ preferredConnectorId = config.connectorId;
2824
+ }).catch(() => void 0).finally(() => refresh());
2825
+ const timer = setInterval(() => void refresh(), 1e3);
2826
+ void ensureConnectorDaemon(launchConfig, "persistent").then(async ({ config }) => {
2827
+ pendingConnector = void 0;
2828
+ preferredConnectorId = config.connectorId;
2829
+ await refresh();
2830
+ }).catch(async (error) => {
2831
+ const config = await readDeviceConfig(launchConfig, getConfigRoot()).catch(() => null);
2832
+ const message = `connector startup failed: ${error instanceof Error ? error.message : String(error)}`;
2833
+ if (config) {
2834
+ pendingConnector = void 0;
2835
+ preferredConnectorId = config.connectorId;
2836
+ appendConnectorDashboardLog(config.connectorId, "error", `[buda-connector] ${message}`);
2837
+ await refresh();
2838
+ } else {
2839
+ pendingConnector = {
2840
+ ...pendingConnector,
2841
+ agent: "Error",
2842
+ heartbeat: "Failed",
2843
+ relay: "Failed",
2844
+ status: "Error"
2845
+ };
2846
+ appendConnectorDashboardLog(pendingConnectorId, "error", `[buda-connector] ${message}`);
2847
+ await refresh();
2848
+ }
2849
+ }).finally(() => {
2850
+ bootstrapSettled = true;
2851
+ });
2852
+ await waitForConnectorViewerExit(dashboardClosed);
2853
+ clearInterval(timer);
2854
+ stopLogs();
2855
+ dashboard.stop();
2856
+ if (!bootstrapSettled) process.exit(0);
2857
+ }
2858
+ async function runDesktopConnectorController(launchConfig) {
2859
+ const { config } = await ensureConnectorDaemon(launchConfig, "transient");
2860
+ if (!config.spaceId?.trim()) {
2861
+ throw new Error("Connector scope did not resolve a canonical spaceId");
2862
+ }
2863
+ const lease = await holdDaemonControl(config, getConfigRoot());
2864
+ process.stdout.write(
2865
+ `${JSON.stringify({
2866
+ type: "buda-connector-ready",
2867
+ connectorId: lease.status.connectorId,
2868
+ pid: lease.status.pid,
2869
+ spaceId: lease.status.spaceId
2870
+ })}
2871
+ `
2872
+ );
2873
+ const stopped = new Promise((resolve) => {
2874
+ process.once("SIGINT", resolve);
2875
+ process.once("SIGTERM", resolve);
2876
+ process.stdin.once("end", resolve);
2877
+ process.stdin.once("close", resolve);
2878
+ if (process.stdin.readableEnded || process.stdin.destroyed) resolve();
2879
+ });
2880
+ await Promise.race([lease.closed, stopped]);
2881
+ lease.close();
2882
+ }
2883
+ async function releaseDesktopConnector(launchConfig) {
2884
+ if (!launchConfig?.spaceId?.trim()) return;
2885
+ const config = await readDeviceConfig(launchConfig, getConfigRoot());
2886
+ if (!config) return;
2887
+ await requestDaemonControl(config, "release", getConfigRoot()).catch(() => void 0);
2888
+ }
2889
+ var getInternalLaunchConfig = () => {
2890
+ const raw = process.env.BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG;
2891
+ if (!raw) return void 0;
2892
+ return JSON.parse(raw);
2893
+ };
2894
+ var getDaemonModeFlag = () => {
2895
+ const index = process.argv.indexOf("--daemon-mode");
2896
+ return process.argv[index + 1] === "transient" ? "transient" : "persistent";
2897
+ };
2898
+
2899
+ // src/onboard.ts
2900
+ import { cancel, intro, isCancel, log, note, outro, spinner, text } from "@clack/prompts";
2901
+
2902
+ // src/bunny-daemon.ts
2903
+ import { mkdir as mkdir7 } from "fs/promises";
2904
+ import { createDaemon } from "@bunny-agent/daemon";
2905
+
2906
+ // src/terminal.ts
2907
+ import { randomBytes as randomBytes3 } from "crypto";
2908
+ import { chmod as chmod3, stat as stat3 } from "fs/promises";
2909
+ import { createRequire } from "module";
2910
+ import path8 from "path";
2911
+ import WebSocket, { WebSocketServer } from "ws";
2912
+ var TERMINAL_IDLE_TTL_MS = 10 * 6e4;
2913
+ var TERMINAL_HANDSHAKE_TIMEOUT_MS = 1e4;
2914
+ var LOCAL_TERMINAL_GRANT_TTL_MS = 3e4;
2915
+ var LOCAL_TERMINAL_CLOSE_SPAWN_FAILED = 4401;
2916
+ var MAX_BUFFER_CHUNKS = 1e3;
2917
+ var MAX_TERMINAL_FRAME_BYTES = 1024 * 1024;
2918
+ var require2 = createRequire(import.meta.url);
2919
+ var ensureNodePtySpawnHelperExecutable = async (options = {}) => {
2920
+ const platform4 = options.platform ?? process.platform;
2921
+ if (platform4 !== "darwin") return;
2922
+ const arch = options.arch ?? process.arch;
2923
+ const entryPath = options.entryPath ?? require2.resolve("node-pty");
2924
+ const packageRoot = path8.dirname(path8.dirname(entryPath));
2925
+ const helperPath = path8.join(packageRoot, "prebuilds", `${platform4}-${arch}`, "spawn-helper");
2926
+ const helper = await stat3(helperPath);
2927
+ if ((helper.mode & 64) === 0) await chmod3(helperPath, helper.mode | 64);
2928
+ };
2929
+ var loadNodePty = async () => {
2930
+ await ensureNodePtySpawnHelperExecutable();
2931
+ return import("node-pty");
2932
+ };
2933
+ var normalizeTerminalCloseCode = (code, fallback = 1011) => code >= 1e3 && code <= 1014 && ![1004, 1005, 1006].includes(code) || code >= 3e3 && code <= 4999 ? code : fallback;
2934
+ var truncateTerminalCloseReason = (reason, maxBytes = 120) => {
2935
+ let result = reason;
2936
+ while (Buffer.byteLength(result, "utf8") > maxBytes) result = result.slice(0, -1);
2937
+ return result;
2938
+ };
2939
+ var sessions2 = /* @__PURE__ */ new Map();
2940
+ var localGrants = /* @__PURE__ */ new Map();
2941
+ var installedServers = /* @__PURE__ */ new WeakSet();
2942
+ var isSafeId = (value) => typeof value === "string" && /^[A-Za-z0-9_.:-]{1,160}$/.test(value);
2943
+ var parseDimension = (value, fallback, min, max) => typeof value === "number" && Number.isSafeInteger(value) ? Math.max(min, Math.min(max, value)) : fallback;
2944
+ var parseConnectorTerminalRequest = (request) => {
2945
+ if (request.method !== "CONNECT" || request.path !== "/api/terminal") {
2946
+ throw new Error("Invalid connector terminal request.");
2947
+ }
2948
+ if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
2949
+ throw new Error("Connector terminal request body is invalid.");
2950
+ }
2951
+ const body = request.body;
2952
+ if (!isSafeId(body.sessionId) || !isSafeId(body.tabId)) {
2953
+ throw new Error("Connector terminal session identity is invalid.");
2954
+ }
2955
+ if (typeof body.token !== "string" || body.token.length < 32 || body.token.length > 2048) {
2956
+ throw new Error("Connector terminal token is invalid.");
2957
+ }
2958
+ return {
2959
+ sessionId: body.sessionId,
2960
+ tabId: body.tabId,
2961
+ token: body.token,
2962
+ cols: parseDimension(body.cols, 80, 10, 500),
2963
+ rows: parseDimension(body.rows, 24, 5, 200)
2964
+ };
2965
+ };
2966
+ var clearIdleTimer = (session) => {
2967
+ if (!session.idleTimer) return;
2968
+ clearTimeout(session.idleTimer);
2969
+ session.idleTimer = null;
2970
+ };
2971
+ var scheduleIdleCleanup = (sessionKey, session) => {
2972
+ clearIdleTimer(session);
2973
+ session.idleTimer = setTimeout(() => {
2974
+ if (session.clients.size > 0 || sessions2.get(sessionKey) !== session) return;
2975
+ sessions2.delete(sessionKey);
2976
+ try {
2977
+ session.pty.kill();
2978
+ } catch {
2979
+ }
2980
+ }, TERMINAL_IDLE_TTL_MS);
2981
+ session.idleTimer.unref();
2982
+ };
2983
+ var sendJson = (socket, value) => {
2984
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(value));
2985
+ };
2986
+ var LocalTerminalUnavailableError = class extends Error {
2987
+ };
2988
+ var serializeErrorMessage = (error) => error instanceof Error ? error.message : "Unknown error";
2989
+ var createTerminalSession = async (grant, nodePtyLoader) => {
2990
+ let nodePty;
2991
+ try {
2992
+ nodePty = await nodePtyLoader();
2993
+ } catch (error) {
2994
+ void writeLog(
2995
+ "error",
2996
+ `local terminal could not load node-pty: ${serializeErrorMessage(error)}`
2997
+ );
2998
+ throw new LocalTerminalUnavailableError(
2999
+ "Local terminal is unavailable because node-pty could not be loaded. The connector remains available for other capabilities."
3000
+ );
3001
+ }
3002
+ const shell = process.platform === "win32" ? "powershell.exe" : process.env.SHELL || "/bin/bash";
3003
+ const shellArgs = process.platform === "win32" ? [] : ["-l"];
3004
+ let pty;
3005
+ try {
3006
+ pty = nodePty.spawn(shell, shellArgs, {
3007
+ name: "xterm-256color",
3008
+ cols: grant.cols,
3009
+ rows: grant.rows,
3010
+ cwd: path8.resolve(grant.cwd),
3011
+ env: { ...process.env, TERM: "xterm-256color" }
3012
+ });
3013
+ } catch (error) {
3014
+ const message = serializeErrorMessage(error);
3015
+ void writeLog(
3016
+ "error",
3017
+ `local terminal failed to spawn shell=${shell} cwd=${grant.cwd}: ${message}`
3018
+ );
3019
+ throw new LocalTerminalUnavailableError(
3020
+ "Local terminal is unavailable because node-pty could not start a shell. The connector remains available for other capabilities."
3021
+ );
3022
+ }
3023
+ const session = {
3024
+ pty,
3025
+ clients: /* @__PURE__ */ new Set(),
3026
+ buffer: [],
3027
+ cols: grant.cols,
3028
+ rows: grant.rows,
3029
+ idleTimer: null
3030
+ };
3031
+ sessions2.set(grant.sessionKey, session);
3032
+ pty.onData((data) => {
3033
+ session.buffer.push(data);
3034
+ if (session.buffer.length > MAX_BUFFER_CHUNKS) session.buffer.shift();
3035
+ for (const socket of session.clients) {
3036
+ if (socket.readyState === WebSocket.OPEN) socket.send(data);
3037
+ }
3038
+ });
3039
+ pty.onExit(({ exitCode }) => {
3040
+ if (sessions2.get(grant.sessionKey) === session) sessions2.delete(grant.sessionKey);
3041
+ clearIdleTimer(session);
3042
+ for (const socket of session.clients) {
3043
+ sendJson(socket, { type: "status", message: `exited (code ${exitCode})` });
3044
+ socket.close(1e3, "Terminal exited");
3045
+ }
3046
+ session.clients.clear();
3047
+ });
3048
+ return session;
3049
+ };
3050
+ var attachLocalTerminal = async (socket, grant, nodePtyLoader) => {
3051
+ const pendingMessages = [];
3052
+ let session;
3053
+ let started = false;
3054
+ const handleMessage = (data) => {
3055
+ if (!session) {
3056
+ pendingMessages.push(data);
3057
+ return;
3058
+ }
1156
3059
  try {
1157
- const payload = JSON.parse(text2);
1158
- errorCode = typeof payload.code === "string" ? payload.code : void 0;
1159
- errorMessage3 = typeof payload.error === "string" ? payload.error : text2;
3060
+ const message = JSON.parse(data);
3061
+ if (message.type === "bridge-start") {
3062
+ if (!started) {
3063
+ started = true;
3064
+ sendJson(socket, { type: "status", message: "connected" });
3065
+ for (const chunk of session.buffer) socket.send(chunk);
3066
+ }
3067
+ return;
3068
+ }
3069
+ if (message.type === "input" && typeof message.data === "string") {
3070
+ session.pty.write(message.data);
3071
+ return;
3072
+ }
3073
+ if (message.type === "resize") {
3074
+ const cols = parseDimension(message.cols, session.cols, 10, 500);
3075
+ const rows = parseDimension(message.rows, session.rows, 5, 200);
3076
+ if (cols !== session.cols || rows !== session.rows) {
3077
+ session.cols = cols;
3078
+ session.rows = rows;
3079
+ session.pty.resize(cols, rows);
3080
+ }
3081
+ return;
3082
+ }
1160
3083
  } catch {
1161
3084
  }
1162
- if (errorCode === "device_revoked") {
1163
- throw new Error(
1164
- `This device is prohibited/revoked and cannot reconnect. Contact support. (${errorMessage3})`
3085
+ session.pty.write(data);
3086
+ };
3087
+ socket.on("message", (raw) => handleMessage(raw.toString()));
3088
+ session = sessions2.get(grant.sessionKey);
3089
+ if (!session) session = await createTerminalSession(grant, nodePtyLoader);
3090
+ else clearIdleTimer(session);
3091
+ session.clients.add(socket);
3092
+ for (const data of pendingMessages.splice(0)) handleMessage(data);
3093
+ const detach = () => {
3094
+ if (!session) return;
3095
+ session.clients.delete(socket);
3096
+ if (session.clients.size === 0) scheduleIdleCleanup(grant.sessionKey, session);
3097
+ };
3098
+ socket.once("close", detach);
3099
+ socket.once("error", detach);
3100
+ };
3101
+ var installConnectorTerminalEndpoint = (server, options = {}) => {
3102
+ if (installedServers.has(server)) return;
3103
+ installedServers.add(server);
3104
+ const terminalServer = new WebSocketServer({ noServer: true });
3105
+ server.on("upgrade", (request, socket, head) => {
3106
+ const url = new URL(request.url ?? "/", "http://connector.local");
3107
+ if (url.pathname !== "/api/terminal") return;
3108
+ const tokenHeader = request.headers["x-buda-local-terminal-token"];
3109
+ const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
3110
+ const grant = token ? localGrants.get(token) : void 0;
3111
+ if (!token || !grant || grant.expiresAt <= Date.now()) {
3112
+ if (token) localGrants.delete(token);
3113
+ socket.destroy();
3114
+ return;
3115
+ }
3116
+ localGrants.delete(token);
3117
+ terminalServer.handleUpgrade(request, socket, head, (client) => {
3118
+ void attachLocalTerminal(client, grant, options.nodePtyLoader ?? loadNodePty).catch(
3119
+ (error) => {
3120
+ const message = error instanceof LocalTerminalUnavailableError ? error.message : "Local terminal is unavailable. The connector remains available for other capabilities.";
3121
+ sendJson(client, { type: "error", fatal: true, message });
3122
+ client.close(LOCAL_TERMINAL_CLOSE_SPAWN_FAILED, "Local terminal unavailable");
3123
+ }
1165
3124
  );
3125
+ });
3126
+ });
3127
+ server.once("close", () => {
3128
+ for (const client of terminalServer.clients) client.terminate();
3129
+ terminalServer.close();
3130
+ localGrants.clear();
3131
+ for (const session of sessions2.values()) {
3132
+ clearIdleTimer(session);
3133
+ try {
3134
+ session.pty.kill();
3135
+ } catch {
3136
+ }
3137
+ }
3138
+ sessions2.clear();
3139
+ });
3140
+ };
3141
+ var registerLocalTerminal = (input) => {
3142
+ if (!isSafeId(input.workspaceKey) || !isSafeId(input.tabId)) {
3143
+ throw new Error("Invalid local terminal workspace session.");
3144
+ }
3145
+ const token = randomBytes3(32).toString("base64url");
3146
+ localGrants.set(token, {
3147
+ sessionKey: `${input.workspaceKey}:${input.tabId}`,
3148
+ workspaceKey: input.workspaceKey,
3149
+ cwd: input.cwd,
3150
+ cols: input.cols,
3151
+ rows: input.rows,
3152
+ expiresAt: Date.now() + LOCAL_TERMINAL_GRANT_TTL_MS
3153
+ });
3154
+ const cleanupTimer = setTimeout(() => localGrants.delete(token), LOCAL_TERMINAL_GRANT_TTL_MS);
3155
+ cleanupTimer.unref();
3156
+ return token;
3157
+ };
3158
+ var toLocalTerminalUrl = (baseUrl) => {
3159
+ const url = new URL("/api/terminal", baseUrl);
3160
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
3161
+ return url.toString();
3162
+ };
3163
+ var connectSocket = async (socket) => new Promise((resolve, reject) => {
3164
+ const onOpen = () => {
3165
+ socket.removeListener("error", onError);
3166
+ resolve();
3167
+ };
3168
+ const onError = (error) => {
3169
+ socket.removeListener("open", onOpen);
3170
+ reject(error);
3171
+ };
3172
+ socket.once("open", onOpen);
3173
+ socket.once("error", onError);
3174
+ });
3175
+ var proxyConnectorTerminalTunnel = async (input) => {
3176
+ const cloud = new WebSocket(
3177
+ getConnectorTerminalUrl(input.serverUrl, input.connectorId, input.sessionId),
3178
+ {
3179
+ headers: { "X-Buda-Terminal-Token": input.cloudToken },
3180
+ handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
1166
3181
  }
1167
- throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
1168
- }
1169
- return await response.json();
1170
- }
1171
- function formatConnectorOwner(connector) {
1172
- return connector.userEmail ?? connector.userId ?? "anonymous";
1173
- }
1174
- async function warnIfStaleBundle(result) {
1175
- if (!result.bundle?.staleClient) return;
1176
- await writeLog(
1177
- "warn",
1178
- `connector bundle out of date \u2014 server expects ${result.bundle.expectedVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
1179
- );
1180
- }
1181
- async function registerConnector(config, launchConfig, bunnyDaemon, options) {
1182
- const result = await postJson(
1183
- config.serverUrl,
1184
- "/api/connectors/register",
1185
- await buildPayload(config, {
1186
- bunnyDaemon,
1187
- ...options?.recoverMissingProfile ? { recovery: "missing_local_profile" } : {}
1188
- }),
1189
- config,
1190
- launchConfig
1191
3182
  );
3183
+ const local = new WebSocket(toLocalTerminalUrl(input.localBaseUrl), {
3184
+ headers: { "X-Buda-Local-Terminal-Token": input.localToken },
3185
+ handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
3186
+ });
3187
+ try {
3188
+ await Promise.all([connectSocket(cloud), connectSocket(local)]);
3189
+ } catch (error) {
3190
+ cloud.terminate();
3191
+ local.terminate();
3192
+ throw error;
3193
+ }
3194
+ let bridgeAccepted = false;
3195
+ let activityEnded = false;
3196
+ const endActivity = () => {
3197
+ if (activityEnded) return;
3198
+ activityEnded = true;
3199
+ input.onActivityEnd?.();
3200
+ };
3201
+ let pendingBytes = 0;
3202
+ const pendingCloudFrames = [];
3203
+ const pendingLocalFrames = [];
3204
+ const bufferFrame = (frames, data, isBinary) => {
3205
+ const bytes = Array.isArray(data) ? data.reduce((total, chunk) => total + chunk.byteLength, 0) : data.byteLength;
3206
+ if (pendingBytes + bytes > MAX_TERMINAL_FRAME_BYTES) {
3207
+ cloud.close(1009, "Terminal handshake buffer too large");
3208
+ local.close(1009, "Terminal handshake buffer too large");
3209
+ return false;
3210
+ }
3211
+ pendingBytes += bytes;
3212
+ frames.push([data, isBinary]);
3213
+ return true;
3214
+ };
3215
+ cloud.on("message", (data, isBinary) => {
3216
+ if (!bridgeAccepted) {
3217
+ if (!isBinary && data.toString() === '{"type":"bridge-accepted"}') {
3218
+ bridgeAccepted = true;
3219
+ local.send(JSON.stringify({ type: "bridge-start" }));
3220
+ for (const [frame, binary] of pendingCloudFrames) {
3221
+ if (local.readyState === WebSocket.OPEN) local.send(frame, { binary });
3222
+ }
3223
+ for (const [frame, binary] of pendingLocalFrames) {
3224
+ if (cloud.readyState === WebSocket.OPEN) cloud.send(frame, { binary });
3225
+ }
3226
+ pendingCloudFrames.length = 0;
3227
+ pendingLocalFrames.length = 0;
3228
+ pendingBytes = 0;
3229
+ return;
3230
+ }
3231
+ bufferFrame(pendingCloudFrames, data, isBinary);
3232
+ return;
3233
+ }
3234
+ if (local.readyState === WebSocket.OPEN) local.send(data, { binary: isBinary });
3235
+ });
3236
+ local.on("message", (data, isBinary) => {
3237
+ if (!bridgeAccepted) {
3238
+ bufferFrame(pendingLocalFrames, data, isBinary);
3239
+ return;
3240
+ }
3241
+ if (cloud.readyState === WebSocket.OPEN) cloud.send(data, { binary: isBinary });
3242
+ });
3243
+ cloud.once("close", (code, reason) => {
3244
+ endActivity();
3245
+ if (!bridgeAccepted) {
3246
+ void writeLog(
3247
+ "warn",
3248
+ `terminal cloud tunnel closed before readiness session=${input.sessionId} code=${code} reason=${truncateTerminalCloseReason(reason.toString())}`
3249
+ );
3250
+ }
3251
+ if (local.readyState === WebSocket.OPEN) {
3252
+ local.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
3253
+ }
3254
+ });
3255
+ local.once("close", (code, reason) => {
3256
+ endActivity();
3257
+ if (cloud.readyState === WebSocket.OPEN) {
3258
+ cloud.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
3259
+ }
3260
+ });
3261
+ cloud.once("error", () => local.terminate());
3262
+ local.once("error", () => cloud.terminate());
3263
+ input.onActivityStart?.();
3264
+ cloud.send(JSON.stringify({ type: "bridge-ready" }));
1192
3265
  await writeLog(
1193
3266
  "info",
1194
- `registered connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
3267
+ `terminal proxy connected session=${input.sessionId} workspace=${input.workspaceKey}`
1195
3268
  );
1196
- await warnIfStaleBundle(result);
3269
+ };
3270
+
3271
+ // src/bunny-daemon.ts
3272
+ async function startEmbeddedBunnyDaemon(config, launchConfig) {
3273
+ const host = getBunnyDaemonHost(launchConfig);
3274
+ const port = getBunnyDaemonPort(launchConfig);
3275
+ const root = config.workdirRoot;
3276
+ await mkdir7(root, { recursive: true });
3277
+ const server = createDaemon({ host, port, root });
3278
+ installConnectorTerminalEndpoint(server);
3279
+ await new Promise((resolve, reject) => {
3280
+ const onError = (error) => {
3281
+ server.off("listening", onListening);
3282
+ reject(error);
3283
+ };
3284
+ const onListening = () => {
3285
+ server.off("error", onError);
3286
+ resolve();
3287
+ };
3288
+ server.once("error", onError);
3289
+ server.once("listening", onListening);
3290
+ server.listen(port, host);
3291
+ });
3292
+ const address = server.address();
3293
+ const actualPort = typeof address === "object" && address ? address.port : port;
3294
+ const baseUrl = `http://${host}:${actualPort}`;
3295
+ await writeLog("info", `bunny daemon listening ${baseUrl}`, {
3296
+ root
3297
+ });
1197
3298
  return {
1198
- connectorId: result.connector.connectorId,
1199
- relayProtocol: result.relayProtocol
3299
+ server,
3300
+ host,
3301
+ port: actualPort,
3302
+ baseUrl,
3303
+ root
1200
3304
  };
1201
3305
  }
1202
- async function heartbeatConnector(config, launchConfig, bunnyDaemon) {
1203
- const logs = takePendingLogs();
1204
- const result = await postJson(
1205
- config.serverUrl,
1206
- "/api/connectors/heartbeat",
1207
- await buildPayload(config, {
1208
- bunnyDaemon,
1209
- logs
1210
- }),
1211
- config,
1212
- launchConfig
1213
- ).catch((error) => {
1214
- restorePendingLogs(logs);
1215
- throw error;
1216
- });
1217
- return { relayProtocol: result.relayProtocol };
1218
- }
1219
- async function unregisterConnector(config, launchConfig) {
1220
- await postJson(
1221
- config.serverUrl,
1222
- "/api/connectors/unregister",
1223
- {
1224
- connectorId: config.connectorId
1225
- },
1226
- config,
1227
- launchConfig
1228
- ).catch((error) => {
1229
- void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
3306
+ async function stopEmbeddedBunnyDaemon(daemon) {
3307
+ await new Promise((resolve) => {
3308
+ daemon.server.close(() => resolve());
1230
3309
  });
3310
+ await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
1231
3311
  }
1232
3312
 
1233
3313
  // src/legacy-workspace-migration.ts
1234
- import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
3314
+ import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
1235
3315
  import { constants as constants2, createReadStream } from "fs";
1236
3316
  import {
1237
- chmod as chmod2,
3317
+ chmod as chmod4,
1238
3318
  copyFile,
1239
3319
  lstat,
1240
- mkdir as mkdir5,
3320
+ mkdir as mkdir8,
1241
3321
  open as open2,
1242
- readdir as readdir2,
1243
- readFile as readFile4,
3322
+ readdir as readdir4,
3323
+ readFile as readFile7,
1244
3324
  readlink,
1245
3325
  realpath,
1246
- rename as rename2,
1247
- rm,
3326
+ rename as rename5,
3327
+ rm as rm2,
1248
3328
  symlink,
1249
- unlink as unlink3
3329
+ unlink as unlink5
1250
3330
  } from "fs/promises";
1251
- import path4 from "path";
3331
+ import path9 from "path";
1252
3332
  var MIGRATION_VERSION = 1;
1253
- var MIGRATION_DIRECTORY = path4.join(".buda", "migrations", "legacy-workspace-v1");
3333
+ var MIGRATION_DIRECTORY = path9.join(".buda", "migrations", "legacy-workspace-v1");
1254
3334
  var JOURNAL_FILENAME = "journal.json";
1255
3335
  var IMMUTABLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
1256
3336
  var LEGACY_TEMPLATE_FILES = [
@@ -1286,7 +3366,7 @@ var LegacyWorkspaceMigrationError = class extends Error {
1286
3366
  recoverable = true;
1287
3367
  };
1288
3368
  var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
1289
- await mkdir5(storageRoot, { recursive: true, mode: 448 });
3369
+ await mkdir8(storageRoot, { recursive: true, mode: 448 });
1290
3370
  const rootInfo = await lstat(storageRoot);
1291
3371
  if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) {
1292
3372
  throw new LegacyWorkspaceMigrationError(
@@ -1295,9 +3375,9 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
1295
3375
  );
1296
3376
  }
1297
3377
  const canonicalRoot = await realpath(storageRoot);
1298
- const migrationDirectory = path4.join(canonicalRoot, MIGRATION_DIRECTORY);
3378
+ const migrationDirectory = path9.join(canonicalRoot, MIGRATION_DIRECTORY);
1299
3379
  await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
1300
- const journalPath = path4.join(migrationDirectory, JOURNAL_FILENAME);
3380
+ const journalPath = path9.join(migrationDirectory, JOURNAL_FILENAME);
1301
3381
  let journal = await readJournal(journalPath);
1302
3382
  if (journal) validateJournalPaths(journal, canonicalRoot, migrationDirectory);
1303
3383
  if (journal?.status === "complete") {
@@ -1330,8 +3410,8 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
1330
3410
  };
1331
3411
  var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
1332
3412
  if (item.phase === "committed") return;
1333
- if (await exists(item.target)) {
1334
- if (!await exists(item.staging) && ["verified", "copied", "copying"].includes(item.phase)) {
3413
+ if (await exists2(item.target)) {
3414
+ if (!await exists2(item.staging) && ["verified", "copied", "copying"].includes(item.phase)) {
1335
3415
  await verifyTree(item.source, item.target);
1336
3416
  item.phase = "committed";
1337
3417
  await writeJournal(journalPath, journal);
@@ -1346,8 +3426,8 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
1346
3426
  item.phase = "copying";
1347
3427
  await writeJournal(journalPath, journal);
1348
3428
  await checkpoint(options, "before-copy", item.id);
1349
- await rm(item.staging, { recursive: true, force: true });
1350
- await ensurePrivateDirectoryWithin(storageRoot, path4.dirname(item.staging));
3429
+ await rm2(item.staging, { recursive: true, force: true });
3430
+ await ensurePrivateDirectoryWithin(storageRoot, path9.dirname(item.staging));
1351
3431
  await copyTree(item.source, item.staging, () => checkpoint(options, "during-copy", item.id));
1352
3432
  await checkpoint(options, "after-copy", item.id);
1353
3433
  item.phase = "copied";
@@ -1358,7 +3438,7 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
1358
3438
  try {
1359
3439
  await verifyTree(item.source, item.staging);
1360
3440
  } catch (error) {
1361
- await rm(item.staging, { recursive: true, force: true });
3441
+ await rm2(item.staging, { recursive: true, force: true });
1362
3442
  item.phase = "copying";
1363
3443
  await writeJournal(journalPath, journal);
1364
3444
  throw error;
@@ -1369,9 +3449,9 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
1369
3449
  }
1370
3450
  if (item.phase === "verified") {
1371
3451
  await checkpoint(options, "before-rename", item.id);
1372
- await ensurePrivateDirectoryWithin(storageRoot, path4.dirname(item.target));
1373
- await rename2(item.staging, item.target);
1374
- await syncDirectory(path4.dirname(item.target));
3452
+ await ensurePrivateDirectoryWithin(storageRoot, path9.dirname(item.target));
3453
+ await rename5(item.staging, item.target);
3454
+ await syncDirectory2(path9.dirname(item.target));
1375
3455
  await checkpoint(options, "after-rename", item.id);
1376
3456
  item.phase = "committed";
1377
3457
  await writeJournal(journalPath, journal);
@@ -1381,14 +3461,14 @@ var resumeRollback = async (journalPath, journal, options) => {
1381
3461
  for (const item of [...journal.items].reverse()) {
1382
3462
  if (item.phase === "rolled-back") continue;
1383
3463
  await checkpoint(options, "before-rollback", item.id);
1384
- if (!await exists(item.source)) {
3464
+ if (!await exists2(item.source)) {
1385
3465
  throw new LegacyWorkspaceMigrationError(
1386
3466
  "MIGRATION_VERIFY_FAILED",
1387
3467
  `Cannot roll back ${item.id}: the preserved legacy source is missing`
1388
3468
  );
1389
3469
  }
1390
- await rm(item.staging, { recursive: true, force: true });
1391
- await rm(item.target, { recursive: true, force: true });
3470
+ await rm2(item.staging, { recursive: true, force: true });
3471
+ await rm2(item.target, { recursive: true, force: true });
1392
3472
  await checkpoint(options, "after-rollback", item.id);
1393
3473
  item.phase = "rolled-back";
1394
3474
  await writeJournal(journalPath, journal);
@@ -1402,12 +3482,12 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
1402
3482
  const ownership = owner ? "explicit-owner" : "shared-legacy";
1403
3483
  const label = owner ? `Legacy Connector root assigned to ${owner.spaceId}/${owner.agentId}` : "Shared legacy Connector root (ownership unassigned)";
1404
3484
  const sources = await inventoryLegacySources(storageRoot);
1405
- const stagingRoot = path4.join(migrationDirectory, "staging", randomUUID2());
3485
+ const stagingRoot = path9.join(migrationDirectory, "staging", randomUUID5());
1406
3486
  const items = sources.map(({ id, source }) => ({
1407
3487
  id,
1408
3488
  source,
1409
3489
  target: deriveTarget(storageRoot, id, owner),
1410
- staging: path4.join(stagingRoot, id),
3490
+ staging: path9.join(stagingRoot, id),
1411
3491
  phase: "pending"
1412
3492
  }));
1413
3493
  assertDistinctTargets(items);
@@ -1424,29 +3504,29 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
1424
3504
  };
1425
3505
  };
1426
3506
  var inventoryLegacySources = async (storageRoot) => {
1427
- const rootEntries = await readdir2(storageRoot, { withFileTypes: true });
3507
+ const rootEntries = await readdir4(storageRoot, { withFileTypes: true });
1428
3508
  const templateNames = new Set(LEGACY_TEMPLATE_FILES);
1429
3509
  for (const entry of rootEntries) {
1430
3510
  if (entry.name.endsWith(".md")) templateNames.add(entry.name);
1431
3511
  }
1432
3512
  const candidates = [
1433
- { id: "agent", source: path4.join(storageRoot, "agent") },
1434
- { id: "space", source: path4.join(storageRoot, "space") },
1435
- { id: "logs", source: path4.join(storageRoot, "logs") },
1436
- { id: "connector-log", source: path4.join(storageRoot, "connector.log") },
1437
- { id: "runtime-config", source: path4.join(storageRoot, "runtime-config") },
1438
- { id: "runtime-config-hidden", source: path4.join(storageRoot, ".runtime-config") },
1439
- { id: "runtime-config-file", source: path4.join(storageRoot, "runtime-config.json") },
1440
- { id: "runtime-config-hidden-file", source: path4.join(storageRoot, ".runtime-config.json") },
1441
- { id: "bunny-runtime", source: path4.join(storageRoot, ".bunny") },
3513
+ { id: "agent", source: path9.join(storageRoot, "agent") },
3514
+ { id: "space", source: path9.join(storageRoot, "space") },
3515
+ { id: "logs", source: path9.join(storageRoot, "logs") },
3516
+ { id: "connector-log", source: path9.join(storageRoot, "connector.log") },
3517
+ { id: "runtime-config", source: path9.join(storageRoot, "runtime-config") },
3518
+ { id: "runtime-config-hidden", source: path9.join(storageRoot, ".runtime-config") },
3519
+ { id: "runtime-config-file", source: path9.join(storageRoot, "runtime-config.json") },
3520
+ { id: "runtime-config-hidden-file", source: path9.join(storageRoot, ".runtime-config.json") },
3521
+ { id: "bunny-runtime", source: path9.join(storageRoot, ".bunny") },
1442
3522
  ...[...templateNames].sort().map((name) => ({
1443
3523
  id: `template-${name}`,
1444
- source: path4.join(storageRoot, name)
3524
+ source: path9.join(storageRoot, name)
1445
3525
  }))
1446
3526
  ];
1447
3527
  const found = [];
1448
3528
  for (const candidate of candidates) {
1449
- if (!await exists(candidate.source)) continue;
3529
+ if (!await exists2(candidate.source)) continue;
1450
3530
  const info = await lstat(candidate.source);
1451
3531
  if (info.isSymbolicLink()) {
1452
3532
  throw new LegacyWorkspaceMigrationError(
@@ -1454,36 +3534,36 @@ var inventoryLegacySources = async (storageRoot) => {
1454
3534
  `Legacy migration source ${candidate.id} must not be a symbolic link`
1455
3535
  );
1456
3536
  }
1457
- if (info.isDirectory() && (await readdir2(candidate.source)).length === 0) continue;
3537
+ if (info.isDirectory() && (await readdir4(candidate.source)).length === 0) continue;
1458
3538
  found.push(candidate);
1459
3539
  }
1460
3540
  return found;
1461
3541
  };
1462
3542
  var deriveTarget = (storageRoot, id, owner) => {
1463
- if (!owner) return path4.join(storageRoot, ".buda", "legacy", "shared", id);
1464
- const spaceRoot = path4.join(storageRoot, ".buda", "spaces", owner.spaceId);
1465
- const agentRoot = path4.join(spaceRoot, "agents", owner.agentId);
1466
- if (id === "agent") return path4.join(agentRoot, "workspace", "legacy-connector-root");
1467
- if (id === "space") return path4.join(spaceRoot, "shared", "legacy-connector-root");
3543
+ if (!owner) return path9.join(storageRoot, ".buda", "legacy", "shared", id);
3544
+ const spaceRoot = path9.join(storageRoot, ".buda", "spaces", owner.spaceId);
3545
+ const agentRoot = path9.join(spaceRoot, "agents", owner.agentId);
3546
+ if (id === "agent") return path9.join(agentRoot, "workspace", "legacy-connector-root");
3547
+ if (id === "space") return path9.join(spaceRoot, "shared", "legacy-connector-root");
1468
3548
  if (id.startsWith("template-")) {
1469
- return path4.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
3549
+ return path9.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
1470
3550
  }
1471
- return path4.join(agentRoot, "state", "legacy-connector-root", id);
3551
+ return path9.join(agentRoot, "state", "legacy-connector-root", id);
1472
3552
  };
1473
3553
  var copyTree = async (source, destination, onEntryCopied) => {
1474
3554
  const info = await lstat(source);
1475
3555
  if (info.isDirectory()) {
1476
- await mkdir5(destination, { mode: info.mode & 4095 });
1477
- const entries = (await readdir2(source)).sort();
3556
+ await mkdir8(destination, { mode: info.mode & 4095 });
3557
+ const entries = (await readdir4(source)).sort();
1478
3558
  for (const entry of entries) {
1479
- await copyTree(path4.join(source, entry), path4.join(destination, entry), onEntryCopied);
3559
+ await copyTree(path9.join(source, entry), path9.join(destination, entry), onEntryCopied);
1480
3560
  }
1481
- await chmod2(destination, info.mode & 4095);
3561
+ await chmod4(destination, info.mode & 4095);
1482
3562
  return;
1483
3563
  }
1484
3564
  if (info.isFile()) {
1485
3565
  await copyFile(source, destination, constants2.COPYFILE_EXCL);
1486
- await chmod2(destination, info.mode & 4095);
3566
+ await chmod4(destination, info.mode & 4095);
1487
3567
  await onEntryCopied();
1488
3568
  return;
1489
3569
  }
@@ -1511,8 +3591,8 @@ var verifyTree = async (source, destination) => {
1511
3591
  }
1512
3592
  if (sourceInfo.isDirectory()) {
1513
3593
  const [sourceEntries, destinationEntries] = await Promise.all([
1514
- readdir2(source),
1515
- readdir2(destination)
3594
+ readdir4(source),
3595
+ readdir4(destination)
1516
3596
  ]);
1517
3597
  sourceEntries.sort();
1518
3598
  destinationEntries.sort();
@@ -1520,7 +3600,7 @@ var verifyTree = async (source, destination) => {
1520
3600
  throw verifyFailure("directory entries differ");
1521
3601
  }
1522
3602
  for (const entry of sourceEntries) {
1523
- await verifyTree(path4.join(source, entry), path4.join(destination, entry));
3603
+ await verifyTree(path9.join(source, entry), path9.join(destination, entry));
1524
3604
  }
1525
3605
  return;
1526
3606
  }
@@ -1550,7 +3630,7 @@ var hashFile = async (filePath) => {
1550
3630
  var readJournal = async (journalPath) => {
1551
3631
  let raw;
1552
3632
  try {
1553
- raw = await readFile4(journalPath, "utf8");
3633
+ raw = await readFile7(journalPath, "utf8");
1554
3634
  } catch (error) {
1555
3635
  if (isMissing(error)) return void 0;
1556
3636
  throw error;
@@ -1562,13 +3642,13 @@ var readJournal = async (journalPath) => {
1562
3642
  throw corrupt("Legacy migration journal contains invalid JSON");
1563
3643
  }
1564
3644
  if (!isJournal(value)) throw corrupt("Legacy migration journal is malformed");
1565
- await chmod2(journalPath, 384);
3645
+ await chmod4(journalPath, 384);
1566
3646
  return value;
1567
3647
  };
1568
3648
  var writeJournal = async (journalPath, journal) => {
1569
3649
  journal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1570
- await ensurePrivateDirectory(path4.dirname(journalPath));
1571
- const temporaryPath = `${journalPath}.${process.pid}.${randomUUID2()}.tmp`;
3650
+ await ensurePrivateDirectory(path9.dirname(journalPath));
3651
+ const temporaryPath = `${journalPath}.${process.pid}.${randomUUID5()}.tmp`;
1572
3652
  const handle = await open2(
1573
3653
  temporaryPath,
1574
3654
  constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
@@ -1582,14 +3662,14 @@ var writeJournal = async (journalPath, journal) => {
1582
3662
  await handle.close();
1583
3663
  }
1584
3664
  try {
1585
- await rename2(temporaryPath, journalPath);
1586
- await chmod2(journalPath, 384);
1587
- await syncDirectory(path4.dirname(journalPath));
3665
+ await rename5(temporaryPath, journalPath);
3666
+ await chmod4(journalPath, 384);
3667
+ await syncDirectory2(path9.dirname(journalPath));
1588
3668
  } finally {
1589
- await unlink3(temporaryPath).catch(() => void 0);
3669
+ await unlink5(temporaryPath).catch(() => void 0);
1590
3670
  }
1591
3671
  };
1592
- var syncDirectory = async (directory) => {
3672
+ var syncDirectory2 = async (directory) => {
1593
3673
  const handle = await open2(directory, constants2.O_RDONLY);
1594
3674
  try {
1595
3675
  await handle.sync();
@@ -1598,7 +3678,7 @@ var syncDirectory = async (directory) => {
1598
3678
  }
1599
3679
  };
1600
3680
  var ensurePrivateDirectory = async (directory) => {
1601
- await mkdir5(directory, { mode: 448 }).catch((error) => {
3681
+ await mkdir8(directory, { mode: 448 }).catch((error) => {
1602
3682
  if (!isAlreadyExists2(error)) throw error;
1603
3683
  });
1604
3684
  const info = await lstat(directory);
@@ -1608,16 +3688,16 @@ var ensurePrivateDirectory = async (directory) => {
1608
3688
  "Legacy migration control path must be a real directory"
1609
3689
  );
1610
3690
  }
1611
- await chmod2(directory, 448);
3691
+ await chmod4(directory, 448);
1612
3692
  };
1613
3693
  var ensurePrivateDirectoryWithin = async (storageRoot, directory) => {
1614
3694
  if (directory !== storageRoot && !isPathInside(storageRoot, directory)) {
1615
3695
  throw corrupt("Legacy migration control path escapes protected storage");
1616
3696
  }
1617
- const relative = path4.relative(storageRoot, directory);
3697
+ const relative = path9.relative(storageRoot, directory);
1618
3698
  let current = storageRoot;
1619
- for (const segment of relative.split(path4.sep).filter(Boolean)) {
1620
- current = path4.join(current, segment);
3699
+ for (const segment of relative.split(path9.sep).filter(Boolean)) {
3700
+ current = path9.join(current, segment);
1621
3701
  await ensurePrivateDirectory(current);
1622
3702
  }
1623
3703
  };
@@ -1642,7 +3722,7 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
1642
3722
  const stagingPaths = /* @__PURE__ */ new Set();
1643
3723
  for (const item of journal.items) {
1644
3724
  const sourceRelative = sourceRelativePath(item.id);
1645
- if (!sourceRelative || item.source !== path4.join(storageRoot, sourceRelative)) {
3725
+ if (!sourceRelative || item.source !== path9.join(storageRoot, sourceRelative)) {
1646
3726
  throw corrupt("Legacy migration journal contains an unexpected source path");
1647
3727
  }
1648
3728
  if (item.target !== deriveTarget(storageRoot, item.id, journal.owner)) {
@@ -1651,8 +3731,8 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
1651
3731
  if (!isPathInside(migrationDirectory, item.staging)) {
1652
3732
  throw corrupt("Legacy migration journal staging path escapes the migration directory");
1653
3733
  }
1654
- const stagingParent = path4.dirname(item.staging);
1655
- if (path4.dirname(stagingParent) !== path4.join(migrationDirectory, "staging") || item.staging !== path4.join(stagingParent, item.id)) {
3734
+ const stagingParent = path9.dirname(item.staging);
3735
+ if (path9.dirname(stagingParent) !== path9.join(migrationDirectory, "staging") || item.staging !== path9.join(stagingParent, item.id)) {
1656
3736
  throw corrupt("Legacy migration journal staging path is not canonical");
1657
3737
  }
1658
3738
  stagingParents.add(stagingParent);
@@ -1682,7 +3762,7 @@ var sourceRelativePath = (id) => {
1682
3762
  if (known[id]) return known[id];
1683
3763
  if (!id.startsWith("template-")) return null;
1684
3764
  const name = id.slice("template-".length);
1685
- return name.endsWith(".md") && path4.basename(name) === name ? name : null;
3765
+ return name.endsWith(".md") && path9.basename(name) === name ? name : null;
1686
3766
  };
1687
3767
  var validateJournalOwnership = (journal) => {
1688
3768
  if (journal.ownership === "explicit-owner") {
@@ -1745,7 +3825,7 @@ var isJournal = (value) => {
1745
3825
  (item) => item && typeof item === "object" && typeof item.id === "string" && typeof item.source === "string" && typeof item.target === "string" && typeof item.staging === "string" && typeof item.phase === "string" && ITEM_PHASES.has(item.phase)
1746
3826
  );
1747
3827
  };
1748
- var exists = async (filePath) => {
3828
+ var exists2 = async (filePath) => {
1749
3829
  try {
1750
3830
  await lstat(filePath);
1751
3831
  return true;
@@ -1755,8 +3835,8 @@ var exists = async (filePath) => {
1755
3835
  }
1756
3836
  };
1757
3837
  var isPathInside = (parent, child) => {
1758
- const relative = path4.relative(parent, child);
1759
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
3838
+ const relative = path9.relative(parent, child);
3839
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path9.sep}`) && !path9.isAbsolute(relative);
1760
3840
  };
1761
3841
  var isMissing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
1762
3842
  var isAlreadyExists2 = (error) => error instanceof Error && "code" in error && error.code === "EEXIST";
@@ -1768,9 +3848,9 @@ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT"
1768
3848
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
1769
3849
 
1770
3850
  // src/relay.ts
1771
- import path6 from "path";
3851
+ import path11 from "path";
1772
3852
  import { createNextHandler } from "@bunny-agent/daemon/nextjs";
1773
- import WebSocket from "ws";
3853
+ import WebSocket2 from "ws";
1774
3854
 
1775
3855
  // src/execution-identity.ts
1776
3856
  import {
@@ -1940,15 +4020,15 @@ import { constants as constants3 } from "fs";
1940
4020
  import {
1941
4021
  copyFile as copyFile2,
1942
4022
  lstat as lstat2,
1943
- mkdir as mkdir6,
1944
- readdir as readdir3,
1945
- readFile as readFile5,
4023
+ mkdir as mkdir9,
4024
+ readdir as readdir5,
4025
+ readFile as readFile8,
1946
4026
  realpath as realpath2,
1947
- rm as rm2,
4027
+ rm as rm3,
1948
4028
  symlink as symlink2,
1949
- writeFile as writeFile2
4029
+ writeFile as writeFile4
1950
4030
  } from "fs/promises";
1951
- import path5 from "path";
4031
+ import path10 from "path";
1952
4032
  var MOUNT_DIRECTORY = "mount";
1953
4033
  var ARCHIVE_MARKER = "archived.json";
1954
4034
  var AGENT_CONTEXT_TEMPLATE_FILES = [
@@ -1960,8 +4040,8 @@ var AGENT_CONTEXT_TEMPLATE_FILES = [
1960
4040
  "BOOTSTRAP.md",
1961
4041
  "MEMORY.md"
1962
4042
  ];
1963
- var deriveSystemVolume = (descriptor) => path5.join(
1964
- path5.dirname(path5.dirname(path5.dirname(path5.dirname(descriptor.sharedVolume)))),
4043
+ var deriveSystemVolume = (descriptor) => path10.join(
4044
+ path10.dirname(path10.dirname(path10.dirname(path10.dirname(descriptor.sharedVolume)))),
1965
4045
  "system"
1966
4046
  );
1967
4047
  var WorkspaceVolumeLifecycle = class {
@@ -1973,14 +4053,14 @@ var WorkspaceVolumeLifecycle = class {
1973
4053
  async mount() {
1974
4054
  await this.ensureVolumes();
1975
4055
  await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
1976
- const mountRoot = path5.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
1977
- await mkdir6(mountRoot, { recursive: true, mode: 448 });
4056
+ const mountRoot = path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
4057
+ await mkdir9(mountRoot, { recursive: true, mode: 448 });
1978
4058
  const mounts = {
1979
4059
  mountRoot,
1980
- agent: path5.join(mountRoot, "agent"),
1981
- space: path5.join(mountRoot, "space"),
1982
- system: path5.join(mountRoot, "system"),
1983
- temporary: path5.join(mountRoot, "tmp")
4060
+ agent: path10.join(mountRoot, "agent"),
4061
+ space: path10.join(mountRoot, "space"),
4062
+ system: path10.join(mountRoot, "system"),
4063
+ temporary: path10.join(mountRoot, "tmp")
1984
4064
  };
1985
4065
  await Promise.all([
1986
4066
  ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
@@ -1988,69 +4068,69 @@ var WorkspaceVolumeLifecycle = class {
1988
4068
  ensureVolumeLink(this.systemVolume, mounts.system),
1989
4069
  ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
1990
4070
  ]);
1991
- const daemonVolumes = path5.join(mountRoot, "volumes");
1992
- await mkdir6(daemonVolumes, { recursive: true, mode: 448 });
4071
+ const daemonVolumes = path10.join(mountRoot, "volumes");
4072
+ await mkdir9(daemonVolumes, { recursive: true, mode: 448 });
1993
4073
  await Promise.all([
1994
- ensureVolumeLink(this.descriptor.privateVolume, path5.join(daemonVolumes, "agent")),
1995
- ensureVolumeLink(this.descriptor.sharedVolume, path5.join(daemonVolumes, "space")),
1996
- ensureVolumeLink(this.systemVolume, path5.join(daemonVolumes, "system")),
1997
- ensureVolumeLink(this.descriptor.temporaryVolume, path5.join(daemonVolumes, "tmp"))
4074
+ ensureVolumeLink(this.descriptor.privateVolume, path10.join(daemonVolumes, "agent")),
4075
+ ensureVolumeLink(this.descriptor.sharedVolume, path10.join(daemonVolumes, "space")),
4076
+ ensureVolumeLink(this.systemVolume, path10.join(daemonVolumes, "system")),
4077
+ ensureVolumeLink(this.descriptor.temporaryVolume, path10.join(daemonVolumes, "tmp"))
1998
4078
  ]);
1999
4079
  return mounts;
2000
4080
  }
2001
4081
  async unmount() {
2002
- await rm2(path5.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
4082
+ await rm3(path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
2003
4083
  recursive: true,
2004
4084
  force: true
2005
4085
  });
2006
4086
  }
2007
4087
  async archiveAgent() {
2008
4088
  await this.unmount();
2009
- await mkdir6(this.descriptor.stateVolume, { recursive: true, mode: 448 });
2010
- await writeFile2(
2011
- path5.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
4089
+ await mkdir9(this.descriptor.stateVolume, { recursive: true, mode: 448 });
4090
+ await writeFile4(
4091
+ path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
2012
4092
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
2013
4093
  `,
2014
4094
  { encoding: "utf8", mode: 384 }
2015
4095
  );
2016
4096
  }
2017
4097
  async restoreAgent() {
2018
- await rm2(path5.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
4098
+ await rm3(path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
2019
4099
  return this.mount();
2020
4100
  }
2021
4101
  async deleteAgent(options) {
2022
4102
  requireConfirmation(options);
2023
4103
  await this.unmount();
2024
- const agentRoot = path5.dirname(this.descriptor.privateVolume);
2025
- await rm2(agentRoot, { recursive: true, force: true });
4104
+ const agentRoot = path10.dirname(this.descriptor.privateVolume);
4105
+ await rm3(agentRoot, { recursive: true, force: true });
2026
4106
  }
2027
4107
  async archiveSpace() {
2028
- await mkdir6(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
2029
- await writeFile2(
2030
- path5.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
4108
+ await mkdir9(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
4109
+ await writeFile4(
4110
+ path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
2031
4111
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
2032
4112
  `,
2033
4113
  { encoding: "utf8", mode: 384 }
2034
4114
  );
2035
4115
  }
2036
4116
  async restoreSpace() {
2037
- await rm2(path5.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
4117
+ await rm3(path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
2038
4118
  }
2039
4119
  async deleteSpace(options) {
2040
4120
  requireConfirmation(options);
2041
4121
  await this.unmount();
2042
- await rm2(this.descriptor.sharedVolume, { recursive: true, force: true });
4122
+ await rm3(this.descriptor.sharedVolume, { recursive: true, force: true });
2043
4123
  }
2044
4124
  async bootstrapAgentTemplates(templateDirectory) {
2045
- await mkdir6(this.descriptor.privateVolume, { recursive: true, mode: 448 });
4125
+ await mkdir9(this.descriptor.privateVolume, { recursive: true, mode: 448 });
2046
4126
  const bootstrapped = [];
2047
4127
  const skipped = [];
2048
4128
  const entries = new Set(
2049
- (await readdir3(templateDirectory, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => entry.name)
4129
+ (await readdir5(templateDirectory, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => entry.name)
2050
4130
  );
2051
4131
  for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
2052
4132
  if (!entries.has(filename)) continue;
2053
- const destination = path5.join(this.descriptor.privateVolume, filename);
4133
+ const destination = path10.join(this.descriptor.privateVolume, filename);
2054
4134
  try {
2055
4135
  await lstat2(destination);
2056
4136
  skipped.push(filename);
@@ -2058,7 +4138,7 @@ var WorkspaceVolumeLifecycle = class {
2058
4138
  if (!isMissingFile(error)) throw error;
2059
4139
  try {
2060
4140
  await copyFile2(
2061
- path5.join(templateDirectory, filename),
4141
+ path10.join(templateDirectory, filename),
2062
4142
  destination,
2063
4143
  constants3.COPYFILE_EXCL
2064
4144
  );
@@ -2079,7 +4159,7 @@ var WorkspaceVolumeLifecycle = class {
2079
4159
  this.descriptor.temporaryVolume,
2080
4160
  this.systemVolume
2081
4161
  ]) {
2082
- await mkdir6(volume, { recursive: true, mode: 448 });
4162
+ await mkdir9(volume, { recursive: true, mode: 448 });
2083
4163
  const info = await lstat2(volume);
2084
4164
  if (info.isSymbolicLink() || !info.isDirectory() || await realpath2(volume) !== volume) {
2085
4165
  throw new Error("Workspace volume must be a canonical real directory");
@@ -2121,6 +4201,20 @@ var RELAY_RECONNECT_JITTER = 0.2;
2121
4201
  var RELAY_FAST_RECONNECT_MS = 500;
2122
4202
  var CONNECTOR_SOCKET_REPLACED_CLOSE_CODE = 4001;
2123
4203
  var CONNECTOR_SOCKET_REVOKED_CLOSE_CODE = 4003;
4204
+ var SAFE_TRACE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
4205
+ var getBudaCodingRunSessionId = (body) => {
4206
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
4207
+ const record = body;
4208
+ for (const key of ["systemEnv", "env"]) {
4209
+ const env = record[key];
4210
+ if (!env || typeof env !== "object" || Array.isArray(env)) continue;
4211
+ const value = env.BUDA_SESSION_ID;
4212
+ if (typeof value !== "string") continue;
4213
+ const normalized = value.trim();
4214
+ if (SAFE_TRACE_ID_PATTERN.test(normalized)) return normalized;
4215
+ }
4216
+ return void 0;
4217
+ };
2124
4218
  var shouldReconnectConnectorRelay = (closeCode) => closeCode !== CONNECTOR_SOCKET_REPLACED_CLOSE_CODE && closeCode !== CONNECTOR_SOCKET_REVOKED_CLOSE_CODE;
2125
4219
  var getConnectorRelayReconnectBaseDelay = (hadOpenConnection, backoffMs) => hadOpenConnection ? RELAY_FAST_RECONNECT_MS : backoffMs;
2126
4220
  var formatConnectorRelayError = (error, depth = 0) => {
@@ -2149,6 +4243,8 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2149
4243
  let reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
2150
4244
  let hadOpenConnection = false;
2151
4245
  let activeRelayProtocol = relayProtocol;
4246
+ let currentInflight = /* @__PURE__ */ new Map();
4247
+ let activeTerminalTunnels = 0;
2152
4248
  const replayGuard = new ConnectorExecutionReplayGuard();
2153
4249
  const clearTimers = () => {
2154
4250
  if (pingTimer) {
@@ -2196,12 +4292,13 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2196
4292
  if (token) headers["X-Buda-Connector-Token"] = token;
2197
4293
  headers["X-Buda-Connector-Id"] = config.connectorId;
2198
4294
  if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
2199
- const ws = new WebSocket(relayUrl, {
4295
+ const ws = new WebSocket2(relayUrl, {
2200
4296
  headers: Object.keys(headers).length > 0 ? headers : void 0,
2201
4297
  handshakeTimeout: RELAY_HANDSHAKE_TIMEOUT_MS
2202
4298
  });
2203
4299
  socket = ws;
2204
4300
  const inflight = /* @__PURE__ */ new Map();
4301
+ currentInflight = inflight;
2205
4302
  let detached = false;
2206
4303
  const detach = () => {
2207
4304
  if (detached) return;
@@ -2215,7 +4312,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2215
4312
  hadOpenConnection = true;
2216
4313
  reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
2217
4314
  pingTimer = setInterval(() => {
2218
- if (ws.readyState !== WebSocket.OPEN) return;
4315
+ if (ws.readyState !== WebSocket2.OPEN) return;
2219
4316
  try {
2220
4317
  ws.ping();
2221
4318
  } catch (error) {
@@ -2245,7 +4342,17 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2245
4342
  config.connectorId,
2246
4343
  activeRelayProtocol,
2247
4344
  replayGuard,
2248
- workspaceRegistry
4345
+ workspaceRegistry,
4346
+ config.serverUrl,
4347
+ bunnyDaemon.baseUrl,
4348
+ {
4349
+ onTerminalOpen: () => {
4350
+ activeTerminalTunnels += 1;
4351
+ },
4352
+ onTerminalClose: () => {
4353
+ activeTerminalTunnels = Math.max(0, activeTerminalTunnels - 1);
4354
+ }
4355
+ }
2249
4356
  );
2250
4357
  });
2251
4358
  ws.on("close", (code) => {
@@ -2277,6 +4384,12 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2277
4384
  scheduleReconnect("connect-throw");
2278
4385
  }
2279
4386
  return {
4387
+ isIdle() {
4388
+ return currentInflight.size === 0 && activeTerminalTunnels === 0;
4389
+ },
4390
+ isConnected() {
4391
+ return socket?.readyState === WebSocket2.OPEN && !suspendedForAccessRevocation;
4392
+ },
2280
4393
  updateRelayProtocol(nextRelayProtocol) {
2281
4394
  const previousKeyId = activeRelayProtocol?.keyId;
2282
4395
  activeRelayProtocol = nextRelayProtocol;
@@ -2308,8 +4421,8 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2308
4421
  }
2309
4422
  };
2310
4423
  }
2311
- async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry) {
2312
- if (!socket || socket.readyState !== WebSocket.OPEN) return;
4424
+ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity) {
4425
+ if (!socket || socket.readyState !== WebSocket2.OPEN) return;
2313
4426
  let message;
2314
4427
  try {
2315
4428
  message = JSON.parse(raw);
@@ -2326,6 +4439,54 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2326
4439
  return;
2327
4440
  }
2328
4441
  if (message.type === "event") {
4442
+ if (message.event.channel === "terminal-open") {
4443
+ try {
4444
+ if (!workspaceRegistry || !serverUrl || !localDaemonBaseUrl) {
4445
+ throw new ConnectorRelayProtocolError("PROTOCOL_UNSUPPORTED");
4446
+ }
4447
+ const identity = verifyConnectorExecutionIdentity({
4448
+ protocolVersion: message.event.payload.protocolVersion,
4449
+ executionIdentity: message.event.payload.executionIdentity,
4450
+ request: message.event.payload.request,
4451
+ protocol: relayProtocol,
4452
+ expectedConnectorId,
4453
+ replayGuard
4454
+ });
4455
+ const terminal = parseConnectorTerminalRequest(message.event.payload.request);
4456
+ if (terminal.sessionId !== message.event.sessionId) {
4457
+ throw new ConnectorRelayProtocolError("IDENTITY_INVALID");
4458
+ }
4459
+ const descriptor = await workspaceRegistry.resolve(identity.spaceId, identity.agentId);
4460
+ if (descriptor.spaceId !== identity.spaceId || descriptor.agentId !== identity.agentId) {
4461
+ throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
4462
+ }
4463
+ const mounts = await new WorkspaceVolumeLifecycle(descriptor).mount();
4464
+ const localToken = registerLocalTerminal({
4465
+ workspaceKey: descriptor.workspaceKey,
4466
+ tabId: terminal.tabId,
4467
+ cwd: mounts.agent,
4468
+ cols: terminal.cols,
4469
+ rows: terminal.rows
4470
+ });
4471
+ await proxyConnectorTerminalTunnel({
4472
+ serverUrl,
4473
+ connectorId: expectedConnectorId,
4474
+ sessionId: terminal.sessionId,
4475
+ cloudToken: terminal.token,
4476
+ localBaseUrl: localDaemonBaseUrl,
4477
+ localToken,
4478
+ workspaceKey: descriptor.workspaceKey,
4479
+ onActivityStart: activity?.onTerminalOpen,
4480
+ onActivityEnd: activity?.onTerminalClose
4481
+ });
4482
+ } catch (error) {
4483
+ await writeLog(
4484
+ "warn",
4485
+ `terminal open rejected: ${error instanceof Error ? error.message : String(error)}`
4486
+ );
4487
+ }
4488
+ return;
4489
+ }
2329
4490
  await writeLog("warn", "relay rejected unsigned browser input event");
2330
4491
  return;
2331
4492
  }
@@ -2351,6 +4512,15 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2351
4512
  }
2352
4513
  const controller = new AbortController();
2353
4514
  inflight.set(message.requestId, controller);
4515
+ const codingRunStartedAt = message.request.path === "/api/coding/run" ? Date.now() : void 0;
4516
+ const codingRunTrace = codingRunStartedAt !== void 0 && executionIdentity ? {
4517
+ requestId: message.requestId,
4518
+ connectorId: executionIdentity.connectorId,
4519
+ agentId: executionIdentity.agentId,
4520
+ runId: executionIdentity.runId,
4521
+ budaSessionId: getBudaCodingRunSessionId(message.request.body) ?? null
4522
+ } : void 0;
4523
+ let codingRunStatus = null;
2354
4524
  try {
2355
4525
  const requestPath = message.request.path;
2356
4526
  const isBrowserRequest = requestPath.startsWith("/browser/") || requestPath === "/browser";
@@ -2411,6 +4581,14 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2411
4581
  signal: controller.signal
2412
4582
  });
2413
4583
  }
4584
+ if (codingRunTrace && codingRunStartedAt !== void 0) {
4585
+ codingRunStatus = response.status;
4586
+ await writeLog("info", "relay request coding run started", {
4587
+ ...codingRunTrace,
4588
+ status: codingRunStatus,
4589
+ durationMs: Date.now() - codingRunStartedAt
4590
+ });
4591
+ }
2414
4592
  sendRelayMessage(socket, {
2415
4593
  type: "start",
2416
4594
  requestId: message.requestId,
@@ -2453,7 +4631,21 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
2453
4631
  type: "end",
2454
4632
  requestId: message.requestId
2455
4633
  });
4634
+ if (codingRunTrace && codingRunStartedAt !== void 0) {
4635
+ await writeLog("info", "relay request coding run completed", {
4636
+ ...codingRunTrace,
4637
+ status: codingRunStatus,
4638
+ durationMs: Date.now() - codingRunStartedAt
4639
+ });
4640
+ }
2456
4641
  } catch (error) {
4642
+ if (codingRunTrace && codingRunStartedAt !== void 0) {
4643
+ await writeLog("error", "relay request coding run error", {
4644
+ ...codingRunTrace,
4645
+ status: codingRunStatus,
4646
+ durationMs: Date.now() - codingRunStartedAt
4647
+ });
4648
+ }
2457
4649
  sendRelayError(socket, message.requestId, error);
2458
4650
  } finally {
2459
4651
  inflight.delete(message.requestId);
@@ -2478,12 +4670,12 @@ var normalizeWorkspaceRequest = (request, mounts) => {
2478
4670
  }
2479
4671
  const body = request.body;
2480
4672
  const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
2481
- const normalized = path6.posix.normalize(cwd);
4673
+ const normalized = path11.posix.normalize(cwd);
2482
4674
  const [mount, relative] = normalized === "/agent" || normalized.startsWith("/agent/") ? [mounts.agent, normalized.slice("/agent".length)] : normalized === "/space" || normalized.startsWith("/space/") ? [mounts.space, normalized.slice("/space".length)] : [];
2483
4675
  if (!mount || relative === void 0) {
2484
4676
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
2485
4677
  }
2486
- return { ...request, body: { ...body, cwd: path6.join(mount, relative) } };
4678
+ return { ...request, body: { ...body, cwd: path11.join(mount, relative) } };
2487
4679
  };
2488
4680
  function sendRelayError(socket, requestId, error) {
2489
4681
  sendRelayMessage(socket, {
@@ -2494,7 +4686,7 @@ function sendRelayError(socket, requestId, error) {
2494
4686
  });
2495
4687
  }
2496
4688
  function sendRelayMessage(socket, message) {
2497
- if (socket.readyState !== WebSocket.OPEN) return;
4689
+ if (socket.readyState !== WebSocket2.OPEN) return;
2498
4690
  socket.send(JSON.stringify(message));
2499
4691
  }
2500
4692
  function headersToRecord(headers) {
@@ -2514,14 +4706,14 @@ function getHeaderValue(headers, name) {
2514
4706
  }
2515
4707
 
2516
4708
  // src/workdir-layout.ts
2517
- import { lstat as lstat3, mkdir as mkdir7, symlink as symlink3, unlink as unlink4 } from "fs/promises";
2518
- import path7 from "path";
4709
+ import { lstat as lstat3, mkdir as mkdir10, symlink as symlink3, unlink as unlink6 } from "fs/promises";
4710
+ import path12 from "path";
2519
4711
  async function provisionWorkdirLayout(workdirRoot) {
2520
4712
  const manifest = await readBundledManifest();
2521
- await mkdir7(workdirRoot, { recursive: true });
2522
- await mkdir7(path7.join(workdirRoot, "system", ".agents"), { recursive: true });
2523
- const systemSkillsLink = path7.join(workdirRoot, "system", ".agents", "skills");
2524
- const systemScriptsLink = path7.join(workdirRoot, "system", "scripts");
4713
+ await mkdir10(workdirRoot, { recursive: true });
4714
+ await mkdir10(path12.join(workdirRoot, "system", ".agents"), { recursive: true });
4715
+ const systemSkillsLink = path12.join(workdirRoot, "system", ".agents", "skills");
4716
+ const systemScriptsLink = path12.join(workdirRoot, "system", "scripts");
2525
4717
  await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
2526
4718
  await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
2527
4719
  await writeLog("info", "workdir layout provisioned", {
@@ -2541,7 +4733,7 @@ async function ensureSymlink(target, linkPath) {
2541
4733
  try {
2542
4734
  const existing = await lstat3(linkPath);
2543
4735
  if (existing.isSymbolicLink()) {
2544
- await unlink4(linkPath);
4736
+ await unlink6(linkPath);
2545
4737
  } else if (existing.isDirectory()) {
2546
4738
  await writeLog(
2547
4739
  "warn",
@@ -2549,28 +4741,28 @@ async function ensureSymlink(target, linkPath) {
2549
4741
  );
2550
4742
  return;
2551
4743
  } else {
2552
- await unlink4(linkPath);
4744
+ await unlink6(linkPath);
2553
4745
  }
2554
4746
  } catch {
2555
4747
  }
2556
- await mkdir7(path7.dirname(linkPath), { recursive: true });
4748
+ await mkdir10(path12.dirname(linkPath), { recursive: true });
2557
4749
  await symlink3(target, linkPath, "dir");
2558
4750
  }
2559
4751
 
2560
4752
  // src/workspace-registry.ts
2561
4753
  import { constants as constants4 } from "fs";
2562
4754
  import {
2563
- chmod as chmod3,
4755
+ chmod as chmod5,
2564
4756
  lstat as lstat4,
2565
- mkdir as mkdir8,
4757
+ mkdir as mkdir11,
2566
4758
  open as open3,
2567
- readFile as readFile6,
4759
+ readFile as readFile9,
2568
4760
  realpath as realpath3,
2569
- rename as rename3,
4761
+ rename as rename6,
2570
4762
  statfs,
2571
- unlink as unlink5
4763
+ unlink as unlink7
2572
4764
  } from "fs/promises";
2573
- import path8 from "path";
4765
+ import path13 from "path";
2574
4766
  var REGISTRY_VERSION = 1;
2575
4767
  var REGISTRY_DIRECTORY = ".buda";
2576
4768
  var REGISTRY_FILENAME = "workspace-registry.json";
@@ -2591,16 +4783,16 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
2591
4783
  this.document = document;
2592
4784
  }
2593
4785
  static async open(storageRoot) {
2594
- await mkdir8(storageRoot, { recursive: true, mode: 448 });
4786
+ await mkdir11(storageRoot, { recursive: true, mode: 448 });
2595
4787
  await rejectUnsafeFilesystem(storageRoot);
2596
4788
  const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
2597
- const registryDirectory = path8.join(canonicalRoot, REGISTRY_DIRECTORY);
4789
+ const registryDirectory = path13.join(canonicalRoot, REGISTRY_DIRECTORY);
2598
4790
  await ensurePrivateDirectory2(registryDirectory);
2599
- const registryPath = path8.join(registryDirectory, REGISTRY_FILENAME);
4791
+ const registryPath = path13.join(registryDirectory, REGISTRY_FILENAME);
2600
4792
  const document = await withExclusiveFileLock(registryPath, async () => {
2601
4793
  let current;
2602
4794
  try {
2603
- current = parseRegistry(await readFile6(registryPath, "utf8"));
4795
+ current = parseRegistry(await readFile9(registryPath, "utf8"));
2604
4796
  } catch (error) {
2605
4797
  if (isMissingFile2(error)) {
2606
4798
  current = { version: REGISTRY_VERSION, storageRoot: canonicalRoot, workspaces: {} };
@@ -2614,11 +4806,11 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
2614
4806
  if (current.storageRoot !== canonicalRoot) {
2615
4807
  throw corrupt2("Registry storage root does not match the selected storage root");
2616
4808
  }
2617
- await chmod3(registryPath, 384);
4809
+ await chmod5(registryPath, 384);
2618
4810
  await reconcileDocument(current, canonicalRoot);
2619
4811
  return current;
2620
4812
  });
2621
- await chmod3(registryPath, 384);
4813
+ await chmod5(registryPath, 384);
2622
4814
  return new _LocalWorkspaceRegistry(registryPath, canonicalRoot, document);
2623
4815
  }
2624
4816
  async resolve(spaceId, agentId) {
@@ -2665,23 +4857,23 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
2665
4857
  };
2666
4858
  var readRegistryDocument = async (registryPath) => {
2667
4859
  try {
2668
- return parseRegistry(await readFile6(registryPath, "utf8"));
4860
+ return parseRegistry(await readFile9(registryPath, "utf8"));
2669
4861
  } catch (error) {
2670
4862
  if (error instanceof WorkspaceRegistryError) throw error;
2671
4863
  throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
2672
4864
  }
2673
4865
  };
2674
4866
  var deriveWorkspace = (storageRoot, spaceId, agentId) => {
2675
- const spaceRoot = path8.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
2676
- const agentRoot = path8.join(spaceRoot, "agents", agentId);
4867
+ const spaceRoot = path13.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
4868
+ const agentRoot = path13.join(spaceRoot, "agents", agentId);
2677
4869
  return {
2678
4870
  workspaceKey: `${spaceId}:${agentId}`,
2679
4871
  spaceId,
2680
4872
  agentId,
2681
- privateVolume: path8.join(agentRoot, "workspace"),
2682
- sharedVolume: path8.join(spaceRoot, "shared"),
2683
- stateVolume: path8.join(agentRoot, "state"),
2684
- temporaryVolume: path8.join(agentRoot, "tmp")
4873
+ privateVolume: path13.join(agentRoot, "workspace"),
4874
+ sharedVolume: path13.join(spaceRoot, "shared"),
4875
+ stateVolume: path13.join(agentRoot, "state"),
4876
+ temporaryVolume: path13.join(agentRoot, "tmp")
2685
4877
  };
2686
4878
  };
2687
4879
  var parseRegistry = (raw) => {
@@ -2763,12 +4955,12 @@ var assertNoCollision = (workspaces, candidate) => {
2763
4955
  }
2764
4956
  };
2765
4957
  var ensurePrivateDirectory2 = async (directory) => {
2766
- await mkdir8(directory, { recursive: true, mode: 448 });
4958
+ await mkdir11(directory, { recursive: true, mode: 448 });
2767
4959
  const info = await lstat4(directory);
2768
4960
  if (info.isSymbolicLink() || !info.isDirectory()) {
2769
4961
  throw corrupt2("Workspace registry path is not a real directory");
2770
4962
  }
2771
- await chmod3(directory, 448);
4963
+ await chmod5(directory, 448);
2772
4964
  };
2773
4965
  var canonicalDirectory = async (directory, label) => {
2774
4966
  const info = await lstat4(directory);
@@ -2802,10 +4994,10 @@ var writeRegistryAtomically = async (registryPath, document) => {
2802
4994
  await handle.close();
2803
4995
  }
2804
4996
  try {
2805
- await rename3(temporaryPath, registryPath);
2806
- await chmod3(registryPath, 384);
4997
+ await rename6(temporaryPath, registryPath);
4998
+ await chmod5(registryPath, 384);
2807
4999
  } catch (error) {
2808
- await unlink5(temporaryPath).catch(() => void 0);
5000
+ await unlink7(temporaryPath).catch(() => void 0);
2809
5001
  throw error;
2810
5002
  }
2811
5003
  };
@@ -2828,8 +5020,8 @@ var assertContained = (root, candidate) => {
2828
5020
  if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
2829
5021
  };
2830
5022
  var isPathInside2 = (parent, child) => {
2831
- const relative = path8.relative(parent, child);
2832
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
5023
+ const relative = path13.relative(parent, child);
5024
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path13.sep}`) && !path13.isAbsolute(relative);
2833
5025
  };
2834
5026
  var toDescriptor = (workspace) => ({
2835
5027
  workspaceKey: workspace.workspaceKey,
@@ -2859,7 +5051,7 @@ var corrupt2 = (message) => new WorkspaceRegistryError("REGISTRY_CORRUPT", messa
2859
5051
  var collision = (message) => new WorkspaceRegistryError("WORKSPACE_COLLISION", message);
2860
5052
 
2861
5053
  // src/onboard.ts
2862
- async function runOnboard(launchConfig) {
5054
+ async function runOnboard(launchConfig, options = {}) {
2863
5055
  intro("Welcome to Buda Connector");
2864
5056
  const existing = await readDeviceConfig(launchConfig);
2865
5057
  let relayProtocol;
@@ -2905,10 +5097,12 @@ async function runOnboard(launchConfig) {
2905
5097
  await provisionWorkdirLayout(config2.workdirRoot);
2906
5098
  try {
2907
5099
  const registration = await registerConnector(config2, launchConfig, void 0, {
2908
- recoverMissingProfile: true
5100
+ recoverMissingProfile: false
2909
5101
  });
2910
5102
  config2.connectorId = registration.connectorId;
5103
+ config2.spaceId = registration.spaceId ?? config2.spaceId;
2911
5104
  relayProtocol = registration.relayProtocol;
5105
+ if (!config2.spaceId) throw new Error("Registration did not resolve a Space");
2912
5106
  await writeDeviceConfig(config2);
2913
5107
  } catch (error) {
2914
5108
  s2.stop("Registration failed");
@@ -2952,23 +5146,33 @@ async function runOnboard(launchConfig) {
2952
5146
  launchConfig
2953
5147
  );
2954
5148
  s.stop("Daemon running");
2955
- await writeLog("info", `server ${config.serverUrl}`);
2956
- await writeLog("info", `config ${paths.configPath}`);
2957
- await writeLog("info", `workdir root ${config.workdirRoot}`);
2958
- await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
2959
- await writeLog("info", `log file ${paths.logPath}`);
2960
5149
  note(
2961
5150
  [
2962
5151
  `Server: ${config.serverUrl}`,
2963
5152
  `Bunny daemon: ${bunnyDaemon.baseUrl}`,
2964
5153
  `Relay: connected`,
2965
- `Logs: ${paths.logPath}`
5154
+ `Logs: ${getDatedConnectorLogPath(paths.logPath)}`
2966
5155
  ].join("\n"),
2967
5156
  "Status"
2968
5157
  );
2969
5158
  outro("Buda Connector is running. Press Ctrl+C to stop.");
5159
+ const dashboard = startConnectorConsoleDashboard({ plain: options.plain });
5160
+ updateConnectorDashboardStatus({
5161
+ agent: "Ready",
5162
+ relay: "Connected",
5163
+ server: config.serverUrl
5164
+ });
5165
+ await writeLog("info", `server ${config.serverUrl}`);
5166
+ await writeLog("info", `config ${paths.configPath}`);
5167
+ await writeLog("info", `workdir root ${config.workdirRoot}`);
5168
+ await writeLog("info", `bunny daemon ${bunnyDaemon.baseUrl}`);
5169
+ await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
2970
5170
  const interval = setInterval(() => {
2971
- heartbeatConnector(config, launchConfig, bunnyDaemon).catch((error) => {
5171
+ heartbeatConnector(config, launchConfig, bunnyDaemon).then(() => {
5172
+ updateConnectorDashboardStatus({
5173
+ heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
5174
+ });
5175
+ }).catch((error) => {
2972
5176
  void writeLog(
2973
5177
  "warn",
2974
5178
  `heartbeat failed: ${error instanceof Error ? error.message : String(error)}`
@@ -2976,6 +5180,7 @@ async function runOnboard(launchConfig) {
2976
5180
  });
2977
5181
  }, HEARTBEAT_INTERVAL_MS);
2978
5182
  const shutdown = async () => {
5183
+ dashboard?.stop();
2979
5184
  clearInterval(interval);
2980
5185
  relay.stop();
2981
5186
  await unregisterConnector(config, launchConfig);
@@ -2987,8 +5192,31 @@ async function runOnboard(launchConfig) {
2987
5192
  }
2988
5193
 
2989
5194
  // src/runtime.ts
2990
- import { mkdir as mkdir9 } from "fs/promises";
2991
- import path9 from "path";
5195
+ import { mkdir as mkdir12 } from "fs/promises";
5196
+ import path14 from "path";
5197
+ var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
5198
+ var RECOVERED_DAEMON_STOP_POLL_MS = 100;
5199
+ var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
5200
+ async function stopRecoveredIdleDaemon(config, configRoot) {
5201
+ const stopStatus = await requestDaemonControl(config, "stop-if-idle", configRoot).catch(
5202
+ () => null
5203
+ );
5204
+ if (!stopStatus) return;
5205
+ if (!stopStatus.idle) {
5206
+ throw new Error(
5207
+ `The recovered Connector ${config.connectorId} is still running an Agent task and cannot be replaced yet.`
5208
+ );
5209
+ }
5210
+ const deadline = Date.now() + RECOVERED_DAEMON_STOP_TIMEOUT_MS;
5211
+ while (Date.now() < deadline) {
5212
+ const status = await requestDaemonControl(config, "status", configRoot, {
5213
+ timeoutMs: 500
5214
+ }).catch(() => null);
5215
+ if (!status) return;
5216
+ await wait3(RECOVERED_DAEMON_STOP_POLL_MS);
5217
+ }
5218
+ throw new Error(`Recovered Connector ${config.connectorId} did not stop cleanly.`);
5219
+ }
2992
5220
  async function runNewConnector(launchConfig) {
2993
5221
  const existing = await readDeviceConfig(launchConfig);
2994
5222
  const config = await ensureDeviceConfig(launchConfig);
@@ -2997,27 +5225,41 @@ async function runNewConnector(launchConfig) {
2997
5225
  await provisionWorkdirLayout(config.workdirRoot);
2998
5226
  await LocalWorkspaceRegistry.open(config.workdirRoot);
2999
5227
  const registration = await registerConnector(config, launchConfig, void 0, {
3000
- recoverMissingProfile: !existing
5228
+ recoverMissingProfile: shouldRecoverMissingProfile(Boolean(existing), launchConfig)
3001
5229
  });
3002
5230
  config.connectorId = registration.connectorId;
5231
+ config.spaceId = registration.spaceId ?? config.spaceId;
3003
5232
  await writeDeviceConfig(config);
3004
5233
  await writeLog("info", `wrote ${paths.configPath}`);
3005
5234
  await writeLog("info", `workdir root ${config.workdirRoot}`);
3006
- await writeLog("info", `log file ${paths.logPath}`);
5235
+ await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
3007
5236
  }
3008
- async function runConnectorDaemon(launchConfig) {
3009
- const existing = await readDeviceConfig(launchConfig);
3010
- const config = await ensureDeviceConfig(launchConfig);
3011
- const paths = getConnectorPaths(config.serverUrl);
3012
- const daemonLockTarget = getConnectorDaemonLockTarget(config.serverUrl, config.connectorId);
3013
- await mkdir9(path9.dirname(daemonLockTarget), { recursive: true, mode: 448 });
5237
+ async function runConnectorDaemon(launchConfig, options = {}) {
5238
+ const configRoot = options.configRoot ?? process.env.BUDA_CONNECTOR_CONFIG_ROOT;
5239
+ const existing = await readDeviceConfig(launchConfig, configRoot);
5240
+ const config = await ensureDeviceConfig(launchConfig, configRoot);
5241
+ setConnectorLogContext({
5242
+ configRoot,
5243
+ connectorId: config.connectorId,
5244
+ serverUrl: config.serverUrl
5245
+ });
5246
+ const paths = getConnectorPaths(config.serverUrl, configRoot);
5247
+ const daemonLockTarget = getConnectorDaemonLockTarget(
5248
+ config.serverUrl,
5249
+ config.connectorId,
5250
+ configRoot
5251
+ );
5252
+ await mkdir12(path14.dirname(daemonLockTarget), { recursive: true, mode: 448 });
3014
5253
  let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
3015
5254
  throw new Error(
3016
5255
  `Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
3017
5256
  );
3018
5257
  });
5258
+ const restoreConsole = options.captureConsole ? installConnectorConsoleCapture() : void 0;
3019
5259
  let bunnyDaemon;
3020
5260
  let relayProtocol;
5261
+ let control;
5262
+ let lastHeartbeatAt;
3021
5263
  try {
3022
5264
  await migrateLegacyWorkspaceRoot(config.workdirRoot);
3023
5265
  await provisionWorkdirLayout(config.workdirRoot);
@@ -3025,15 +5267,22 @@ async function runConnectorDaemon(launchConfig) {
3025
5267
  bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
3026
5268
  const activeBunnyDaemon = bunnyDaemon;
3027
5269
  const registration = await registerConnector(config, launchConfig, activeBunnyDaemon, {
3028
- recoverMissingProfile: !existing
5270
+ recoverMissingProfile: shouldRecoverMissingProfile(Boolean(existing), launchConfig)
3029
5271
  });
3030
5272
  const connectorIdChanged = config.connectorId !== registration.connectorId;
3031
5273
  if (connectorIdChanged) {
5274
+ const registeredConfig = {
5275
+ ...config,
5276
+ connectorId: registration.connectorId,
5277
+ spaceId: registration.spaceId ?? config.spaceId
5278
+ };
5279
+ await stopRecoveredIdleDaemon(registeredConfig, configRoot);
3032
5280
  const registeredLockTarget = getConnectorDaemonLockTarget(
3033
5281
  config.serverUrl,
3034
- registration.connectorId
5282
+ registration.connectorId,
5283
+ configRoot
3035
5284
  );
3036
- await mkdir9(path9.dirname(registeredLockTarget), { recursive: true, mode: 448 });
5285
+ await mkdir12(path14.dirname(registeredLockTarget), { recursive: true, mode: 448 });
3037
5286
  const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
3038
5287
  timeoutMs: 0
3039
5288
  }).catch(() => {
@@ -3045,8 +5294,15 @@ async function runConnectorDaemon(launchConfig) {
3045
5294
  daemonLock = registeredLock;
3046
5295
  }
3047
5296
  config.connectorId = registration.connectorId;
5297
+ setConnectorLogContext({ connectorId: config.connectorId });
5298
+ config.spaceId = registration.spaceId ?? config.spaceId;
3048
5299
  relayProtocol = registration.relayProtocol;
3049
- if (!existing || connectorIdChanged) await writeDeviceConfig(config);
5300
+ if (!config.spaceId) {
5301
+ throw new Error("Connector registration did not return a canonical spaceId");
5302
+ }
5303
+ if (!existing || connectorIdChanged || existing.spaceId !== config.spaceId) {
5304
+ await writeDeviceConfig(config, configRoot);
5305
+ }
3050
5306
  const relay = startConnectorRelay(
3051
5307
  config,
3052
5308
  activeBunnyDaemon,
@@ -3057,12 +5313,16 @@ async function runConnectorDaemon(launchConfig) {
3057
5313
  await writeLog("info", `server ${config.serverUrl}`);
3058
5314
  await writeLog("info", `config ${paths.configPath}`);
3059
5315
  await writeLog("info", `workdir root ${config.workdirRoot}`);
3060
- await writeLog("info", `log file ${paths.logPath}`);
5316
+ await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
3061
5317
  await writeLog("info", `bunny daemon ${activeBunnyDaemon.baseUrl}`);
3062
5318
  const interval = setInterval(() => {
3063
5319
  heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
5320
+ lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
3064
5321
  relay.updateRelayProtocol(nextRelayProtocol);
3065
5322
  relay.resumeAfterAccessRestore();
5323
+ updateConnectorDashboardStatus({
5324
+ heartbeat: `OK ${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}`
5325
+ });
3066
5326
  }).catch((error) => {
3067
5327
  void writeLog(
3068
5328
  "warn",
@@ -3081,23 +5341,37 @@ async function runConnectorDaemon(launchConfig) {
3081
5341
  await unregisterConnector(config, launchConfig);
3082
5342
  await stopEmbeddedBunnyDaemon(activeBunnyDaemon);
3083
5343
  } finally {
5344
+ await control?.close();
3084
5345
  await daemonLock.release();
3085
- process.exit(0);
5346
+ await restoreConsole?.();
5347
+ if (options.exitOnShutdown !== false) process.exit(0);
3086
5348
  }
3087
5349
  };
5350
+ control = await startDaemonControlServer({
5351
+ config,
5352
+ configRoot,
5353
+ getLastHeartbeatAt: () => lastHeartbeatAt,
5354
+ isIdle: () => relay.isIdle(),
5355
+ isRelayConnected: () => relay.isConnected(),
5356
+ mode: options.mode ?? "persistent",
5357
+ onStop: shutdown,
5358
+ version: options.version
5359
+ });
3088
5360
  process.once("SIGINT", () => void shutdown());
3089
5361
  process.once("SIGTERM", () => void shutdown());
3090
5362
  } catch (error) {
3091
5363
  if (bunnyDaemon) await stopEmbeddedBunnyDaemon(bunnyDaemon);
3092
5364
  await daemonLock.release();
5365
+ await restoreConsole?.();
3093
5366
  throw error;
3094
5367
  }
3095
5368
  }
5369
+ var shouldRecoverMissingProfile = (hasExistingProfile, launchConfig) => !hasExistingProfile && Boolean(getConnectorToken(launchConfig)) && !launchConfig?.oauthToken?.trim();
3096
5370
 
3097
5371
  // src/cli.ts
3098
5372
  function printHelp() {
3099
5373
  console.log(
3100
- "Help: buda-connector [onboard | new | dev | daemon] [--token <token>] [--server-url <url>] [--workdir <path>] [--launch-config-stdin]\n pnpm --filter @buda-ai/connector dev\n\nCommands (default: onboard in TTY, daemon otherwise):\n onboard Interactive first-time setup + start daemon\n new Create/register a new connector identity\n dev Run the connector daemon in development\n daemon Run the connector daemon for sidecar/service usage"
5374
+ "Help: buda-connector [onboard | new | dev | daemon] [--token <token>] [--server-url <url>] [--workdir <path>] [--plain] [--launch-config-stdin]\n pnpm --filter @buda-ai/connector dev\n\nCommands (default: onboard in TTY, daemon otherwise):\n onboard Interactive first-time setup + start daemon\n new Create/register a new connector identity\n dev Run the connector daemon in development\n daemon Run the connector daemon for sidecar/service usage\n\nOptions:\n --plain Disable the interactive dashboard and print ordinary logs"
3101
5375
  );
3102
5376
  }
3103
5377
  function readFlagValue(name) {
@@ -3110,6 +5384,7 @@ function readCliLaunchConfig() {
3110
5384
  const bunnyDaemonPort = readFlagValue("--bunny-daemon-port");
3111
5385
  return {
3112
5386
  serverUrl: readFlagValue("--server-url"),
5387
+ spaceId: readFlagValue("--space-id"),
3113
5388
  workdirRoot: readFlagValue("--workdir"),
3114
5389
  oauthToken: readFlagValue("--oauth-token"),
3115
5390
  token: readFlagValue("--token"),
@@ -3157,25 +5432,49 @@ async function main() {
3157
5432
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
3158
5433
  const defaultCommand = isTTY ? "onboard" : "daemon";
3159
5434
  const command = !rawCommand || rawCommand.startsWith("-") ? defaultCommand : rawCommand;
5435
+ const configRoot = readFlagValue("--config-root");
5436
+ if (configRoot) process.env.BUDA_CONNECTOR_CONFIG_ROOT = configRoot;
3160
5437
  process.on("unhandledRejection", (reason) => {
3161
5438
  console.error("[buda-connector] unhandledRejection:", reason);
3162
5439
  });
3163
5440
  process.on("uncaughtException", (error) => {
3164
5441
  console.error("[buda-connector] uncaughtException:", error);
3165
5442
  });
3166
- const launchConfig = await readLaunchConfig();
5443
+ const launchConfig = process.argv.includes("--internal-daemon") ? getInternalLaunchConfig() : await readLaunchConfig();
5444
+ const plain = process.argv.includes("--plain");
3167
5445
  if (command === "new") {
3168
5446
  await runNewConnector(launchConfig);
3169
5447
  return;
3170
5448
  }
3171
5449
  if (command === "onboard") {
3172
- await runOnboard(launchConfig);
5450
+ await runOnboard(launchConfig, { plain });
3173
5451
  return;
3174
5452
  }
3175
- if (command === "dev" || command === "daemon") {
5453
+ if (command === "dev" || process.argv.includes("--foreground")) {
5454
+ startConnectorConsoleDashboard({ plain });
3176
5455
  await runConnectorDaemon(launchConfig);
3177
5456
  return;
3178
5457
  }
5458
+ if (command === "daemon" && process.argv.includes("--internal-daemon")) {
5459
+ await runConnectorDaemon(launchConfig, {
5460
+ captureConsole: true,
5461
+ mode: getDaemonModeFlag(),
5462
+ version: CONNECTOR_RUNTIME_VERSION
5463
+ });
5464
+ return;
5465
+ }
5466
+ if (command === "daemon" && process.argv.includes("--desktop-release")) {
5467
+ await releaseDesktopConnector(launchConfig);
5468
+ return;
5469
+ }
5470
+ if (command === "daemon" && process.argv.includes("--desktop")) {
5471
+ await runDesktopConnectorController(launchConfig);
5472
+ return;
5473
+ }
5474
+ if (command === "daemon") {
5475
+ await runConnectorDashboard(launchConfig, { plain });
5476
+ return;
5477
+ }
3179
5478
  printHelp();
3180
5479
  process.exit(1);
3181
5480
  }