@buda-ai/connector 0.1.7 → 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,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-MCKGQKYU.js";
3
2
 
4
3
  // src/config.ts
5
- import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
4
+ import { createHash as createHash2, randomBytes, randomUUID as randomUUID2 } from "crypto";
6
5
  import { chmod, mkdir, readFile as readFile2, rename, unlink as unlink2, writeFile } from "fs/promises";
7
6
  import { homedir, hostname, platform as platform2 } from "os";
8
7
  import path from "path";
9
8
 
10
9
  // src/exclusive-file-lock.ts
10
+ import { randomUUID } from "crypto";
11
11
  import { open, readFile, stat, unlink } from "fs/promises";
12
12
  var DEFAULT_TIMEOUT_MS = 1e4;
13
13
  var DEFAULT_STALE_MS = 12e4;
@@ -34,8 +34,11 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
34
34
  await wait(pollMs);
35
35
  continue;
36
36
  }
37
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, createdAt: Date.now() })}
38
- `);
37
+ const ownerToken = randomUUID();
38
+ await handle.writeFile(
39
+ `${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
40
+ `
41
+ );
39
42
  await handle.sync();
40
43
  let released = false;
41
44
  return {
@@ -43,7 +46,8 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
43
46
  if (released) return;
44
47
  released = true;
45
48
  await handle.close();
46
- 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);
47
51
  }
48
52
  };
49
53
  }
@@ -155,6 +159,17 @@ function getConnectorProfileKey(serverUrl, token) {
155
159
  const discriminator = token?.trim() ? getConnectorTokenFingerprint(token) : "legacy-no-token";
156
160
  return `${getServerOriginKey(serverUrl)}--${discriminator}`;
157
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
+ }
158
173
  function getConnectorPaths(serverUrl, configRoot = path.join(homedir(), ".buda")) {
159
174
  const scopedLegacyDir = path.join(configRoot, "servers", getServerOriginKey(serverUrl));
160
175
  return {
@@ -176,6 +191,17 @@ function getConnectorDaemonLockTarget(serverUrl, connectorId, configRoot = path.
176
191
  "connector-daemon"
177
192
  );
178
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
+ }
179
205
  function getServerUrl(launchConfig) {
180
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(/\/$/, "");
181
207
  }
@@ -208,15 +234,16 @@ function getBunnyDaemonPort(launchConfig) {
208
234
  function getConnectorToken(launchConfig) {
209
235
  return launchConfig?.token?.trim() || process.env.BUDA_CONNECTOR_TOKEN?.trim() || void 0;
210
236
  }
211
- function buildDeviceConfig(launchConfig, deviceId = `dvc_${randomUUID()}`) {
237
+ function buildDeviceConfig(launchConfig, deviceId = `dvc_${randomUUID2()}`) {
212
238
  const host = hostname();
213
239
  const token = getConnectorToken(launchConfig);
214
240
  return {
215
- connectorId: `cnr_${randomUUID()}`,
241
+ connectorId: `cnr_${randomUUID2()}`,
216
242
  deviceId,
217
243
  deviceName: host,
218
244
  hostLabel: `${host} (${platform2()})`,
219
245
  serverUrl: getServerUrl(launchConfig),
246
+ ...launchConfig?.spaceId?.trim() ? { spaceId: launchConfig.spaceId.trim() } : {},
220
247
  workdirRoot: getWorkdirRoot(launchConfig),
221
248
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
222
249
  deviceToken: randomBytes(32).toString("base64url"),
@@ -228,12 +255,18 @@ function isDeviceConfig(value) {
228
255
  const config = value;
229
256
  return typeof config.connectorId === "string" && typeof config.deviceId === "string" && typeof config.serverUrl === "string";
230
257
  }
258
+ function isCanonicalDeviceConfig(value) {
259
+ return isDeviceConfig(value) && typeof value.spaceId === "string" && value.spaceId.trim().length > 0;
260
+ }
231
261
  async function readJsonFile(configPath) {
232
262
  try {
233
263
  const raw = await readFile2(configPath, "utf8");
234
264
  return JSON.parse(raw);
235
- } catch {
236
- 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 });
237
270
  }
238
271
  }
239
272
  function migrateLegacyProfiles(values) {
@@ -250,36 +283,68 @@ function migrateLegacyProfiles(values) {
250
283
  async function loadDeviceConfigStore(configPath) {
251
284
  const parsed = await readJsonFile(configPath);
252
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
+ }
253
308
  if (candidate?.version === 3 && candidate.profiles && typeof candidate.profiles === "object") {
254
309
  return {
255
310
  store: {
256
- version: 3,
311
+ version: 4,
257
312
  ...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
258
- profiles: candidate.profiles
313
+ identities: {},
314
+ aliases: {}
259
315
  },
316
+ legacyProfiles: candidate.profiles,
260
317
  needsWrite: false
261
318
  };
262
319
  }
263
320
  if (candidate && (candidate.version === 1 || candidate.version === 2) && candidate.servers && typeof candidate.servers === "object") {
264
321
  return {
265
322
  store: {
266
- version: 3,
323
+ version: 4,
267
324
  ...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
268
- profiles: migrateLegacyProfiles(Object.values(candidate.servers))
325
+ identities: {},
326
+ aliases: {}
269
327
  },
270
- needsWrite: true
328
+ legacyProfiles: migrateLegacyProfiles(Object.values(candidate.servers)),
329
+ needsWrite: false
271
330
  };
272
331
  }
273
332
  if (isDeviceConfig(parsed)) {
274
333
  return {
275
334
  store: {
276
- version: 3,
277
- profiles: migrateLegacyProfiles([parsed])
335
+ version: 4,
336
+ identities: {},
337
+ aliases: {}
278
338
  },
279
- needsWrite: true
339
+ legacyProfiles: migrateLegacyProfiles([parsed]),
340
+ needsWrite: false
280
341
  };
281
342
  }
282
- return { store: { version: 3, profiles: {} }, needsWrite: false };
343
+ return {
344
+ store: { version: 4, identities: {}, aliases: {} },
345
+ legacyProfiles: {},
346
+ needsWrite: false
347
+ };
283
348
  }
284
349
  function normalizeStoredWorkdirRoot(config, paths) {
285
350
  const configuredRoot = config.workdirRoot?.trim();
@@ -297,7 +362,7 @@ function normalizeStoredWorkdirRoot(config, paths) {
297
362
  }
298
363
  async function writeDeviceConfigStore(configPath, store) {
299
364
  await mkdir(path.dirname(configPath), { recursive: true });
300
- const temporaryPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
365
+ const temporaryPath = `${configPath}.${process.pid}.${randomUUID2()}.tmp`;
301
366
  try {
302
367
  await writeFile(temporaryPath, `${JSON.stringify(store, null, 2)}
303
368
  `, { mode: 384 });
@@ -307,36 +372,70 @@ async function writeDeviceConfigStore(configPath, store) {
307
372
  await unlink2(temporaryPath).catch(() => void 0);
308
373
  }
309
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
+ }
310
401
  async function readDeviceConfig(launchConfig, configRoot) {
311
402
  const serverUrl = getServerUrl(launchConfig);
312
403
  const paths = getConnectorPaths(serverUrl, configRoot);
313
- const launchToken = getConnectorToken(launchConfig);
314
404
  await mkdir(paths.configDir, { recursive: true });
315
405
  return withExclusiveFileLock(paths.configPath, async () => {
316
- const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
317
- const profileKey = getConnectorProfileKey(serverUrl, launchToken);
318
- const storedConfig = store.profiles[profileKey];
319
- let config = isDeviceConfig(storedConfig) ? storedConfig : null;
320
- let normalizedWorkdirRoot = false;
321
- if (config) {
322
- const normalized = normalizeStoredWorkdirRoot(config, paths);
323
- config = normalized.config;
324
- normalizedWorkdirRoot = normalized.changed;
325
- store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
326
- }
327
- if (needsWrite || normalizedWorkdirRoot) {
328
- 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);
329
422
  }
330
- return config;
423
+ return [...unique.values()].sort(
424
+ (left, right) => left.createdAt.localeCompare(right.createdAt) || left.connectorId.localeCompare(right.connectorId)
425
+ );
331
426
  });
332
427
  }
333
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();
334
433
  const paths = getConnectorPaths(config.serverUrl, configRoot);
335
434
  await mkdir(config.workdirRoot, { recursive: true });
336
435
  await mkdir(paths.configDir, { recursive: true });
337
436
  await withExclusiveFileLock(paths.configPath, async () => {
338
437
  const { store } = await loadDeviceConfigStore(paths.configPath);
339
- store.profiles[getConnectorProfileKey(config.serverUrl, config.token)] = config;
438
+ putCanonicalDeviceConfig(store, { ...config, spaceId });
340
439
  await writeDeviceConfigStore(paths.configPath, store);
341
440
  });
342
441
  }
@@ -344,13 +443,12 @@ async function getOrCreateSystemDeviceId(configRoot, resolver = resolveSystemDev
344
443
  const paths = getConnectorPaths(DEFAULT_SERVER_URL, configRoot);
345
444
  await mkdir(paths.configDir, { recursive: true });
346
445
  return withExclusiveFileLock(paths.configPath, async () => {
347
- const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
446
+ const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
348
447
  if (store.systemDeviceId) {
349
- if (needsWrite) await writeDeviceConfigStore(paths.configPath, store);
350
448
  return store.systemDeviceId;
351
449
  }
352
- const existingDeviceId = Object.values(store.profiles).find(isDeviceConfig)?.deviceId;
353
- 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()}`;
354
452
  await writeDeviceConfigStore(paths.configPath, store);
355
453
  return store.systemDeviceId;
356
454
  });
@@ -362,33 +460,28 @@ async function createDeviceConfig(launchConfig, configRoot, resolver) {
362
460
  async function ensureDeviceConfig(launchConfig, configRoot, resolver = resolveSystemDeviceId) {
363
461
  const serverUrl = getServerUrl(launchConfig);
364
462
  const paths = getConnectorPaths(serverUrl, configRoot);
365
- const token = getConnectorToken(launchConfig);
366
- const profileKey = getConnectorProfileKey(serverUrl, token);
367
463
  await mkdir(paths.configDir, { recursive: true });
368
464
  const config = await withExclusiveFileLock(paths.configPath, async () => {
369
- const { store, needsWrite } = await loadDeviceConfigStore(paths.configPath);
370
- const storedConfig = store.profiles[profileKey];
371
- if (isDeviceConfig(storedConfig)) {
372
- const normalized = normalizeStoredWorkdirRoot(storedConfig, paths);
373
- if (needsWrite || normalized.changed) {
374
- store.profiles[profileKey] = normalized.config;
375
- await writeDeviceConfigStore(paths.configPath, store);
376
- }
377
- const existing = normalized.config;
378
- const overrideToken = getConnectorToken(launchConfig);
379
- return {
465
+ const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
466
+ const existing = findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths);
467
+ if (existing) {
468
+ const resolved = {
380
469
  ...existing,
381
470
  serverUrl,
382
471
  workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig),
383
- ...overrideToken ? { token: overrideToken } : {}
472
+ ...getConnectorToken(launchConfig) ? { token: getConnectorToken(launchConfig) } : {}
384
473
  };
474
+ if (isCanonicalDeviceConfig(resolved)) {
475
+ putCanonicalDeviceConfig(store, resolved);
476
+ await writeDeviceConfigStore(paths.configPath, store);
477
+ }
478
+ return resolved;
385
479
  }
386
- if (!store.systemDeviceId) {
387
- const existingDeviceId = Object.values(store.profiles).find(isDeviceConfig)?.deviceId;
388
- store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID()}`;
389
- }
390
- const created = buildDeviceConfig(launchConfig, store.systemDeviceId);
391
- 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);
392
485
  await writeDeviceConfigStore(paths.configPath, store);
393
486
  return created;
394
487
  });
@@ -401,6 +494,7 @@ function parseLaunchConfig(raw) {
401
494
  const bunnyDaemonPort = typeof parsed.bunnyDaemonPort === "number" && Number.isInteger(parsed.bunnyDaemonPort) ? parsed.bunnyDaemonPort : void 0;
402
495
  return {
403
496
  serverUrl: typeof parsed.serverUrl === "string" ? parsed.serverUrl : void 0,
497
+ spaceId: typeof parsed.spaceId === "string" ? parsed.spaceId : void 0,
404
498
  workdirRoot: typeof parsed.workdirRoot === "string" ? parsed.workdirRoot : void 0,
405
499
  oauthToken: typeof parsed.oauthToken === "string" ? parsed.oauthToken : void 0,
406
500
  token: typeof parsed.token === "string" ? parsed.token : void 0,
@@ -413,9 +507,327 @@ function parseLaunchConfig(raw) {
413
507
  import { closeSync, openSync } from "fs";
414
508
  import { ReadStream, WriteStream } from "tty";
415
509
  import { format, stripVTControlCharacters } from "util";
510
+
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
416
827
  var MAX_LINES_PER_PANE = 500;
417
828
  var MIN_DASHBOARD_WIDTH = 60;
418
829
  var MIN_DASHBOARD_HEIGHT = 18;
830
+ var DASHBOARD_TITLE = `Buda Connector v${CONNECTOR_RUNTIME_VERSION}`;
419
831
  var colors = {
420
832
  cyan: "\x1B[36m",
421
833
  dim: "\x1B[2m",
@@ -475,9 +887,12 @@ var formatDuration = (durationMs) => {
475
887
  return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
476
888
  };
477
889
  var createDashboardState = (startedAt = Date.now()) => ({
890
+ activeSection: "activity",
478
891
  activePane: "activity",
479
892
  activity: [],
480
893
  agentDebug: [],
894
+ connectorLogs: {},
895
+ connectors: [],
481
896
  heartbeat: "Waiting",
482
897
  relay: "Starting",
483
898
  agent: "Starting",
@@ -538,37 +953,189 @@ var scrollDashboardPane = (state, pane, delta) => {
538
953
  const nextScroll = delta === "follow" ? 0 : delta === "oldest" ? maxScroll : Math.max(0, Math.min(maxScroll, state.scroll[pane] + delta));
539
954
  return { ...state, scroll: { ...state.scroll, [pane]: nextScroll } };
540
955
  };
541
- var parseDashboardMouseAction = (input, size) => {
542
- const mouseEventPrefix = "\x1B[<";
543
- if (!input.includes(mouseEventPrefix)) return void 0;
544
- const event = input.split(mouseEventPrefix).at(-1);
545
- const match = event?.match(/^(\d+);\d+;(\d+)[Mm]/);
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]/);
546
1008
  if (!match) return void 0;
547
- const button = Number(match[1]);
548
1009
  const row = Number(match[2]);
549
- const availablePaneRows = Math.max(0, size.height - 6);
1010
+ const connectorHeight = getConnectorSectionHeight(state, size.height);
1011
+ const availablePaneRows = size.height - 7 - connectorHeight;
550
1012
  const activityHeight = Math.floor(availablePaneRows / 2);
551
- const activityStart = 6;
552
- const debugStart = activityStart + activityHeight;
553
- const pane = row >= activityStart && row < debugStart ? "activity" : row >= debugStart && row < size.height ? "agentDebug" : void 0;
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;
554
1016
  if (!pane) return void 0;
555
- if (button === 64) return { pane, delta: 3 };
556
- if (button === 65) return { pane, delta: -3 };
557
- if (button === 0) return { pane };
558
- return void 0;
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 };
559
1085
  };
560
- var getTerminalCopyGesture = (termProgram = process.env.TERM_PROGRAM) => termProgram === "Apple_Terminal" ? "Fn+drag copy" : "Shift+drag copy";
561
1086
  var renderStatus = (state, width, now) => {
562
1087
  const innerWidth = width - 2;
563
1088
  const status = (value) => value;
1089
+ const connector = selectedConnector(state);
564
1090
  return [
565
1091
  `+${"-".repeat(innerWidth)}+`,
566
- `|${colors.cyan}${truncate(" Buda Connector", innerWidth)}${colors.reset}|`,
1092
+ `|${colors.cyan}${truncate(` ${DASHBOARD_TITLE}`, innerWidth)}${colors.reset}|`,
1093
+ `|${truncate(` Connector ${connector?.label ?? connector?.connectorId ?? "None"} Space ${connector?.spaceId ?? "Unknown"}`, innerWidth)}|`,
567
1094
  `|${truncate(` Relay ${status(state.relay)} Agent ${status(state.agent)} Heartbeat ${status(state.heartbeat)}`, innerWidth)}|`,
568
1095
  `|${truncate(` Server ${state.server} Uptime ${formatDuration(now - state.startedAt)}`, innerWidth)}|`,
569
1096
  `+${"-".repeat(innerWidth)}+`
570
1097
  ];
571
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
+ };
572
1139
  var renderPane = (title, lines, height, width, active, scroll) => {
573
1140
  const innerWidth = width - 2;
574
1141
  const contentHeight = Math.max(1, height - 2);
@@ -594,29 +1161,67 @@ var renderPane = (title, lines, height, width, active, scroll) => {
594
1161
  output.push(`+${"-".repeat(innerWidth)}+`);
595
1162
  return output;
596
1163
  };
597
- var renderFooter = (width) => `${colors.dim}${truncate(` ${getTerminalCopyGesture()} | Click/wheel pane | Tab switch pane | F follow`, width)}${colors.reset}`;
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
+ };
598
1200
  var renderDashboard = (state, size, now = Date.now()) => {
599
1201
  const width = Math.max(10, size.width);
600
1202
  const height = Math.max(4, size.height);
601
1203
  if (width < MIN_DASHBOARD_WIDTH || height < MIN_DASHBOARD_HEIGHT) {
602
1204
  return [
603
- truncate("Buda Connector", width),
1205
+ truncate(DASHBOARD_TITLE, width),
604
1206
  truncate("Terminal too small for dashboard. Resize or restart with --plain.", width),
605
1207
  truncate(`Relay ${state.relay} | Agent ${state.agent}`, width),
606
1208
  truncate(`Heartbeat ${state.heartbeat}`, width)
607
1209
  ].slice(0, height).join("\n");
608
1210
  }
609
- const availablePaneRows = height - 6;
1211
+ if (state.prompt) return renderPrompt(state, size);
1212
+ const connectorHeight = getConnectorSectionHeight(state, height);
1213
+ const availablePaneRows = height - 7 - connectorHeight;
610
1214
  const activityHeight = Math.floor(availablePaneRows / 2);
611
1215
  const debugHeight = availablePaneRows - activityHeight;
612
1216
  return [
613
1217
  ...renderStatus(state, width, now),
1218
+ ...connectorHeight ? renderConnectors(state, connectorHeight, width) : [],
614
1219
  ...renderPane(
615
1220
  "Activity",
616
1221
  state.activity,
617
1222
  activityHeight,
618
1223
  width,
619
- state.activePane === "activity",
1224
+ state.activeSection === "activity",
620
1225
  state.scroll.activity
621
1226
  ),
622
1227
  ...renderPane(
@@ -624,15 +1229,16 @@ var renderDashboard = (state, size, now = Date.now()) => {
624
1229
  state.agentDebug,
625
1230
  debugHeight,
626
1231
  width,
627
- state.activePane === "agentDebug",
1232
+ state.activeSection === "agentDebug",
628
1233
  state.scroll.agentDebug
629
1234
  ),
630
- renderFooter(width)
1235
+ renderFooter(state, width)
631
1236
  ].join("\n");
632
1237
  };
633
1238
  var ConnectorConsoleDashboard = class {
634
- constructor(streams) {
1239
+ constructor(streams, manager) {
635
1240
  this.streams = streams;
1241
+ this.manager = manager;
636
1242
  }
637
1243
  state = createDashboardState();
638
1244
  stopped = false;
@@ -658,6 +1264,38 @@ var ConnectorConsoleDashboard = class {
658
1264
  this.state = { ...this.state, ...update };
659
1265
  this.scheduleRender();
660
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
+ }
661
1299
  stop = () => {
662
1300
  if (this.stopped) return;
663
1301
  this.stopped = true;
@@ -689,43 +1327,59 @@ var ConnectorConsoleDashboard = class {
689
1327
  }
690
1328
  handleInput = (chunk) => {
691
1329
  const key = chunk.toString();
692
- if (key === "") {
1330
+ if (key === "" && !this.manager) {
693
1331
  this.stop();
694
1332
  process.kill(process.pid, "SIGINT");
695
1333
  return;
696
1334
  }
697
- const size = {
698
- width: this.streams.output.columns || 80,
699
- height: this.streams.output.rows || 24
700
- };
701
- const mouseAction = parseDashboardMouseAction(key, size);
702
- if (mouseAction) {
703
- this.state = { ...this.state, activePane: mouseAction.pane };
704
- if (mouseAction.delta !== void 0) {
705
- this.state = scrollDashboardPane(this.state, mouseAction.pane, mouseAction.delta);
706
- }
707
- } else if (key === " ") {
708
- this.state = {
709
- ...this.state,
710
- activePane: this.state.activePane === "activity" ? "agentDebug" : "activity"
711
- };
712
- } else if (key === "f" || key === "F" || key === "\x1B[F") {
713
- this.state = scrollDashboardPane(this.state, this.state.activePane, "follow");
714
- } else if (key === "\x1B[H") {
715
- this.state = scrollDashboardPane(this.state, this.state.activePane, "oldest");
716
- } else if (key === "\x1B[A") {
717
- this.state = scrollDashboardPane(this.state, this.state.activePane, 1);
718
- } else if (key === "\x1B[B") {
719
- this.state = scrollDashboardPane(this.state, this.state.activePane, -1);
720
- } else if (key === "\x1B[5~") {
721
- this.state = scrollDashboardPane(this.state, this.state.activePane, 8);
722
- } else if (key === "\x1B[6~") {
723
- this.state = scrollDashboardPane(this.state, this.state.activePane, -8);
724
- } else {
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();
725
1350
  return;
726
1351
  }
1352
+ const result = reduceDashboardInput(this.state, key, Boolean(this.manager));
1353
+ this.state = result.state;
727
1354
  this.scheduleRender();
1355
+ if (result.command) void this.runCommand(result.command);
728
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
+ }
729
1383
  scheduleRender = () => {
730
1384
  if (this.stopped || this.renderQueued) return;
731
1385
  this.renderQueued = true;
@@ -744,30 +1398,49 @@ var startConnectorConsoleDashboard = (options = {}) => {
744
1398
  const streams = options.streams ?? resolveDashboardStreams();
745
1399
  if (!streams?.input.isTTY || !streams.output.isTTY) return void 0;
746
1400
  if (activeDashboard) return activeDashboard;
747
- activeDashboard = new ConnectorConsoleDashboard(streams);
1401
+ activeDashboard = new ConnectorConsoleDashboard(streams, options.manager);
748
1402
  activeDashboard.start();
749
1403
  return activeDashboard;
750
1404
  };
751
1405
  var updateConnectorDashboardStatus = (update) => {
752
1406
  activeDashboard?.updateStatus(update);
753
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
+ };
754
1414
 
755
- // src/onboard.ts
756
- import { cancel, intro, isCancel, log, note, outro, spinner, text } from "@clack/prompts";
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";
757
1419
 
758
- // src/bunny-daemon.ts
759
- import { mkdir as mkdir3 } from "fs/promises";
760
- import { createDaemon } from "@bunny-agent/daemon";
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";
761
1429
 
762
1430
  // src/logger.ts
763
- import { appendFile, mkdir as mkdir2, readdir, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
764
- import path2 from "path";
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";
765
1434
  var CONNECTOR_LOG_MAX_BYTES = 2 * 1024 * 1024;
766
1435
  var CONNECTOR_LOG_BACKUP_COUNT = 2;
767
1436
  var CONNECTOR_LOG_RETENTION_DAYS = 7;
768
1437
  var pendingLogs = [];
769
1438
  var fileWriteQueue = Promise.resolve();
770
1439
  var lastPrunedDate = "";
1440
+ var activeLogContext;
1441
+ var setConnectorLogContext = (context) => {
1442
+ activeLogContext = { ...activeLogContext, ...context };
1443
+ };
771
1444
  var ignoreMissingFile = (error) => {
772
1445
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
773
1446
  return;
@@ -781,17 +1454,17 @@ var formatConnectorLogDate = (date) => {
781
1454
  return `${year}-${month}-${day}`;
782
1455
  };
783
1456
  var getDatedConnectorLogPath = (baseLogPath, date = /* @__PURE__ */ new Date()) => {
784
- const parsed = path2.parse(baseLogPath);
785
- return path2.join(parsed.dir, `${parsed.name}-${formatConnectorLogDate(date)}${parsed.ext}`);
1457
+ const parsed = path3.parse(baseLogPath);
1458
+ return path3.join(parsed.dir, `${parsed.name}-${formatConnectorLogDate(date)}${parsed.ext}`);
786
1459
  };
787
1460
  async function pruneConnectorLogs(baseLogPath, now = /* @__PURE__ */ new Date(), retentionDays = CONNECTOR_LOG_RETENTION_DAYS) {
788
- const parsed = path2.parse(baseLogPath);
1461
+ const parsed = path3.parse(baseLogPath);
789
1462
  const prefix = `${parsed.name}-`;
790
1463
  const cutoff = new Date(now);
791
1464
  cutoff.setHours(0, 0, 0, 0);
792
1465
  cutoff.setDate(cutoff.getDate() - Math.max(0, retentionDays - 1));
793
1466
  const cutoffDate = formatConnectorLogDate(cutoff);
794
- const files = await readdir(parsed.dir).catch(() => []);
1467
+ const files = await readdir2(parsed.dir).catch(() => []);
795
1468
  const expired = files.filter((fileName) => {
796
1469
  if (!fileName.startsWith(prefix)) return false;
797
1470
  const date = fileName.slice(prefix.length, prefix.length + 10);
@@ -801,7 +1474,7 @@ async function pruneConnectorLogs(baseLogPath, now = /* @__PURE__ */ new Date(),
801
1474
  const isLogSegment = suffix === parsed.ext || /^\d+$/.test(segment);
802
1475
  return isDatedLog && isLogSegment && date < cutoffDate;
803
1476
  });
804
- await Promise.all(expired.map((fileName) => unlink3(path2.join(parsed.dir, fileName))));
1477
+ await Promise.all(expired.map((fileName) => unlink4(path3.join(parsed.dir, fileName))));
805
1478
  return expired.length;
806
1479
  }
807
1480
  async function rotateConnectorLogIfNeeded(logPath, maxBytes = CONNECTOR_LOG_MAX_BYTES, backupCount = CONNECTOR_LOG_BACKUP_COUNT) {
@@ -810,11 +1483,11 @@ async function rotateConnectorLogIfNeeded(logPath, maxBytes = CONNECTOR_LOG_MAX_
810
1483
  return 0;
811
1484
  });
812
1485
  if (currentSize < maxBytes) return false;
813
- await unlink3(`${logPath}.${backupCount}`).catch(ignoreMissingFile);
1486
+ await unlink4(`${logPath}.${backupCount}`).catch(ignoreMissingFile);
814
1487
  for (let index = backupCount - 1; index >= 1; index -= 1) {
815
- await rename2(`${logPath}.${index}`, `${logPath}.${index + 1}`).catch(ignoreMissingFile);
1488
+ await rename3(`${logPath}.${index}`, `${logPath}.${index + 1}`).catch(ignoreMissingFile);
816
1489
  }
817
- await rename2(logPath, `${logPath}.1`).catch(ignoreMissingFile);
1490
+ await rename3(logPath, `${logPath}.1`).catch(ignoreMissingFile);
818
1491
  return true;
819
1492
  }
820
1493
  var appendConnectorLog = (baseLogPath, line, now = /* @__PURE__ */ new Date()) => {
@@ -832,19 +1505,62 @@ var appendConnectorLog = (baseLogPath, line, now = /* @__PURE__ */ new Date()) =
832
1505
  fileWriteQueue = queuedWrite.catch(() => void 0);
833
1506
  return queuedWrite;
834
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
+ };
835
1551
  async function writeLog(level, message, metadata) {
836
1552
  const entry = {
837
1553
  level,
838
1554
  message,
839
- metadata,
1555
+ metadata: {
1556
+ ...metadata ?? {},
1557
+ ...activeLogContext?.connectorId ? { connectorId: activeLogContext.connectorId } : {}
1558
+ },
840
1559
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
841
1560
  };
842
1561
  pendingLogs.push(entry);
843
1562
  if (pendingLogs.length > 100) pendingLogs.splice(0, pendingLogs.length - 100);
844
- const paths = getConnectorPaths(getServerUrl());
845
- await mkdir2(path2.dirname(paths.logPath), { recursive: true }).catch(() => void 0);
846
- await appendConnectorLog(paths.logPath, `${JSON.stringify(entry)}
847
- `).catch(() => void 0);
1563
+ await persistLocalEntry(entry);
848
1564
  const line = `[buda-connector] ${message}`;
849
1565
  if (level === "error") console.error(line);
850
1566
  else if (level === "warn") console.warn(line);
@@ -857,767 +1573,376 @@ function restorePendingLogs(logs) {
857
1573
  pendingLogs.unshift(...logs);
858
1574
  }
859
1575
 
860
- // src/terminal.ts
861
- import { randomBytes as randomBytes2 } from "crypto";
862
- import path3 from "path";
863
- import WebSocket, { WebSocketServer } from "ws";
864
- var TERMINAL_IDLE_TTL_MS = 10 * 6e4;
865
- var TERMINAL_HANDSHAKE_TIMEOUT_MS = 1e4;
866
- var LOCAL_TERMINAL_GRANT_TTL_MS = 3e4;
867
- var LOCAL_TERMINAL_CLOSE_SPAWN_FAILED = 4401;
868
- var MAX_BUFFER_CHUNKS = 1e3;
869
- var MAX_TERMINAL_FRAME_BYTES = 1024 * 1024;
870
- var normalizeTerminalCloseCode = (code, fallback = 1011) => code >= 1e3 && code <= 1014 && ![1004, 1005, 1006].includes(code) || code >= 3e3 && code <= 4999 ? code : fallback;
871
- var truncateTerminalCloseReason = (reason, maxBytes = 120) => {
872
- let result = reason;
873
- while (Buffer.byteLength(result, "utf8") > maxBytes) result = result.slice(0, -1);
874
- return result;
875
- };
1576
+ // src/browser.ts
1577
+ var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
1578
+ var MAX_SCREENCAST_DIMENSION = 2560;
1579
+ var SESSION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,320}$/;
876
1580
  var sessions = /* @__PURE__ */ new Map();
877
- var localGrants = /* @__PURE__ */ new Map();
878
- var installedServers = /* @__PURE__ */ new WeakSet();
879
- var isSafeId = (value) => typeof value === "string" && /^[A-Za-z0-9_.:-]{1,160}$/.test(value);
880
- var parseDimension = (value, fallback, min, max) => typeof value === "number" && Number.isSafeInteger(value) ? Math.max(min, Math.min(max, value)) : fallback;
881
- var parseConnectorTerminalRequest = (request) => {
882
- if (request.method !== "CONNECT" || request.path !== "/api/terminal") {
883
- throw new Error("Invalid connector terminal request.");
1581
+ var sessionPromises = /* @__PURE__ */ new Map();
1582
+ var getBrowserMode = () => {
1583
+ const configured = process.env.BUDA_CONNECTOR_BROWSER_MODE?.trim().toLowerCase();
1584
+ return configured === "auto" || configured === "headed" || configured === "headless" ? configured : "headless";
1585
+ };
1586
+ var getSystemBrowserCandidates = (platform4, homeDirectory, env) => {
1587
+ if (platform4 === "darwin") {
1588
+ return [
1589
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1590
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
1591
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
1592
+ `${homeDirectory}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
1593
+ `${homeDirectory}/Applications/Chromium.app/Contents/MacOS/Chromium`,
1594
+ `${homeDirectory}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`
1595
+ ];
884
1596
  }
885
- if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
886
- throw new Error("Connector terminal request body is invalid.");
1597
+ if (platform4 === "win32") {
1598
+ const localAppData = env.LOCALAPPDATA;
1599
+ const programFiles = env.PROGRAMFILES;
1600
+ const programFilesX86 = env["PROGRAMFILES(X86)"];
1601
+ return [
1602
+ localAppData ? `${localAppData}\\Google\\Chrome\\Application\\chrome.exe` : "",
1603
+ localAppData ? `${localAppData}\\Chromium\\Application\\chrome.exe` : "",
1604
+ programFiles ? `${programFiles}\\Google\\Chrome\\Application\\chrome.exe` : "",
1605
+ programFilesX86 ? `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe` : "",
1606
+ programFiles ? `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe` : "",
1607
+ programFilesX86 ? `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe` : ""
1608
+ ];
887
1609
  }
888
- const body = request.body;
889
- if (!isSafeId(body.sessionId) || !isSafeId(body.tabId)) {
890
- throw new Error("Connector terminal session identity is invalid.");
1610
+ return [
1611
+ "/usr/bin/google-chrome",
1612
+ "/usr/bin/google-chrome-stable",
1613
+ "/usr/bin/chromium",
1614
+ "/usr/bin/chromium-browser",
1615
+ "/snap/bin/chromium",
1616
+ "/usr/bin/microsoft-edge",
1617
+ "/usr/bin/microsoft-edge-stable"
1618
+ ];
1619
+ };
1620
+ var resolveBrowserAvailability = (options = {}) => {
1621
+ const env = options.env ?? process.env;
1622
+ const platform4 = options.platform ?? getPlatform();
1623
+ const homeDirectory = options.homeDirectory ?? homedir2();
1624
+ const pathIsExecutable = options.pathIsExecutable ?? ((candidate) => {
1625
+ try {
1626
+ if (!statSync(candidate).isFile()) return false;
1627
+ accessSync(candidate, constants.X_OK);
1628
+ return true;
1629
+ } catch {
1630
+ return false;
1631
+ }
1632
+ });
1633
+ const configuredPath = env.BUDA_CONNECTOR_BROWSER_PATH?.trim();
1634
+ if (configuredPath) {
1635
+ if (pathIsExecutable(configuredPath)) {
1636
+ return { available: true, executablePath: configuredPath };
1637
+ }
1638
+ return {
1639
+ available: false,
1640
+ error: `BUDA_CONNECTOR_BROWSER_PATH is missing or not executable: ${configuredPath}. Set it to a Chrome, Chromium, or Edge executable.`
1641
+ };
891
1642
  }
892
- if (typeof body.token !== "string" || body.token.length < 32 || body.token.length > 2048) {
893
- throw new Error("Connector terminal token is invalid.");
1643
+ const executablePath = getSystemBrowserCandidates(platform4, homeDirectory, env).find(
1644
+ (candidate) => candidate && pathIsExecutable(candidate)
1645
+ );
1646
+ if (executablePath) {
1647
+ return { available: true, executablePath };
894
1648
  }
895
1649
  return {
896
- sessionId: body.sessionId,
897
- tabId: body.tabId,
898
- token: body.token,
899
- cols: parseDimension(body.cols, 80, 10, 500),
900
- rows: parseDimension(body.rows, 24, 5, 200)
1650
+ available: false,
1651
+ error: "No supported system browser was found. Install Google Chrome, Chromium, or Microsoft Edge, or set BUDA_CONNECTOR_BROWSER_PATH to its executable. The connector browser capability will not be advertised."
901
1652
  };
902
1653
  };
903
- var clearIdleTimer = (session) => {
904
- if (!session.idleTimer) return;
905
- clearTimeout(session.idleTimer);
906
- session.idleTimer = null;
907
- };
908
- var scheduleIdleCleanup = (sessionKey, session) => {
909
- clearIdleTimer(session);
910
- session.idleTimer = setTimeout(() => {
911
- if (session.clients.size > 0 || sessions.get(sessionKey) !== session) return;
912
- sessions.delete(sessionKey);
1654
+ async function launchPlaywright(viewport = DEFAULT_VIEWPORT) {
1655
+ const availability = resolveBrowserAvailability();
1656
+ if (!availability.available) throw new Error(availability.error);
1657
+ const { chromium } = await import("playwright-core");
1658
+ const mode = getBrowserMode();
1659
+ const executablePath = availability.executablePath;
1660
+ let browser = null;
1661
+ if (mode !== "headless") {
913
1662
  try {
914
- session.pty.kill();
915
- } catch {
1663
+ await writeLog("info", `launching headed system browser ${executablePath}`);
1664
+ browser = await chromium.launch({
1665
+ executablePath,
1666
+ headless: false,
1667
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
1668
+ });
1669
+ } catch (error) {
1670
+ if (mode === "headed") throw error;
1671
+ await writeLog(
1672
+ "warn",
1673
+ `headed browser launch failed, falling back to headless: ${error instanceof Error ? error.message : String(error)}`
1674
+ );
916
1675
  }
917
- }, TERMINAL_IDLE_TTL_MS);
918
- session.idleTimer.unref();
919
- };
920
- var sendJson = (socket, value) => {
921
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(value));
922
- };
923
- var LocalTerminalUnavailableError = class extends Error {
1676
+ }
1677
+ if (!browser) {
1678
+ await writeLog("info", `launching headless system browser ${executablePath}`);
1679
+ browser = await chromium.launch({
1680
+ executablePath,
1681
+ headless: true,
1682
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]
1683
+ });
1684
+ }
1685
+ const context = await browser.newContext({ viewport, deviceScaleFactor: 1 });
1686
+ const page = await context.newPage();
1687
+ const cdpSession = await context.newCDPSession(page);
1688
+ return { browser, page, cdpSession };
1689
+ }
1690
+ var normalizeSessionId = (value) => {
1691
+ if (typeof value !== "string") return null;
1692
+ const normalized = value.trim();
1693
+ return SESSION_ID_PATTERN.test(normalized) ? normalized : null;
924
1694
  };
925
- var serializeErrorMessage = (error) => error instanceof Error ? error.message : "Unknown error";
926
- var createTerminalSession = async (grant, nodePtyLoader) => {
927
- let nodePty;
928
- try {
929
- nodePty = await nodePtyLoader();
930
- } catch (error) {
931
- void writeLog(
932
- "error",
933
- `local terminal could not load node-pty: ${serializeErrorMessage(error)}`
934
- );
935
- throw new LocalTerminalUnavailableError(
936
- "Local terminal is unavailable because node-pty could not be loaded. The connector remains available for other capabilities."
937
- );
1695
+ async function ensureSession(sessionId, initialViewport = DEFAULT_VIEWPORT) {
1696
+ const activeSession = sessions.get(sessionId);
1697
+ if (activeSession) return activeSession;
1698
+ let sessionPromise = sessionPromises.get(sessionId);
1699
+ if (!sessionPromise) {
1700
+ sessionPromise = (async () => {
1701
+ await writeLog("info", `launching local browser session ${sessionId}`);
1702
+ const { browser, page, cdpSession } = await launchPlaywright(initialViewport);
1703
+ const nextSession = {
1704
+ page,
1705
+ browser,
1706
+ cdpSession,
1707
+ screencastClients: /* @__PURE__ */ new Set(),
1708
+ isScreencasting: false,
1709
+ screencastListenerAttached: false,
1710
+ resizeQueue: Promise.resolve(),
1711
+ inputQueue: Promise.resolve(),
1712
+ navigationQueue: Promise.resolve(),
1713
+ clientIds: /* @__PURE__ */ new Set()
1714
+ };
1715
+ sessions.set(sessionId, nextSession);
1716
+ browser.on("disconnected", () => {
1717
+ if (sessions.get(sessionId) === nextSession) sessions.delete(sessionId);
1718
+ });
1719
+ return nextSession;
1720
+ })();
1721
+ sessionPromises.set(sessionId, sessionPromise);
938
1722
  }
939
- const shell = process.platform === "win32" ? "powershell.exe" : process.env.SHELL || "/bin/bash";
940
- const shellArgs = process.platform === "win32" ? [] : ["-l"];
941
- let pty;
942
1723
  try {
943
- pty = nodePty.spawn(shell, shellArgs, {
944
- name: "xterm-256color",
945
- cols: grant.cols,
946
- rows: grant.rows,
947
- cwd: path3.resolve(grant.cwd),
948
- env: { ...process.env, TERM: "xterm-256color" }
1724
+ return await sessionPromise;
1725
+ } finally {
1726
+ if (sessionPromises.get(sessionId) === sessionPromise) sessionPromises.delete(sessionId);
1727
+ }
1728
+ }
1729
+ async function startScreencast(s) {
1730
+ if (s.isScreencasting) return;
1731
+ if (!s.screencastListenerAttached) {
1732
+ s.screencastListenerAttached = true;
1733
+ s.cdpSession.on("Page.screencastFrame", (params) => {
1734
+ const metadata = params.metadata;
1735
+ const viewport = s.page.viewportSize() ?? DEFAULT_VIEWPORT;
1736
+ const frame = {
1737
+ data: params.data,
1738
+ viewportWidth: metadata.deviceWidth ?? viewport.width,
1739
+ viewportHeight: metadata.deviceHeight ?? viewport.height
1740
+ };
1741
+ for (const listener of s.screencastClients) listener(frame);
1742
+ void s.cdpSession.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
1743
+ });
949
1744
  });
950
- } catch (error) {
951
- const message = serializeErrorMessage(error);
952
- void writeLog(
953
- "error",
954
- `local terminal failed to spawn shell=${shell} cwd=${grant.cwd}: ${message}`
955
- );
956
- throw new LocalTerminalUnavailableError(
957
- "Local terminal is unavailable because node-pty could not start a shell. The connector remains available for other capabilities."
958
- );
959
1745
  }
960
- const session = {
961
- pty,
962
- clients: /* @__PURE__ */ new Set(),
963
- buffer: [],
964
- cols: grant.cols,
965
- rows: grant.rows,
966
- idleTimer: null
967
- };
968
- sessions.set(grant.sessionKey, session);
969
- pty.onData((data) => {
970
- session.buffer.push(data);
971
- if (session.buffer.length > MAX_BUFFER_CHUNKS) session.buffer.shift();
972
- for (const socket of session.clients) {
973
- if (socket.readyState === WebSocket.OPEN) socket.send(data);
974
- }
1746
+ await s.cdpSession.send("Page.startScreencast", {
1747
+ format: "jpeg",
1748
+ quality: 90,
1749
+ maxWidth: MAX_SCREENCAST_DIMENSION,
1750
+ maxHeight: MAX_SCREENCAST_DIMENSION,
1751
+ everyNthFrame: 2
975
1752
  });
976
- pty.onExit(({ exitCode }) => {
977
- if (sessions.get(grant.sessionKey) === session) sessions.delete(grant.sessionKey);
978
- clearIdleTimer(session);
979
- for (const socket of session.clients) {
980
- sendJson(socket, { type: "status", message: `exited (code ${exitCode})` });
981
- socket.close(1e3, "Terminal exited");
982
- }
983
- session.clients.clear();
1753
+ s.isScreencasting = true;
1754
+ }
1755
+ async function stopScreencast(s) {
1756
+ if (!s.isScreencasting) return;
1757
+ await s.cdpSession.send("Page.stopScreencast").catch(() => {
984
1758
  });
985
- return session;
986
- };
987
- var attachLocalTerminal = async (socket, grant, nodePtyLoader) => {
988
- const pendingMessages = [];
989
- let session;
990
- let started = false;
991
- const handleMessage = (data) => {
992
- if (!session) {
993
- pendingMessages.push(data);
994
- return;
995
- }
996
- try {
997
- const message = JSON.parse(data);
998
- if (message.type === "bridge-start") {
999
- if (!started) {
1000
- started = true;
1001
- sendJson(socket, { type: "status", message: "connected" });
1002
- for (const chunk of session.buffer) socket.send(chunk);
1003
- }
1004
- return;
1005
- }
1006
- if (message.type === "input" && typeof message.data === "string") {
1007
- session.pty.write(message.data);
1008
- return;
1009
- }
1010
- if (message.type === "resize") {
1011
- const cols = parseDimension(message.cols, session.cols, 10, 500);
1012
- const rows = parseDimension(message.rows, session.rows, 5, 200);
1013
- if (cols !== session.cols || rows !== session.rows) {
1014
- session.cols = cols;
1015
- session.rows = rows;
1016
- session.pty.resize(cols, rows);
1017
- }
1018
- return;
1019
- }
1020
- } catch {
1021
- }
1022
- session.pty.write(data);
1023
- };
1024
- socket.on("message", (raw) => handleMessage(raw.toString()));
1025
- session = sessions.get(grant.sessionKey);
1026
- if (!session) session = await createTerminalSession(grant, nodePtyLoader);
1027
- else clearIdleTimer(session);
1028
- session.clients.add(socket);
1029
- for (const data of pendingMessages.splice(0)) handleMessage(data);
1030
- const detach = () => {
1031
- if (!session) return;
1032
- session.clients.delete(socket);
1033
- if (session.clients.size === 0) scheduleIdleCleanup(grant.sessionKey, session);
1034
- };
1035
- socket.once("close", detach);
1036
- socket.once("error", detach);
1037
- };
1038
- var installConnectorTerminalEndpoint = (server, options = {}) => {
1039
- if (installedServers.has(server)) return;
1040
- installedServers.add(server);
1041
- const terminalServer = new WebSocketServer({ noServer: true });
1042
- server.on("upgrade", (request, socket, head) => {
1043
- const url = new URL(request.url ?? "/", "http://connector.local");
1044
- if (url.pathname !== "/api/terminal") return;
1045
- const tokenHeader = request.headers["x-buda-local-terminal-token"];
1046
- const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
1047
- const grant = token ? localGrants.get(token) : void 0;
1048
- if (!token || !grant || grant.expiresAt <= Date.now()) {
1049
- if (token) localGrants.delete(token);
1050
- socket.destroy();
1051
- return;
1052
- }
1053
- localGrants.delete(token);
1054
- terminalServer.handleUpgrade(request, socket, head, (client) => {
1055
- void attachLocalTerminal(
1056
- client,
1057
- grant,
1058
- options.nodePtyLoader ?? (() => import("./lib-UX6C35VI.js"))
1059
- ).catch((error) => {
1060
- const message = error instanceof LocalTerminalUnavailableError ? error.message : "Local terminal is unavailable. The connector remains available for other capabilities.";
1061
- sendJson(client, { type: "error", fatal: true, message });
1062
- client.close(LOCAL_TERMINAL_CLOSE_SPAWN_FAILED, "Local terminal unavailable");
1063
- });
1064
- });
1759
+ s.isScreencasting = false;
1760
+ }
1761
+ async function closeSession(sessionId) {
1762
+ const activeSession = sessions.get(sessionId) ?? await sessionPromises.get(sessionId)?.catch(() => null);
1763
+ if (!activeSession) return;
1764
+ if (sessions.get(sessionId) === activeSession) sessions.delete(sessionId);
1765
+ await stopScreencast(activeSession);
1766
+ await activeSession.cdpSession.detach().catch(() => {
1065
1767
  });
1066
- server.once("close", () => {
1067
- for (const client of terminalServer.clients) client.terminate();
1068
- terminalServer.close();
1069
- localGrants.clear();
1070
- for (const session of sessions.values()) {
1071
- clearIdleTimer(session);
1072
- try {
1073
- session.pty.kill();
1074
- } catch {
1075
- }
1076
- }
1077
- sessions.clear();
1768
+ await activeSession.browser.close().catch(() => {
1078
1769
  });
1079
- };
1080
- var registerLocalTerminal = (input) => {
1081
- if (!isSafeId(input.workspaceKey) || !isSafeId(input.tabId)) {
1082
- throw new Error("Invalid local terminal workspace session.");
1083
- }
1084
- const token = randomBytes2(32).toString("base64url");
1085
- localGrants.set(token, {
1086
- sessionKey: `${input.workspaceKey}:${input.tabId}`,
1087
- workspaceKey: input.workspaceKey,
1088
- cwd: input.cwd,
1089
- cols: input.cols,
1090
- rows: input.rows,
1091
- expiresAt: Date.now() + LOCAL_TERMINAL_GRANT_TTL_MS
1770
+ await writeLog("info", `browser session stopped ${sessionId}`);
1771
+ }
1772
+ async function closeAllBrowserSessions() {
1773
+ const sessionIds = /* @__PURE__ */ new Set([...sessions.keys(), ...sessionPromises.keys()]);
1774
+ await Promise.all([...sessionIds].map((sessionId) => closeSession(sessionId)));
1775
+ }
1776
+ async function resizeBrowser(s, viewport) {
1777
+ const resize = s.resizeQueue.catch(() => {
1778
+ }).then(async () => {
1779
+ const current = s.page.viewportSize();
1780
+ if (current?.width === viewport.width && current.height === viewport.height) return;
1781
+ const wasScreencasting = s.isScreencasting;
1782
+ if (wasScreencasting) await stopScreencast(s);
1783
+ await s.page.setViewportSize(viewport);
1784
+ if (wasScreencasting) await startScreencast(s);
1785
+ await writeLog("info", `browser resized to ${viewport.width}x${viewport.height}`);
1092
1786
  });
1093
- const cleanupTimer = setTimeout(() => localGrants.delete(token), LOCAL_TERMINAL_GRANT_TTL_MS);
1094
- cleanupTimer.unref();
1095
- return token;
1096
- };
1097
- var toLocalTerminalUrl = (baseUrl) => {
1098
- const url = new URL("/api/terminal", baseUrl);
1099
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1100
- return url.toString();
1787
+ s.resizeQueue = resize;
1788
+ await resize;
1789
+ }
1790
+ async function navigateBrowser(s, url, options) {
1791
+ const navigation = s.navigationQueue.catch(() => {
1792
+ }).then(async () => {
1793
+ if (s.page.url() === url) return null;
1794
+ return s.page.goto(url, options);
1795
+ });
1796
+ s.navigationQueue = navigation.then(() => {
1797
+ });
1798
+ return navigation;
1799
+ }
1800
+ var normalizeViewportDimension = (value, min, max, fallback) => {
1801
+ const dimension = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : fallback;
1802
+ return Math.min(max, Math.max(min, dimension));
1101
1803
  };
1102
- var connectSocket = async (socket) => new Promise((resolve, reject) => {
1103
- const onOpen = () => {
1104
- socket.removeListener("error", onError);
1105
- resolve();
1106
- };
1107
- const onError = (error) => {
1108
- socket.removeListener("open", onOpen);
1109
- reject(error);
1110
- };
1111
- socket.once("open", onOpen);
1112
- socket.once("error", onError);
1113
- });
1114
- var proxyConnectorTerminalTunnel = async (input) => {
1115
- const cloud = new WebSocket(
1116
- getConnectorTerminalUrl(input.serverUrl, input.connectorId, input.sessionId),
1117
- {
1118
- headers: { "X-Buda-Terminal-Token": input.cloudToken },
1119
- handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
1120
- }
1121
- );
1122
- const local = new WebSocket(toLocalTerminalUrl(input.localBaseUrl), {
1123
- headers: { "X-Buda-Local-Terminal-Token": input.localToken },
1124
- handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
1804
+ var normalizeTimeout = (value, fallback, max) => typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(1e3, Math.round(value))) : fallback;
1805
+ function jsonResponse(status, body) {
1806
+ return new Response(JSON.stringify(body), {
1807
+ status,
1808
+ headers: { "content-type": "application/json" }
1125
1809
  });
1126
- try {
1127
- await Promise.all([connectSocket(cloud), connectSocket(local)]);
1128
- } catch (error) {
1129
- cloud.terminate();
1130
- local.terminate();
1131
- throw error;
1132
- }
1133
- let bridgeAccepted = false;
1134
- let pendingBytes = 0;
1135
- const pendingCloudFrames = [];
1136
- const pendingLocalFrames = [];
1137
- const bufferFrame = (frames, data, isBinary) => {
1138
- const bytes = Array.isArray(data) ? data.reduce((total, chunk) => total + chunk.byteLength, 0) : data.byteLength;
1139
- if (pendingBytes + bytes > MAX_TERMINAL_FRAME_BYTES) {
1140
- cloud.close(1009, "Terminal handshake buffer too large");
1141
- local.close(1009, "Terminal handshake buffer too large");
1142
- return false;
1143
- }
1144
- pendingBytes += bytes;
1145
- frames.push([data, isBinary]);
1146
- return true;
1147
- };
1148
- cloud.on("message", (data, isBinary) => {
1149
- if (!bridgeAccepted) {
1150
- if (!isBinary && data.toString() === '{"type":"bridge-accepted"}') {
1151
- bridgeAccepted = true;
1152
- local.send(JSON.stringify({ type: "bridge-start" }));
1153
- for (const [frame, binary] of pendingCloudFrames) {
1154
- if (local.readyState === WebSocket.OPEN) local.send(frame, { binary });
1155
- }
1156
- for (const [frame, binary] of pendingLocalFrames) {
1157
- if (cloud.readyState === WebSocket.OPEN) cloud.send(frame, { binary });
1158
- }
1159
- pendingCloudFrames.length = 0;
1160
- pendingLocalFrames.length = 0;
1161
- pendingBytes = 0;
1810
+ }
1811
+ var getRequestSessionId = (context) => normalizeSessionId(context.sessionId);
1812
+ async function handleBrowserInput(sessionIdValue, body) {
1813
+ const sessionId = normalizeSessionId(sessionIdValue);
1814
+ if (!sessionId) throw new Error("A valid browser session id is required.");
1815
+ const ev = body ?? {};
1816
+ const s = await ensureSession(sessionId);
1817
+ const input = s.inputQueue.catch(() => {
1818
+ }).then(async () => {
1819
+ switch (ev.type) {
1820
+ case "mouseMove":
1821
+ await s.cdpSession.send("Input.dispatchMouseEvent", {
1822
+ type: "mouseMoved",
1823
+ x: ev.x,
1824
+ y: ev.y
1825
+ });
1162
1826
  return;
1163
- }
1164
- bufferFrame(pendingCloudFrames, data, isBinary);
1165
- return;
1827
+ case "mouseDown":
1828
+ await s.cdpSession.send("Input.dispatchMouseEvent", {
1829
+ type: "mousePressed",
1830
+ x: ev.x,
1831
+ y: ev.y,
1832
+ button: "left",
1833
+ clickCount: 1
1834
+ });
1835
+ return;
1836
+ case "mouseUp":
1837
+ await s.cdpSession.send("Input.dispatchMouseEvent", {
1838
+ type: "mouseReleased",
1839
+ x: ev.x,
1840
+ y: ev.y,
1841
+ button: "left",
1842
+ clickCount: 1
1843
+ });
1844
+ return;
1845
+ case "keyDown":
1846
+ await s.cdpSession.send("Input.dispatchKeyEvent", {
1847
+ type: "keyDown",
1848
+ key: ev.key,
1849
+ text: ev.text ?? ""
1850
+ });
1851
+ return;
1852
+ case "keyUp":
1853
+ await s.cdpSession.send("Input.dispatchKeyEvent", {
1854
+ type: "keyUp",
1855
+ key: ev.key
1856
+ });
1857
+ return;
1858
+ case "scroll":
1859
+ await s.cdpSession.send("Input.dispatchMouseEvent", {
1860
+ type: "mouseWheel",
1861
+ x: ev.x,
1862
+ y: ev.y,
1863
+ deltaX: 0,
1864
+ deltaY: ev.deltaY
1865
+ });
1866
+ return;
1867
+ default:
1868
+ throw new Error(`Unknown browser input event: ${String(ev.type)}`);
1166
1869
  }
1167
- if (local.readyState === WebSocket.OPEN) local.send(data, { binary: isBinary });
1168
1870
  });
1169
- local.on("message", (data, isBinary) => {
1170
- if (!bridgeAccepted) {
1171
- bufferFrame(pendingLocalFrames, data, isBinary);
1172
- return;
1173
- }
1174
- if (cloud.readyState === WebSocket.OPEN) cloud.send(data, { binary: isBinary });
1175
- });
1176
- cloud.once("close", (code, reason) => {
1177
- if (!bridgeAccepted) {
1178
- void writeLog(
1179
- "warn",
1180
- `terminal cloud tunnel closed before readiness session=${input.sessionId} code=${code} reason=${truncateTerminalCloseReason(reason.toString())}`
1181
- );
1182
- }
1183
- if (local.readyState === WebSocket.OPEN) {
1184
- local.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
1185
- }
1186
- });
1187
- local.once("close", (code, reason) => {
1188
- if (cloud.readyState === WebSocket.OPEN) {
1189
- cloud.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
1190
- }
1191
- });
1192
- cloud.once("error", () => local.terminate());
1193
- local.once("error", () => cloud.terminate());
1194
- cloud.send(JSON.stringify({ type: "bridge-ready" }));
1195
- await writeLog(
1196
- "info",
1197
- `terminal proxy connected session=${input.sessionId} workspace=${input.workspaceKey}`
1198
- );
1199
- };
1200
-
1201
- // src/bunny-daemon.ts
1202
- async function startEmbeddedBunnyDaemon(config, launchConfig) {
1203
- const host = getBunnyDaemonHost(launchConfig);
1204
- const port = getBunnyDaemonPort(launchConfig);
1205
- const root = config.workdirRoot;
1206
- await mkdir3(root, { recursive: true });
1207
- const server = createDaemon({ host, port, root });
1208
- installConnectorTerminalEndpoint(server);
1209
- await new Promise((resolve, reject) => {
1210
- const onError = (error) => {
1211
- server.off("listening", onListening);
1212
- reject(error);
1213
- };
1214
- const onListening = () => {
1215
- server.off("error", onError);
1216
- resolve();
1217
- };
1218
- server.once("error", onError);
1219
- server.once("listening", onListening);
1220
- server.listen(port, host);
1221
- });
1222
- const address = server.address();
1223
- const actualPort = typeof address === "object" && address ? address.port : port;
1224
- const baseUrl = `http://${host}:${actualPort}`;
1225
- await writeLog("info", `bunny daemon listening ${baseUrl}`, {
1226
- root
1227
- });
1228
- return {
1229
- server,
1230
- host,
1231
- port: actualPort,
1232
- baseUrl,
1233
- root
1234
- };
1235
- }
1236
- async function stopEmbeddedBunnyDaemon(daemon) {
1237
- await new Promise((resolve) => {
1238
- daemon.server.close(() => resolve());
1239
- });
1240
- await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
1871
+ s.inputQueue = input;
1872
+ await input;
1241
1873
  }
1242
-
1243
- // src/cloud-client.ts
1244
- import { createHash as createHash4 } from "crypto";
1245
- import { platform as platform3, release } from "os";
1246
-
1247
- // src/browser.ts
1248
- import { accessSync, constants, statSync } from "fs";
1249
- import { mkdir as mkdir4 } from "fs/promises";
1250
- import { platform as getPlatform, homedir as homedir2 } from "os";
1251
- import path4 from "path";
1252
- var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
1253
- var MAX_SCREENCAST_DIMENSION = 2560;
1254
- var SESSION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,320}$/;
1255
- var sessions2 = /* @__PURE__ */ new Map();
1256
- var sessionPromises = /* @__PURE__ */ new Map();
1257
- var getBrowserMode = () => {
1258
- const configured = process.env.BUDA_CONNECTOR_BROWSER_MODE?.trim().toLowerCase();
1259
- return configured === "auto" || configured === "headed" || configured === "headless" ? configured : "headless";
1260
- };
1261
- var getSystemBrowserCandidates = (platform4, homeDirectory, env) => {
1262
- if (platform4 === "darwin") {
1263
- return [
1264
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1265
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
1266
- "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
1267
- `${homeDirectory}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
1268
- `${homeDirectory}/Applications/Chromium.app/Contents/MacOS/Chromium`,
1269
- `${homeDirectory}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`
1270
- ];
1271
- }
1272
- if (platform4 === "win32") {
1273
- const localAppData = env.LOCALAPPDATA;
1274
- const programFiles = env.PROGRAMFILES;
1275
- const programFilesX86 = env["PROGRAMFILES(X86)"];
1276
- return [
1277
- localAppData ? `${localAppData}\\Google\\Chrome\\Application\\chrome.exe` : "",
1278
- localAppData ? `${localAppData}\\Chromium\\Application\\chrome.exe` : "",
1279
- programFiles ? `${programFiles}\\Google\\Chrome\\Application\\chrome.exe` : "",
1280
- programFilesX86 ? `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe` : "",
1281
- programFiles ? `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe` : "",
1282
- programFilesX86 ? `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe` : ""
1283
- ];
1874
+ var getPageResult = async (s) => ({
1875
+ url: s.page.url(),
1876
+ title: await s.page.title().catch(() => "")
1877
+ });
1878
+ var resolveScreenshotPath = (workdirRoot, filePath) => {
1879
+ if (!workdirRoot) throw new Error("Connector workdir is unavailable for screenshots.");
1880
+ const raw = typeof filePath === "string" ? filePath.trim() : "";
1881
+ if (!raw) throw new Error("file_path is required");
1882
+ const agentRoot = path4.resolve(workdirRoot, "agent");
1883
+ const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
1884
+ const absolutePath = path4.resolve(agentRoot, relativeInput);
1885
+ if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path4.sep}`)) {
1886
+ throw new Error("file_path must stay under the connector agent drive");
1284
1887
  }
1285
- return [
1286
- "/usr/bin/google-chrome",
1287
- "/usr/bin/google-chrome-stable",
1288
- "/usr/bin/chromium",
1289
- "/usr/bin/chromium-browser",
1290
- "/snap/bin/chromium",
1291
- "/usr/bin/microsoft-edge",
1292
- "/usr/bin/microsoft-edge-stable"
1293
- ];
1888
+ return {
1889
+ absolutePath,
1890
+ relativePath: path4.relative(agentRoot, absolutePath).split(path4.sep).join("/")
1891
+ };
1294
1892
  };
1295
- var resolveBrowserAvailability = (options = {}) => {
1296
- const env = options.env ?? process.env;
1297
- const platform4 = options.platform ?? getPlatform();
1298
- const homeDirectory = options.homeDirectory ?? homedir2();
1299
- const pathIsExecutable = options.pathIsExecutable ?? ((candidate) => {
1300
- try {
1301
- if (!statSync(candidate).isFile()) return false;
1302
- accessSync(candidate, constants.X_OK);
1303
- return true;
1304
- } catch {
1305
- return false;
1306
- }
1307
- });
1308
- const configuredPath = env.BUDA_CONNECTOR_BROWSER_PATH?.trim();
1309
- if (configuredPath) {
1310
- if (pathIsExecutable(configuredPath)) {
1311
- return { available: true, executablePath: configuredPath };
1312
- }
1313
- return {
1314
- available: false,
1315
- error: `BUDA_CONNECTOR_BROWSER_PATH is missing or not executable: ${configuredPath}. Set it to a Chrome, Chromium, or Edge executable.`
1893
+ async function handleBrowserRequest(requestPath, method, body, context = {}) {
1894
+ const route = new URL(requestPath, "http://connector.local").pathname.replace(
1895
+ /^\/browser\/?/,
1896
+ ""
1897
+ );
1898
+ const sessionId = getRequestSessionId(context);
1899
+ if (!sessionId)
1900
+ return jsonResponse(400, { ok: false, error: "valid browser session id required" });
1901
+ if (route === "start" && method === "POST") {
1902
+ const input = body ?? {};
1903
+ const viewport = {
1904
+ width: normalizeViewportDimension(input.width, 320, 2560, DEFAULT_VIEWPORT.width),
1905
+ height: normalizeViewportDimension(input.height, 240, 2560, DEFAULT_VIEWPORT.height)
1316
1906
  };
1907
+ const s = await ensureSession(sessionId, viewport);
1908
+ const clientId = normalizeSessionId(input.clientId);
1909
+ if (clientId) s.clientIds.add(clientId);
1910
+ await resizeBrowser(s, viewport);
1911
+ const url = input.url?.trim() || "about:blank";
1912
+ await writeLog("info", `browser viewport ${viewport.width}x${viewport.height}`);
1913
+ await navigateBrowser(s, url);
1914
+ await writeLog("info", `browser ready at ${s.page.url()}`);
1915
+ return jsonResponse(200, { ok: true, ...await getPageResult(s) });
1317
1916
  }
1318
- const executablePath = getSystemBrowserCandidates(platform4, homeDirectory, env).find(
1319
- (candidate) => candidate && pathIsExecutable(candidate)
1320
- );
1321
- if (executablePath) {
1322
- return { available: true, executablePath };
1917
+ if (route === "stop" && method === "POST") {
1918
+ await closeSession(sessionId);
1919
+ return jsonResponse(200, { ok: true });
1323
1920
  }
1324
- return {
1325
- available: false,
1326
- error: "No supported system browser was found. Install Google Chrome, Chromium, or Microsoft Edge, or set BUDA_CONNECTOR_BROWSER_PATH to its executable. The connector browser capability will not be advertised."
1327
- };
1328
- };
1329
- async function launchPlaywright(viewport = DEFAULT_VIEWPORT) {
1330
- const availability = resolveBrowserAvailability();
1331
- if (!availability.available) throw new Error(availability.error);
1332
- const { chromium } = await import("playwright-core");
1333
- const mode = getBrowserMode();
1334
- const executablePath = availability.executablePath;
1335
- let browser = null;
1336
- if (mode !== "headless") {
1337
- try {
1338
- await writeLog("info", `launching headed system browser ${executablePath}`);
1339
- browser = await chromium.launch({
1340
- executablePath,
1341
- headless: false,
1342
- args: ["--no-sandbox", "--disable-setuid-sandbox"]
1343
- });
1344
- } catch (error) {
1345
- if (mode === "headed") throw error;
1346
- await writeLog(
1347
- "warn",
1348
- `headed browser launch failed, falling back to headless: ${error instanceof Error ? error.message : String(error)}`
1349
- );
1350
- }
1921
+ if (route === "release" && method === "POST") {
1922
+ const input = body ?? {};
1923
+ const clientId = normalizeSessionId(input.clientId);
1924
+ if (!clientId) return jsonResponse(400, { ok: false, error: "valid client id required" });
1925
+ const s = sessions.get(sessionId);
1926
+ if (!s) return jsonResponse(200, { ok: true, closed: false });
1927
+ s.clientIds.delete(clientId);
1928
+ if (s.clientIds.size > 0) return jsonResponse(200, { ok: true, closed: false });
1929
+ await closeSession(sessionId);
1930
+ return jsonResponse(200, { ok: true, closed: true });
1351
1931
  }
1352
- if (!browser) {
1353
- await writeLog("info", `launching headless system browser ${executablePath}`);
1354
- browser = await chromium.launch({
1355
- executablePath,
1356
- headless: true,
1357
- args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]
1358
- });
1359
- }
1360
- const context = await browser.newContext({ viewport, deviceScaleFactor: 1 });
1361
- const page = await context.newPage();
1362
- const cdpSession = await context.newCDPSession(page);
1363
- return { browser, page, cdpSession };
1364
- }
1365
- var normalizeSessionId = (value) => {
1366
- if (typeof value !== "string") return null;
1367
- const normalized = value.trim();
1368
- return SESSION_ID_PATTERN.test(normalized) ? normalized : null;
1369
- };
1370
- async function ensureSession(sessionId, initialViewport = DEFAULT_VIEWPORT) {
1371
- const activeSession = sessions2.get(sessionId);
1372
- if (activeSession) return activeSession;
1373
- let sessionPromise = sessionPromises.get(sessionId);
1374
- if (!sessionPromise) {
1375
- sessionPromise = (async () => {
1376
- await writeLog("info", `launching local browser session ${sessionId}`);
1377
- const { browser, page, cdpSession } = await launchPlaywright(initialViewport);
1378
- const nextSession = {
1379
- page,
1380
- browser,
1381
- cdpSession,
1382
- screencastClients: /* @__PURE__ */ new Set(),
1383
- isScreencasting: false,
1384
- screencastListenerAttached: false,
1385
- resizeQueue: Promise.resolve(),
1386
- inputQueue: Promise.resolve(),
1387
- navigationQueue: Promise.resolve(),
1388
- clientIds: /* @__PURE__ */ new Set()
1389
- };
1390
- sessions2.set(sessionId, nextSession);
1391
- browser.on("disconnected", () => {
1392
- if (sessions2.get(sessionId) === nextSession) sessions2.delete(sessionId);
1393
- });
1394
- return nextSession;
1395
- })();
1396
- sessionPromises.set(sessionId, sessionPromise);
1397
- }
1398
- try {
1399
- return await sessionPromise;
1400
- } finally {
1401
- if (sessionPromises.get(sessionId) === sessionPromise) sessionPromises.delete(sessionId);
1402
- }
1403
- }
1404
- async function startScreencast(s) {
1405
- if (s.isScreencasting) return;
1406
- if (!s.screencastListenerAttached) {
1407
- s.screencastListenerAttached = true;
1408
- s.cdpSession.on("Page.screencastFrame", (params) => {
1409
- const metadata = params.metadata;
1410
- const viewport = s.page.viewportSize() ?? DEFAULT_VIEWPORT;
1411
- const frame = {
1412
- data: params.data,
1413
- viewportWidth: metadata.deviceWidth ?? viewport.width,
1414
- viewportHeight: metadata.deviceHeight ?? viewport.height
1415
- };
1416
- for (const listener of s.screencastClients) listener(frame);
1417
- void s.cdpSession.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
1418
- });
1419
- });
1420
- }
1421
- await s.cdpSession.send("Page.startScreencast", {
1422
- format: "jpeg",
1423
- quality: 90,
1424
- maxWidth: MAX_SCREENCAST_DIMENSION,
1425
- maxHeight: MAX_SCREENCAST_DIMENSION,
1426
- everyNthFrame: 2
1427
- });
1428
- s.isScreencasting = true;
1429
- }
1430
- async function stopScreencast(s) {
1431
- if (!s.isScreencasting) return;
1432
- await s.cdpSession.send("Page.stopScreencast").catch(() => {
1433
- });
1434
- s.isScreencasting = false;
1435
- }
1436
- async function closeSession(sessionId) {
1437
- const activeSession = sessions2.get(sessionId) ?? await sessionPromises.get(sessionId)?.catch(() => null);
1438
- if (!activeSession) return;
1439
- if (sessions2.get(sessionId) === activeSession) sessions2.delete(sessionId);
1440
- await stopScreencast(activeSession);
1441
- await activeSession.cdpSession.detach().catch(() => {
1442
- });
1443
- await activeSession.browser.close().catch(() => {
1444
- });
1445
- await writeLog("info", `browser session stopped ${sessionId}`);
1446
- }
1447
- async function closeAllBrowserSessions() {
1448
- const sessionIds = /* @__PURE__ */ new Set([...sessions2.keys(), ...sessionPromises.keys()]);
1449
- await Promise.all([...sessionIds].map((sessionId) => closeSession(sessionId)));
1450
- }
1451
- async function resizeBrowser(s, viewport) {
1452
- const resize = s.resizeQueue.catch(() => {
1453
- }).then(async () => {
1454
- const current = s.page.viewportSize();
1455
- if (current?.width === viewport.width && current.height === viewport.height) return;
1456
- const wasScreencasting = s.isScreencasting;
1457
- if (wasScreencasting) await stopScreencast(s);
1458
- await s.page.setViewportSize(viewport);
1459
- if (wasScreencasting) await startScreencast(s);
1460
- await writeLog("info", `browser resized to ${viewport.width}x${viewport.height}`);
1461
- });
1462
- s.resizeQueue = resize;
1463
- await resize;
1464
- }
1465
- async function navigateBrowser(s, url, options) {
1466
- const navigation = s.navigationQueue.catch(() => {
1467
- }).then(async () => {
1468
- if (s.page.url() === url) return null;
1469
- return s.page.goto(url, options);
1470
- });
1471
- s.navigationQueue = navigation.then(() => {
1472
- });
1473
- return navigation;
1474
- }
1475
- var normalizeViewportDimension = (value, min, max, fallback) => {
1476
- const dimension = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : fallback;
1477
- return Math.min(max, Math.max(min, dimension));
1478
- };
1479
- var normalizeTimeout = (value, fallback, max) => typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(1e3, Math.round(value))) : fallback;
1480
- function jsonResponse(status, body) {
1481
- return new Response(JSON.stringify(body), {
1482
- status,
1483
- headers: { "content-type": "application/json" }
1484
- });
1485
- }
1486
- var getRequestSessionId = (context) => normalizeSessionId(context.sessionId);
1487
- async function handleBrowserInput(sessionIdValue, body) {
1488
- const sessionId = normalizeSessionId(sessionIdValue);
1489
- if (!sessionId) throw new Error("A valid browser session id is required.");
1490
- const ev = body ?? {};
1491
- const s = await ensureSession(sessionId);
1492
- const input = s.inputQueue.catch(() => {
1493
- }).then(async () => {
1494
- switch (ev.type) {
1495
- case "mouseMove":
1496
- await s.cdpSession.send("Input.dispatchMouseEvent", {
1497
- type: "mouseMoved",
1498
- x: ev.x,
1499
- y: ev.y
1500
- });
1501
- return;
1502
- case "mouseDown":
1503
- await s.cdpSession.send("Input.dispatchMouseEvent", {
1504
- type: "mousePressed",
1505
- x: ev.x,
1506
- y: ev.y,
1507
- button: "left",
1508
- clickCount: 1
1509
- });
1510
- return;
1511
- case "mouseUp":
1512
- await s.cdpSession.send("Input.dispatchMouseEvent", {
1513
- type: "mouseReleased",
1514
- x: ev.x,
1515
- y: ev.y,
1516
- button: "left",
1517
- clickCount: 1
1518
- });
1519
- return;
1520
- case "keyDown":
1521
- await s.cdpSession.send("Input.dispatchKeyEvent", {
1522
- type: "keyDown",
1523
- key: ev.key,
1524
- text: ev.text ?? ""
1525
- });
1526
- return;
1527
- case "keyUp":
1528
- await s.cdpSession.send("Input.dispatchKeyEvent", {
1529
- type: "keyUp",
1530
- key: ev.key
1531
- });
1532
- return;
1533
- case "scroll":
1534
- await s.cdpSession.send("Input.dispatchMouseEvent", {
1535
- type: "mouseWheel",
1536
- x: ev.x,
1537
- y: ev.y,
1538
- deltaX: 0,
1539
- deltaY: ev.deltaY
1540
- });
1541
- return;
1542
- default:
1543
- throw new Error(`Unknown browser input event: ${String(ev.type)}`);
1544
- }
1545
- });
1546
- s.inputQueue = input;
1547
- await input;
1548
- }
1549
- var getPageResult = async (s) => ({
1550
- url: s.page.url(),
1551
- title: await s.page.title().catch(() => "")
1552
- });
1553
- var resolveScreenshotPath = (workdirRoot, filePath) => {
1554
- if (!workdirRoot) throw new Error("Connector workdir is unavailable for screenshots.");
1555
- const raw = typeof filePath === "string" ? filePath.trim() : "";
1556
- if (!raw) throw new Error("file_path is required");
1557
- const agentRoot = path4.resolve(workdirRoot, "agent");
1558
- const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
1559
- const absolutePath = path4.resolve(agentRoot, relativeInput);
1560
- if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path4.sep}`)) {
1561
- throw new Error("file_path must stay under the connector agent drive");
1562
- }
1563
- return {
1564
- absolutePath,
1565
- relativePath: path4.relative(agentRoot, absolutePath).split(path4.sep).join("/")
1566
- };
1567
- };
1568
- async function handleBrowserRequest(requestPath, method, body, context = {}) {
1569
- const route = new URL(requestPath, "http://connector.local").pathname.replace(
1570
- /^\/browser\/?/,
1571
- ""
1572
- );
1573
- const sessionId = getRequestSessionId(context);
1574
- if (!sessionId)
1575
- return jsonResponse(400, { ok: false, error: "valid browser session id required" });
1576
- if (route === "start" && method === "POST") {
1577
- const input = body ?? {};
1578
- const viewport = {
1579
- width: normalizeViewportDimension(input.width, 320, 2560, DEFAULT_VIEWPORT.width),
1580
- height: normalizeViewportDimension(input.height, 240, 2560, DEFAULT_VIEWPORT.height)
1581
- };
1582
- const s = await ensureSession(sessionId, viewport);
1583
- const clientId = normalizeSessionId(input.clientId);
1584
- if (clientId) s.clientIds.add(clientId);
1585
- await resizeBrowser(s, viewport);
1586
- const url = input.url?.trim() || "about:blank";
1587
- await writeLog("info", `browser viewport ${viewport.width}x${viewport.height}`);
1588
- await navigateBrowser(s, url);
1589
- await writeLog("info", `browser ready at ${s.page.url()}`);
1590
- return jsonResponse(200, { ok: true, ...await getPageResult(s) });
1591
- }
1592
- if (route === "stop" && method === "POST") {
1593
- await closeSession(sessionId);
1594
- return jsonResponse(200, { ok: true });
1595
- }
1596
- if (route === "release" && method === "POST") {
1597
- const input = body ?? {};
1598
- const clientId = normalizeSessionId(input.clientId);
1599
- if (!clientId) return jsonResponse(400, { ok: false, error: "valid client id required" });
1600
- const s = sessions2.get(sessionId);
1601
- if (!s) return jsonResponse(200, { ok: true, closed: false });
1602
- s.clientIds.delete(clientId);
1603
- if (s.clientIds.size > 0) return jsonResponse(200, { ok: true, closed: false });
1604
- await closeSession(sessionId);
1605
- return jsonResponse(200, { ok: true, closed: true });
1606
- }
1607
- if (route === "navigate" && method === "POST") {
1608
- const input = body ?? {};
1609
- const url = input.url?.trim();
1610
- if (!url) return jsonResponse(400, { ok: false, error: "url required" });
1611
- const s = await ensureSession(sessionId);
1612
- const response = await navigateBrowser(s, url, {
1613
- waitUntil: input.waitUntil ?? "domcontentloaded",
1614
- timeout: normalizeTimeout(input.timeoutMs, 3e4, 6e4)
1615
- });
1616
- return jsonResponse(200, {
1617
- ok: true,
1618
- action: "navigate",
1619
- ...await getPageResult(s),
1620
- status: response?.status() ?? null
1932
+ if (route === "navigate" && method === "POST") {
1933
+ const input = body ?? {};
1934
+ const url = input.url?.trim();
1935
+ if (!url) return jsonResponse(400, { ok: false, error: "url required" });
1936
+ const s = await ensureSession(sessionId);
1937
+ const response = await navigateBrowser(s, url, {
1938
+ waitUntil: input.waitUntil ?? "domcontentloaded",
1939
+ timeout: normalizeTimeout(input.timeoutMs, 3e4, 6e4)
1940
+ });
1941
+ return jsonResponse(200, {
1942
+ ok: true,
1943
+ action: "navigate",
1944
+ ...await getPageResult(s),
1945
+ status: response?.status() ?? null
1621
1946
  });
1622
1947
  }
1623
1948
  if (route === "click" && method === "POST") {
@@ -1706,7 +2031,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
1706
2031
  return jsonResponse(200, { ok: true });
1707
2032
  }
1708
2033
  if (route === "location" && method === "GET") {
1709
- const s = sessions2.get(sessionId);
2034
+ const s = sessions.get(sessionId);
1710
2035
  if (!s) return jsonResponse(200, { ok: true, url: "", title: "" });
1711
2036
  return jsonResponse(200, { ok: true, ...await getPageResult(s) });
1712
2037
  }
@@ -1737,7 +2062,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
1737
2062
  if (s.screencastClients.size === 0) void stopScreencast(s);
1738
2063
  };
1739
2064
  checkInterval = setInterval(() => {
1740
- if (sessions2.get(sessionId) !== s) {
2065
+ if (sessions.get(sessionId) !== s) {
1741
2066
  cleanup();
1742
2067
  controller.close();
1743
2068
  }
@@ -1761,8 +2086,16 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
1761
2086
 
1762
2087
  // src/bundled.ts
1763
2088
  import { createHash as createHash3 } from "crypto";
1764
- import { cpSync, existsSync, rmSync } from "fs";
1765
- import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
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";
1766
2099
  import path5 from "path";
1767
2100
  import { fileURLToPath } from "url";
1768
2101
  var PKG_ROOT = path5.resolve(fileURLToPath(import.meta.url), "../..");
@@ -1771,16 +2104,39 @@ var SYSTEM_SKILLS_DIR = path5.join(BUNDLED_DIR, "system-skills");
1771
2104
  var SCRIPTS_DIR = path5.join(BUNDLED_DIR, "scripts");
1772
2105
  var SHARED_DIR = path5.join(BUNDLED_DIR, "shared");
1773
2106
  var MONOREPO_SRC = path5.resolve(PKG_ROOT, "../buda/agent-templates");
1774
- function syncTemplatesFromMonorepo() {
2107
+ function syncTemplatesFromMonorepo(options = {}) {
1775
2108
  if (!existsSync(MONOREPO_SRC)) {
1776
2109
  throw new Error(`Cannot sync agent-templates: source missing at ${MONOREPO_SRC}`);
1777
2110
  }
1778
- rmSync(BUNDLED_DIR, { recursive: true, force: true });
1779
- cpSync(MONOREPO_SRC, BUNDLED_DIR, {
1780
- recursive: true,
1781
- dereference: true,
1782
- filter: (src) => !src.split(path5.sep).includes("extensions")
1783
- });
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
+ }
1784
2140
  }
1785
2141
  ensureBundledDir();
1786
2142
  function ensureBundledDir() {
@@ -1798,7 +2154,7 @@ function ensureBundledDir() {
1798
2154
  var cached = null;
1799
2155
  async function readBundledManifest() {
1800
2156
  if (cached) return cached;
1801
- const systemSkillNames = (await readdir2(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();
1802
2158
  const version = await hashDir(BUNDLED_DIR);
1803
2159
  cached = {
1804
2160
  version,
@@ -1813,12 +2169,12 @@ async function readBundledManifest() {
1813
2169
  async function hashDir(dir) {
1814
2170
  const hash = createHash3("sha256");
1815
2171
  await walk(dir, async (relPath, abs) => {
1816
- hash.update(`${relPath}:`).update(await readFile3(abs));
2172
+ hash.update(`${relPath}:`).update(await readFile4(abs));
1817
2173
  });
1818
2174
  return hash.digest("hex");
1819
2175
  }
1820
2176
  async function walk(dir, visit, base = dir) {
1821
- const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
2177
+ const entries = await readdir3(dir, { withFileTypes: true }).catch(() => []);
1822
2178
  entries.sort((a, b) => a.name.localeCompare(b.name));
1823
2179
  for (const entry of entries) {
1824
2180
  const abs = path5.join(dir, entry.name);
@@ -1843,6 +2199,30 @@ var BASE_CONNECTOR_CAPABILITIES = [
1843
2199
  "shell",
1844
2200
  "git"
1845
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
+ }
1846
2226
  var browserAvailabilityWarningLogged = false;
1847
2227
  var getBrowserAvailabilityWarning = (browserAvailability) => browserAvailability.available ? null : `${browserAvailability.error} Browser automation is disabled, but shell, filesystem, Git, and coding capabilities remain available.`;
1848
2228
  var getAdvertisedCapabilities = (browserAvailability = resolveBrowserAvailability(), deviceToken) => [
@@ -1864,7 +2244,7 @@ async function buildPayload(config, extra) {
1864
2244
  deviceName: config.deviceName,
1865
2245
  hostLabel: config.hostLabel,
1866
2246
  platform: `${platform3()} ${release()}`,
1867
- version: "dev",
2247
+ version: CONNECTOR_RUNTIME_VERSION,
1868
2248
  pid: process.pid,
1869
2249
  workdirRoot: config.workdirRoot,
1870
2250
  baseUrl: extra?.bunnyDaemon?.baseUrl,
@@ -1893,127 +2273,1064 @@ async function buildPayload(config, extra) {
1893
2273
  ...extra?.logs?.length ? { logs: extra.logs } : {},
1894
2274
  ...extra?.recovery ? { recovery: extra.recovery } : {}
1895
2275
  };
1896
- }
1897
- function buildAuthHeaders(config, launchConfig) {
1898
- const headers = {
1899
- "X-Buda-Connector-Id": config.connectorId
2276
+ }
2277
+ function buildAuthHeaders(config, launchConfig) {
2278
+ const headers = {
2279
+ "X-Buda-Connector-Id": config.connectorId
2280
+ };
2281
+ const token = config.token?.trim() || getConnectorToken(launchConfig);
2282
+ const oauthToken = launchConfig?.oauthToken?.trim();
2283
+ if (oauthToken) headers.Authorization = `Bearer ${oauthToken}`;
2284
+ if (token) headers["X-Buda-Connector-Token"] = token;
2285
+ if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
2286
+ return headers;
2287
+ }
2288
+ async function postJson(serverUrl, pathname, body, config, launchConfig) {
2289
+ const response = await fetch(`${serverUrl}${pathname}`, {
2290
+ method: "POST",
2291
+ headers: {
2292
+ "Content-Type": "application/json",
2293
+ ...buildAuthHeaders(config, launchConfig)
2294
+ },
2295
+ body: JSON.stringify(body)
2296
+ });
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
+ }
3059
+ try {
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
+ }
3083
+ } catch {
3084
+ }
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
+ }
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
3181
+ }
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?.();
1900
3200
  };
1901
- const token = config.token?.trim() || getConnectorToken(launchConfig);
1902
- if (token) headers["X-Buda-Connector-Token"] = token;
1903
- if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
1904
- return headers;
1905
- }
1906
- async function postJson(serverUrl, pathname, body, config, launchConfig) {
1907
- const response = await fetch(`${serverUrl}${pathname}`, {
1908
- method: "POST",
1909
- headers: {
1910
- "Content-Type": "application/json",
1911
- ...buildAuthHeaders(config, launchConfig)
1912
- },
1913
- body: JSON.stringify(body)
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 });
1914
3235
  });
1915
- if (!response.ok) {
1916
- const text2 = await response.text().catch(() => "");
1917
- let errorCode;
1918
- let errorMessage3 = text2;
1919
- try {
1920
- const payload = JSON.parse(text2);
1921
- errorCode = typeof payload.code === "string" ? payload.code : void 0;
1922
- errorMessage3 = typeof payload.error === "string" ? payload.error : text2;
1923
- } catch {
3236
+ local.on("message", (data, isBinary) => {
3237
+ if (!bridgeAccepted) {
3238
+ bufferFrame(pendingLocalFrames, data, isBinary);
3239
+ return;
1924
3240
  }
1925
- if (errorCode === "device_revoked") {
1926
- throw new Error(
1927
- `This device is prohibited/revoked and cannot reconnect. Contact support. (${errorMessage3})`
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())}`
1928
3249
  );
1929
3250
  }
1930
- throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
1931
- }
1932
- return await response.json();
1933
- }
1934
- function formatConnectorOwner(connector) {
1935
- return connector.userEmail ?? connector.userId ?? "anonymous";
1936
- }
1937
- async function warnIfStaleBundle(result) {
1938
- if (!result.bundle?.staleClient) return;
1939
- await writeLog(
1940
- "warn",
1941
- `connector bundle out of date \u2014 server expects ${result.bundle.expectedVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
1942
- );
1943
- }
1944
- async function registerConnector(config, launchConfig, bunnyDaemon, options) {
1945
- const result = await postJson(
1946
- config.serverUrl,
1947
- "/api/connectors/register",
1948
- await buildPayload(config, {
1949
- bunnyDaemon,
1950
- ...options?.recoverMissingProfile ? { recovery: "missing_local_profile" } : {}
1951
- }),
1952
- config,
1953
- launchConfig
1954
- );
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" }));
1955
3265
  await writeLog(
1956
3266
  "info",
1957
- `registered connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
3267
+ `terminal proxy connected session=${input.sessionId} workspace=${input.workspaceKey}`
1958
3268
  );
1959
- 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
+ });
1960
3298
  return {
1961
- connectorId: result.connector.connectorId,
1962
- relayProtocol: result.relayProtocol
3299
+ server,
3300
+ host,
3301
+ port: actualPort,
3302
+ baseUrl,
3303
+ root
1963
3304
  };
1964
3305
  }
1965
- async function heartbeatConnector(config, launchConfig, bunnyDaemon) {
1966
- const logs = takePendingLogs();
1967
- const result = await postJson(
1968
- config.serverUrl,
1969
- "/api/connectors/heartbeat",
1970
- await buildPayload(config, {
1971
- bunnyDaemon,
1972
- logs
1973
- }),
1974
- config,
1975
- launchConfig
1976
- ).catch((error) => {
1977
- restorePendingLogs(logs);
1978
- throw error;
1979
- });
1980
- return { relayProtocol: result.relayProtocol };
1981
- }
1982
- async function unregisterConnector(config, launchConfig) {
1983
- await postJson(
1984
- config.serverUrl,
1985
- "/api/connectors/unregister",
1986
- {
1987
- connectorId: config.connectorId
1988
- },
1989
- config,
1990
- launchConfig
1991
- ).catch((error) => {
1992
- 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());
1993
3309
  });
3310
+ await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
1994
3311
  }
1995
3312
 
1996
3313
  // src/legacy-workspace-migration.ts
1997
- import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
3314
+ import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
1998
3315
  import { constants as constants2, createReadStream } from "fs";
1999
3316
  import {
2000
- chmod as chmod2,
3317
+ chmod as chmod4,
2001
3318
  copyFile,
2002
3319
  lstat,
2003
- mkdir as mkdir5,
3320
+ mkdir as mkdir8,
2004
3321
  open as open2,
2005
- readdir as readdir3,
2006
- readFile as readFile4,
3322
+ readdir as readdir4,
3323
+ readFile as readFile7,
2007
3324
  readlink,
2008
3325
  realpath,
2009
- rename as rename3,
2010
- rm,
3326
+ rename as rename5,
3327
+ rm as rm2,
2011
3328
  symlink,
2012
- unlink as unlink4
3329
+ unlink as unlink5
2013
3330
  } from "fs/promises";
2014
- import path6 from "path";
3331
+ import path9 from "path";
2015
3332
  var MIGRATION_VERSION = 1;
2016
- var MIGRATION_DIRECTORY = path6.join(".buda", "migrations", "legacy-workspace-v1");
3333
+ var MIGRATION_DIRECTORY = path9.join(".buda", "migrations", "legacy-workspace-v1");
2017
3334
  var JOURNAL_FILENAME = "journal.json";
2018
3335
  var IMMUTABLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
2019
3336
  var LEGACY_TEMPLATE_FILES = [
@@ -2049,7 +3366,7 @@ var LegacyWorkspaceMigrationError = class extends Error {
2049
3366
  recoverable = true;
2050
3367
  };
2051
3368
  var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
2052
- await mkdir5(storageRoot, { recursive: true, mode: 448 });
3369
+ await mkdir8(storageRoot, { recursive: true, mode: 448 });
2053
3370
  const rootInfo = await lstat(storageRoot);
2054
3371
  if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) {
2055
3372
  throw new LegacyWorkspaceMigrationError(
@@ -2058,9 +3375,9 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
2058
3375
  );
2059
3376
  }
2060
3377
  const canonicalRoot = await realpath(storageRoot);
2061
- const migrationDirectory = path6.join(canonicalRoot, MIGRATION_DIRECTORY);
3378
+ const migrationDirectory = path9.join(canonicalRoot, MIGRATION_DIRECTORY);
2062
3379
  await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
2063
- const journalPath = path6.join(migrationDirectory, JOURNAL_FILENAME);
3380
+ const journalPath = path9.join(migrationDirectory, JOURNAL_FILENAME);
2064
3381
  let journal = await readJournal(journalPath);
2065
3382
  if (journal) validateJournalPaths(journal, canonicalRoot, migrationDirectory);
2066
3383
  if (journal?.status === "complete") {
@@ -2093,8 +3410,8 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
2093
3410
  };
2094
3411
  var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
2095
3412
  if (item.phase === "committed") return;
2096
- if (await exists(item.target)) {
2097
- 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)) {
2098
3415
  await verifyTree(item.source, item.target);
2099
3416
  item.phase = "committed";
2100
3417
  await writeJournal(journalPath, journal);
@@ -2109,8 +3426,8 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
2109
3426
  item.phase = "copying";
2110
3427
  await writeJournal(journalPath, journal);
2111
3428
  await checkpoint(options, "before-copy", item.id);
2112
- await rm(item.staging, { recursive: true, force: true });
2113
- await ensurePrivateDirectoryWithin(storageRoot, path6.dirname(item.staging));
3429
+ await rm2(item.staging, { recursive: true, force: true });
3430
+ await ensurePrivateDirectoryWithin(storageRoot, path9.dirname(item.staging));
2114
3431
  await copyTree(item.source, item.staging, () => checkpoint(options, "during-copy", item.id));
2115
3432
  await checkpoint(options, "after-copy", item.id);
2116
3433
  item.phase = "copied";
@@ -2121,7 +3438,7 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
2121
3438
  try {
2122
3439
  await verifyTree(item.source, item.staging);
2123
3440
  } catch (error) {
2124
- await rm(item.staging, { recursive: true, force: true });
3441
+ await rm2(item.staging, { recursive: true, force: true });
2125
3442
  item.phase = "copying";
2126
3443
  await writeJournal(journalPath, journal);
2127
3444
  throw error;
@@ -2132,9 +3449,9 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
2132
3449
  }
2133
3450
  if (item.phase === "verified") {
2134
3451
  await checkpoint(options, "before-rename", item.id);
2135
- await ensurePrivateDirectoryWithin(storageRoot, path6.dirname(item.target));
2136
- await rename3(item.staging, item.target);
2137
- await syncDirectory(path6.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));
2138
3455
  await checkpoint(options, "after-rename", item.id);
2139
3456
  item.phase = "committed";
2140
3457
  await writeJournal(journalPath, journal);
@@ -2144,14 +3461,14 @@ var resumeRollback = async (journalPath, journal, options) => {
2144
3461
  for (const item of [...journal.items].reverse()) {
2145
3462
  if (item.phase === "rolled-back") continue;
2146
3463
  await checkpoint(options, "before-rollback", item.id);
2147
- if (!await exists(item.source)) {
3464
+ if (!await exists2(item.source)) {
2148
3465
  throw new LegacyWorkspaceMigrationError(
2149
3466
  "MIGRATION_VERIFY_FAILED",
2150
3467
  `Cannot roll back ${item.id}: the preserved legacy source is missing`
2151
3468
  );
2152
3469
  }
2153
- await rm(item.staging, { recursive: true, force: true });
2154
- 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 });
2155
3472
  await checkpoint(options, "after-rollback", item.id);
2156
3473
  item.phase = "rolled-back";
2157
3474
  await writeJournal(journalPath, journal);
@@ -2165,12 +3482,12 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
2165
3482
  const ownership = owner ? "explicit-owner" : "shared-legacy";
2166
3483
  const label = owner ? `Legacy Connector root assigned to ${owner.spaceId}/${owner.agentId}` : "Shared legacy Connector root (ownership unassigned)";
2167
3484
  const sources = await inventoryLegacySources(storageRoot);
2168
- const stagingRoot = path6.join(migrationDirectory, "staging", randomUUID2());
3485
+ const stagingRoot = path9.join(migrationDirectory, "staging", randomUUID5());
2169
3486
  const items = sources.map(({ id, source }) => ({
2170
3487
  id,
2171
3488
  source,
2172
3489
  target: deriveTarget(storageRoot, id, owner),
2173
- staging: path6.join(stagingRoot, id),
3490
+ staging: path9.join(stagingRoot, id),
2174
3491
  phase: "pending"
2175
3492
  }));
2176
3493
  assertDistinctTargets(items);
@@ -2187,29 +3504,29 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
2187
3504
  };
2188
3505
  };
2189
3506
  var inventoryLegacySources = async (storageRoot) => {
2190
- const rootEntries = await readdir3(storageRoot, { withFileTypes: true });
3507
+ const rootEntries = await readdir4(storageRoot, { withFileTypes: true });
2191
3508
  const templateNames = new Set(LEGACY_TEMPLATE_FILES);
2192
3509
  for (const entry of rootEntries) {
2193
3510
  if (entry.name.endsWith(".md")) templateNames.add(entry.name);
2194
3511
  }
2195
3512
  const candidates = [
2196
- { id: "agent", source: path6.join(storageRoot, "agent") },
2197
- { id: "space", source: path6.join(storageRoot, "space") },
2198
- { id: "logs", source: path6.join(storageRoot, "logs") },
2199
- { id: "connector-log", source: path6.join(storageRoot, "connector.log") },
2200
- { id: "runtime-config", source: path6.join(storageRoot, "runtime-config") },
2201
- { id: "runtime-config-hidden", source: path6.join(storageRoot, ".runtime-config") },
2202
- { id: "runtime-config-file", source: path6.join(storageRoot, "runtime-config.json") },
2203
- { id: "runtime-config-hidden-file", source: path6.join(storageRoot, ".runtime-config.json") },
2204
- { id: "bunny-runtime", source: path6.join(storageRoot, ".bunny") },
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") },
2205
3522
  ...[...templateNames].sort().map((name) => ({
2206
3523
  id: `template-${name}`,
2207
- source: path6.join(storageRoot, name)
3524
+ source: path9.join(storageRoot, name)
2208
3525
  }))
2209
3526
  ];
2210
3527
  const found = [];
2211
3528
  for (const candidate of candidates) {
2212
- if (!await exists(candidate.source)) continue;
3529
+ if (!await exists2(candidate.source)) continue;
2213
3530
  const info = await lstat(candidate.source);
2214
3531
  if (info.isSymbolicLink()) {
2215
3532
  throw new LegacyWorkspaceMigrationError(
@@ -2217,36 +3534,36 @@ var inventoryLegacySources = async (storageRoot) => {
2217
3534
  `Legacy migration source ${candidate.id} must not be a symbolic link`
2218
3535
  );
2219
3536
  }
2220
- if (info.isDirectory() && (await readdir3(candidate.source)).length === 0) continue;
3537
+ if (info.isDirectory() && (await readdir4(candidate.source)).length === 0) continue;
2221
3538
  found.push(candidate);
2222
3539
  }
2223
3540
  return found;
2224
3541
  };
2225
3542
  var deriveTarget = (storageRoot, id, owner) => {
2226
- if (!owner) return path6.join(storageRoot, ".buda", "legacy", "shared", id);
2227
- const spaceRoot = path6.join(storageRoot, ".buda", "spaces", owner.spaceId);
2228
- const agentRoot = path6.join(spaceRoot, "agents", owner.agentId);
2229
- if (id === "agent") return path6.join(agentRoot, "workspace", "legacy-connector-root");
2230
- if (id === "space") return path6.join(spaceRoot, "shared", "legacy-connector-root");
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");
2231
3548
  if (id.startsWith("template-")) {
2232
- return path6.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
3549
+ return path9.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
2233
3550
  }
2234
- return path6.join(agentRoot, "state", "legacy-connector-root", id);
3551
+ return path9.join(agentRoot, "state", "legacy-connector-root", id);
2235
3552
  };
2236
3553
  var copyTree = async (source, destination, onEntryCopied) => {
2237
3554
  const info = await lstat(source);
2238
3555
  if (info.isDirectory()) {
2239
- await mkdir5(destination, { mode: info.mode & 4095 });
2240
- const entries = (await readdir3(source)).sort();
3556
+ await mkdir8(destination, { mode: info.mode & 4095 });
3557
+ const entries = (await readdir4(source)).sort();
2241
3558
  for (const entry of entries) {
2242
- await copyTree(path6.join(source, entry), path6.join(destination, entry), onEntryCopied);
3559
+ await copyTree(path9.join(source, entry), path9.join(destination, entry), onEntryCopied);
2243
3560
  }
2244
- await chmod2(destination, info.mode & 4095);
3561
+ await chmod4(destination, info.mode & 4095);
2245
3562
  return;
2246
3563
  }
2247
3564
  if (info.isFile()) {
2248
3565
  await copyFile(source, destination, constants2.COPYFILE_EXCL);
2249
- await chmod2(destination, info.mode & 4095);
3566
+ await chmod4(destination, info.mode & 4095);
2250
3567
  await onEntryCopied();
2251
3568
  return;
2252
3569
  }
@@ -2274,8 +3591,8 @@ var verifyTree = async (source, destination) => {
2274
3591
  }
2275
3592
  if (sourceInfo.isDirectory()) {
2276
3593
  const [sourceEntries, destinationEntries] = await Promise.all([
2277
- readdir3(source),
2278
- readdir3(destination)
3594
+ readdir4(source),
3595
+ readdir4(destination)
2279
3596
  ]);
2280
3597
  sourceEntries.sort();
2281
3598
  destinationEntries.sort();
@@ -2283,7 +3600,7 @@ var verifyTree = async (source, destination) => {
2283
3600
  throw verifyFailure("directory entries differ");
2284
3601
  }
2285
3602
  for (const entry of sourceEntries) {
2286
- await verifyTree(path6.join(source, entry), path6.join(destination, entry));
3603
+ await verifyTree(path9.join(source, entry), path9.join(destination, entry));
2287
3604
  }
2288
3605
  return;
2289
3606
  }
@@ -2313,7 +3630,7 @@ var hashFile = async (filePath) => {
2313
3630
  var readJournal = async (journalPath) => {
2314
3631
  let raw;
2315
3632
  try {
2316
- raw = await readFile4(journalPath, "utf8");
3633
+ raw = await readFile7(journalPath, "utf8");
2317
3634
  } catch (error) {
2318
3635
  if (isMissing(error)) return void 0;
2319
3636
  throw error;
@@ -2325,13 +3642,13 @@ var readJournal = async (journalPath) => {
2325
3642
  throw corrupt("Legacy migration journal contains invalid JSON");
2326
3643
  }
2327
3644
  if (!isJournal(value)) throw corrupt("Legacy migration journal is malformed");
2328
- await chmod2(journalPath, 384);
3645
+ await chmod4(journalPath, 384);
2329
3646
  return value;
2330
3647
  };
2331
3648
  var writeJournal = async (journalPath, journal) => {
2332
3649
  journal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2333
- await ensurePrivateDirectory(path6.dirname(journalPath));
2334
- const temporaryPath = `${journalPath}.${process.pid}.${randomUUID2()}.tmp`;
3650
+ await ensurePrivateDirectory(path9.dirname(journalPath));
3651
+ const temporaryPath = `${journalPath}.${process.pid}.${randomUUID5()}.tmp`;
2335
3652
  const handle = await open2(
2336
3653
  temporaryPath,
2337
3654
  constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
@@ -2345,14 +3662,14 @@ var writeJournal = async (journalPath, journal) => {
2345
3662
  await handle.close();
2346
3663
  }
2347
3664
  try {
2348
- await rename3(temporaryPath, journalPath);
2349
- await chmod2(journalPath, 384);
2350
- await syncDirectory(path6.dirname(journalPath));
3665
+ await rename5(temporaryPath, journalPath);
3666
+ await chmod4(journalPath, 384);
3667
+ await syncDirectory2(path9.dirname(journalPath));
2351
3668
  } finally {
2352
- await unlink4(temporaryPath).catch(() => void 0);
3669
+ await unlink5(temporaryPath).catch(() => void 0);
2353
3670
  }
2354
3671
  };
2355
- var syncDirectory = async (directory) => {
3672
+ var syncDirectory2 = async (directory) => {
2356
3673
  const handle = await open2(directory, constants2.O_RDONLY);
2357
3674
  try {
2358
3675
  await handle.sync();
@@ -2361,7 +3678,7 @@ var syncDirectory = async (directory) => {
2361
3678
  }
2362
3679
  };
2363
3680
  var ensurePrivateDirectory = async (directory) => {
2364
- await mkdir5(directory, { mode: 448 }).catch((error) => {
3681
+ await mkdir8(directory, { mode: 448 }).catch((error) => {
2365
3682
  if (!isAlreadyExists2(error)) throw error;
2366
3683
  });
2367
3684
  const info = await lstat(directory);
@@ -2371,16 +3688,16 @@ var ensurePrivateDirectory = async (directory) => {
2371
3688
  "Legacy migration control path must be a real directory"
2372
3689
  );
2373
3690
  }
2374
- await chmod2(directory, 448);
3691
+ await chmod4(directory, 448);
2375
3692
  };
2376
3693
  var ensurePrivateDirectoryWithin = async (storageRoot, directory) => {
2377
3694
  if (directory !== storageRoot && !isPathInside(storageRoot, directory)) {
2378
3695
  throw corrupt("Legacy migration control path escapes protected storage");
2379
3696
  }
2380
- const relative = path6.relative(storageRoot, directory);
3697
+ const relative = path9.relative(storageRoot, directory);
2381
3698
  let current = storageRoot;
2382
- for (const segment of relative.split(path6.sep).filter(Boolean)) {
2383
- current = path6.join(current, segment);
3699
+ for (const segment of relative.split(path9.sep).filter(Boolean)) {
3700
+ current = path9.join(current, segment);
2384
3701
  await ensurePrivateDirectory(current);
2385
3702
  }
2386
3703
  };
@@ -2405,7 +3722,7 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
2405
3722
  const stagingPaths = /* @__PURE__ */ new Set();
2406
3723
  for (const item of journal.items) {
2407
3724
  const sourceRelative = sourceRelativePath(item.id);
2408
- if (!sourceRelative || item.source !== path6.join(storageRoot, sourceRelative)) {
3725
+ if (!sourceRelative || item.source !== path9.join(storageRoot, sourceRelative)) {
2409
3726
  throw corrupt("Legacy migration journal contains an unexpected source path");
2410
3727
  }
2411
3728
  if (item.target !== deriveTarget(storageRoot, item.id, journal.owner)) {
@@ -2414,8 +3731,8 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
2414
3731
  if (!isPathInside(migrationDirectory, item.staging)) {
2415
3732
  throw corrupt("Legacy migration journal staging path escapes the migration directory");
2416
3733
  }
2417
- const stagingParent = path6.dirname(item.staging);
2418
- if (path6.dirname(stagingParent) !== path6.join(migrationDirectory, "staging") || item.staging !== path6.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)) {
2419
3736
  throw corrupt("Legacy migration journal staging path is not canonical");
2420
3737
  }
2421
3738
  stagingParents.add(stagingParent);
@@ -2445,7 +3762,7 @@ var sourceRelativePath = (id) => {
2445
3762
  if (known[id]) return known[id];
2446
3763
  if (!id.startsWith("template-")) return null;
2447
3764
  const name = id.slice("template-".length);
2448
- return name.endsWith(".md") && path6.basename(name) === name ? name : null;
3765
+ return name.endsWith(".md") && path9.basename(name) === name ? name : null;
2449
3766
  };
2450
3767
  var validateJournalOwnership = (journal) => {
2451
3768
  if (journal.ownership === "explicit-owner") {
@@ -2508,7 +3825,7 @@ var isJournal = (value) => {
2508
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)
2509
3826
  );
2510
3827
  };
2511
- var exists = async (filePath) => {
3828
+ var exists2 = async (filePath) => {
2512
3829
  try {
2513
3830
  await lstat(filePath);
2514
3831
  return true;
@@ -2518,8 +3835,8 @@ var exists = async (filePath) => {
2518
3835
  }
2519
3836
  };
2520
3837
  var isPathInside = (parent, child) => {
2521
- const relative = path6.relative(parent, child);
2522
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
3838
+ const relative = path9.relative(parent, child);
3839
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path9.sep}`) && !path9.isAbsolute(relative);
2523
3840
  };
2524
3841
  var isMissing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
2525
3842
  var isAlreadyExists2 = (error) => error instanceof Error && "code" in error && error.code === "EEXIST";
@@ -2531,7 +3848,7 @@ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT"
2531
3848
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
2532
3849
 
2533
3850
  // src/relay.ts
2534
- import path8 from "path";
3851
+ import path11 from "path";
2535
3852
  import { createNextHandler } from "@bunny-agent/daemon/nextjs";
2536
3853
  import WebSocket2 from "ws";
2537
3854
 
@@ -2703,15 +4020,15 @@ import { constants as constants3 } from "fs";
2703
4020
  import {
2704
4021
  copyFile as copyFile2,
2705
4022
  lstat as lstat2,
2706
- mkdir as mkdir6,
2707
- readdir as readdir4,
2708
- readFile as readFile5,
4023
+ mkdir as mkdir9,
4024
+ readdir as readdir5,
4025
+ readFile as readFile8,
2709
4026
  realpath as realpath2,
2710
- rm as rm2,
4027
+ rm as rm3,
2711
4028
  symlink as symlink2,
2712
- writeFile as writeFile2
4029
+ writeFile as writeFile4
2713
4030
  } from "fs/promises";
2714
- import path7 from "path";
4031
+ import path10 from "path";
2715
4032
  var MOUNT_DIRECTORY = "mount";
2716
4033
  var ARCHIVE_MARKER = "archived.json";
2717
4034
  var AGENT_CONTEXT_TEMPLATE_FILES = [
@@ -2723,8 +4040,8 @@ var AGENT_CONTEXT_TEMPLATE_FILES = [
2723
4040
  "BOOTSTRAP.md",
2724
4041
  "MEMORY.md"
2725
4042
  ];
2726
- var deriveSystemVolume = (descriptor) => path7.join(
2727
- path7.dirname(path7.dirname(path7.dirname(path7.dirname(descriptor.sharedVolume)))),
4043
+ var deriveSystemVolume = (descriptor) => path10.join(
4044
+ path10.dirname(path10.dirname(path10.dirname(path10.dirname(descriptor.sharedVolume)))),
2728
4045
  "system"
2729
4046
  );
2730
4047
  var WorkspaceVolumeLifecycle = class {
@@ -2736,14 +4053,14 @@ var WorkspaceVolumeLifecycle = class {
2736
4053
  async mount() {
2737
4054
  await this.ensureVolumes();
2738
4055
  await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
2739
- const mountRoot = path7.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
2740
- 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 });
2741
4058
  const mounts = {
2742
4059
  mountRoot,
2743
- agent: path7.join(mountRoot, "agent"),
2744
- space: path7.join(mountRoot, "space"),
2745
- system: path7.join(mountRoot, "system"),
2746
- temporary: path7.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")
2747
4064
  };
2748
4065
  await Promise.all([
2749
4066
  ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
@@ -2751,69 +4068,69 @@ var WorkspaceVolumeLifecycle = class {
2751
4068
  ensureVolumeLink(this.systemVolume, mounts.system),
2752
4069
  ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
2753
4070
  ]);
2754
- const daemonVolumes = path7.join(mountRoot, "volumes");
2755
- await mkdir6(daemonVolumes, { recursive: true, mode: 448 });
4071
+ const daemonVolumes = path10.join(mountRoot, "volumes");
4072
+ await mkdir9(daemonVolumes, { recursive: true, mode: 448 });
2756
4073
  await Promise.all([
2757
- ensureVolumeLink(this.descriptor.privateVolume, path7.join(daemonVolumes, "agent")),
2758
- ensureVolumeLink(this.descriptor.sharedVolume, path7.join(daemonVolumes, "space")),
2759
- ensureVolumeLink(this.systemVolume, path7.join(daemonVolumes, "system")),
2760
- ensureVolumeLink(this.descriptor.temporaryVolume, path7.join(daemonVolumes, "tmp"))
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"))
2761
4078
  ]);
2762
4079
  return mounts;
2763
4080
  }
2764
4081
  async unmount() {
2765
- await rm2(path7.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
4082
+ await rm3(path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
2766
4083
  recursive: true,
2767
4084
  force: true
2768
4085
  });
2769
4086
  }
2770
4087
  async archiveAgent() {
2771
4088
  await this.unmount();
2772
- await mkdir6(this.descriptor.stateVolume, { recursive: true, mode: 448 });
2773
- await writeFile2(
2774
- path7.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),
2775
4092
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
2776
4093
  `,
2777
4094
  { encoding: "utf8", mode: 384 }
2778
4095
  );
2779
4096
  }
2780
4097
  async restoreAgent() {
2781
- await rm2(path7.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
4098
+ await rm3(path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
2782
4099
  return this.mount();
2783
4100
  }
2784
4101
  async deleteAgent(options) {
2785
4102
  requireConfirmation(options);
2786
4103
  await this.unmount();
2787
- const agentRoot = path7.dirname(this.descriptor.privateVolume);
2788
- await rm2(agentRoot, { recursive: true, force: true });
4104
+ const agentRoot = path10.dirname(this.descriptor.privateVolume);
4105
+ await rm3(agentRoot, { recursive: true, force: true });
2789
4106
  }
2790
4107
  async archiveSpace() {
2791
- await mkdir6(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
2792
- await writeFile2(
2793
- path7.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),
2794
4111
  `${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
2795
4112
  `,
2796
4113
  { encoding: "utf8", mode: 384 }
2797
4114
  );
2798
4115
  }
2799
4116
  async restoreSpace() {
2800
- await rm2(path7.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
4117
+ await rm3(path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
2801
4118
  }
2802
4119
  async deleteSpace(options) {
2803
4120
  requireConfirmation(options);
2804
4121
  await this.unmount();
2805
- await rm2(this.descriptor.sharedVolume, { recursive: true, force: true });
4122
+ await rm3(this.descriptor.sharedVolume, { recursive: true, force: true });
2806
4123
  }
2807
4124
  async bootstrapAgentTemplates(templateDirectory) {
2808
- await mkdir6(this.descriptor.privateVolume, { recursive: true, mode: 448 });
4125
+ await mkdir9(this.descriptor.privateVolume, { recursive: true, mode: 448 });
2809
4126
  const bootstrapped = [];
2810
4127
  const skipped = [];
2811
4128
  const entries = new Set(
2812
- (await readdir4(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)
2813
4130
  );
2814
4131
  for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
2815
4132
  if (!entries.has(filename)) continue;
2816
- const destination = path7.join(this.descriptor.privateVolume, filename);
4133
+ const destination = path10.join(this.descriptor.privateVolume, filename);
2817
4134
  try {
2818
4135
  await lstat2(destination);
2819
4136
  skipped.push(filename);
@@ -2821,7 +4138,7 @@ var WorkspaceVolumeLifecycle = class {
2821
4138
  if (!isMissingFile(error)) throw error;
2822
4139
  try {
2823
4140
  await copyFile2(
2824
- path7.join(templateDirectory, filename),
4141
+ path10.join(templateDirectory, filename),
2825
4142
  destination,
2826
4143
  constants3.COPYFILE_EXCL
2827
4144
  );
@@ -2842,7 +4159,7 @@ var WorkspaceVolumeLifecycle = class {
2842
4159
  this.descriptor.temporaryVolume,
2843
4160
  this.systemVolume
2844
4161
  ]) {
2845
- await mkdir6(volume, { recursive: true, mode: 448 });
4162
+ await mkdir9(volume, { recursive: true, mode: 448 });
2846
4163
  const info = await lstat2(volume);
2847
4164
  if (info.isSymbolicLink() || !info.isDirectory() || await realpath2(volume) !== volume) {
2848
4165
  throw new Error("Workspace volume must be a canonical real directory");
@@ -2926,6 +4243,8 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2926
4243
  let reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
2927
4244
  let hadOpenConnection = false;
2928
4245
  let activeRelayProtocol = relayProtocol;
4246
+ let currentInflight = /* @__PURE__ */ new Map();
4247
+ let activeTerminalTunnels = 0;
2929
4248
  const replayGuard = new ConnectorExecutionReplayGuard();
2930
4249
  const clearTimers = () => {
2931
4250
  if (pingTimer) {
@@ -2979,6 +4298,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
2979
4298
  });
2980
4299
  socket = ws;
2981
4300
  const inflight = /* @__PURE__ */ new Map();
4301
+ currentInflight = inflight;
2982
4302
  let detached = false;
2983
4303
  const detach = () => {
2984
4304
  if (detached) return;
@@ -3024,7 +4344,15 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
3024
4344
  replayGuard,
3025
4345
  workspaceRegistry,
3026
4346
  config.serverUrl,
3027
- bunnyDaemon.baseUrl
4347
+ bunnyDaemon.baseUrl,
4348
+ {
4349
+ onTerminalOpen: () => {
4350
+ activeTerminalTunnels += 1;
4351
+ },
4352
+ onTerminalClose: () => {
4353
+ activeTerminalTunnels = Math.max(0, activeTerminalTunnels - 1);
4354
+ }
4355
+ }
3028
4356
  );
3029
4357
  });
3030
4358
  ws.on("close", (code) => {
@@ -3056,6 +4384,12 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
3056
4384
  scheduleReconnect("connect-throw");
3057
4385
  }
3058
4386
  return {
4387
+ isIdle() {
4388
+ return currentInflight.size === 0 && activeTerminalTunnels === 0;
4389
+ },
4390
+ isConnected() {
4391
+ return socket?.readyState === WebSocket2.OPEN && !suspendedForAccessRevocation;
4392
+ },
3059
4393
  updateRelayProtocol(nextRelayProtocol) {
3060
4394
  const previousKeyId = activeRelayProtocol?.keyId;
3061
4395
  activeRelayProtocol = nextRelayProtocol;
@@ -3087,7 +4421,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
3087
4421
  }
3088
4422
  };
3089
4423
  }
3090
- async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl) {
4424
+ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity) {
3091
4425
  if (!socket || socket.readyState !== WebSocket2.OPEN) return;
3092
4426
  let message;
3093
4427
  try {
@@ -3141,7 +4475,9 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
3141
4475
  cloudToken: terminal.token,
3142
4476
  localBaseUrl: localDaemonBaseUrl,
3143
4477
  localToken,
3144
- workspaceKey: descriptor.workspaceKey
4478
+ workspaceKey: descriptor.workspaceKey,
4479
+ onActivityStart: activity?.onTerminalOpen,
4480
+ onActivityEnd: activity?.onTerminalClose
3145
4481
  });
3146
4482
  } catch (error) {
3147
4483
  await writeLog(
@@ -3334,12 +4670,12 @@ var normalizeWorkspaceRequest = (request, mounts) => {
3334
4670
  }
3335
4671
  const body = request.body;
3336
4672
  const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
3337
- const normalized = path8.posix.normalize(cwd);
4673
+ const normalized = path11.posix.normalize(cwd);
3338
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)] : [];
3339
4675
  if (!mount || relative === void 0) {
3340
4676
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
3341
4677
  }
3342
- return { ...request, body: { ...body, cwd: path8.join(mount, relative) } };
4678
+ return { ...request, body: { ...body, cwd: path11.join(mount, relative) } };
3343
4679
  };
3344
4680
  function sendRelayError(socket, requestId, error) {
3345
4681
  sendRelayMessage(socket, {
@@ -3370,14 +4706,14 @@ function getHeaderValue(headers, name) {
3370
4706
  }
3371
4707
 
3372
4708
  // src/workdir-layout.ts
3373
- import { lstat as lstat3, mkdir as mkdir7, symlink as symlink3, unlink as unlink5 } from "fs/promises";
3374
- import path9 from "path";
4709
+ import { lstat as lstat3, mkdir as mkdir10, symlink as symlink3, unlink as unlink6 } from "fs/promises";
4710
+ import path12 from "path";
3375
4711
  async function provisionWorkdirLayout(workdirRoot) {
3376
4712
  const manifest = await readBundledManifest();
3377
- await mkdir7(workdirRoot, { recursive: true });
3378
- await mkdir7(path9.join(workdirRoot, "system", ".agents"), { recursive: true });
3379
- const systemSkillsLink = path9.join(workdirRoot, "system", ".agents", "skills");
3380
- const systemScriptsLink = path9.join(workdirRoot, "system", "scripts");
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");
3381
4717
  await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
3382
4718
  await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
3383
4719
  await writeLog("info", "workdir layout provisioned", {
@@ -3397,7 +4733,7 @@ async function ensureSymlink(target, linkPath) {
3397
4733
  try {
3398
4734
  const existing = await lstat3(linkPath);
3399
4735
  if (existing.isSymbolicLink()) {
3400
- await unlink5(linkPath);
4736
+ await unlink6(linkPath);
3401
4737
  } else if (existing.isDirectory()) {
3402
4738
  await writeLog(
3403
4739
  "warn",
@@ -3405,28 +4741,28 @@ async function ensureSymlink(target, linkPath) {
3405
4741
  );
3406
4742
  return;
3407
4743
  } else {
3408
- await unlink5(linkPath);
4744
+ await unlink6(linkPath);
3409
4745
  }
3410
4746
  } catch {
3411
4747
  }
3412
- await mkdir7(path9.dirname(linkPath), { recursive: true });
4748
+ await mkdir10(path12.dirname(linkPath), { recursive: true });
3413
4749
  await symlink3(target, linkPath, "dir");
3414
4750
  }
3415
4751
 
3416
4752
  // src/workspace-registry.ts
3417
4753
  import { constants as constants4 } from "fs";
3418
4754
  import {
3419
- chmod as chmod3,
4755
+ chmod as chmod5,
3420
4756
  lstat as lstat4,
3421
- mkdir as mkdir8,
4757
+ mkdir as mkdir11,
3422
4758
  open as open3,
3423
- readFile as readFile6,
4759
+ readFile as readFile9,
3424
4760
  realpath as realpath3,
3425
- rename as rename4,
4761
+ rename as rename6,
3426
4762
  statfs,
3427
- unlink as unlink6
4763
+ unlink as unlink7
3428
4764
  } from "fs/promises";
3429
- import path10 from "path";
4765
+ import path13 from "path";
3430
4766
  var REGISTRY_VERSION = 1;
3431
4767
  var REGISTRY_DIRECTORY = ".buda";
3432
4768
  var REGISTRY_FILENAME = "workspace-registry.json";
@@ -3447,16 +4783,16 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
3447
4783
  this.document = document;
3448
4784
  }
3449
4785
  static async open(storageRoot) {
3450
- await mkdir8(storageRoot, { recursive: true, mode: 448 });
4786
+ await mkdir11(storageRoot, { recursive: true, mode: 448 });
3451
4787
  await rejectUnsafeFilesystem(storageRoot);
3452
4788
  const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
3453
- const registryDirectory = path10.join(canonicalRoot, REGISTRY_DIRECTORY);
4789
+ const registryDirectory = path13.join(canonicalRoot, REGISTRY_DIRECTORY);
3454
4790
  await ensurePrivateDirectory2(registryDirectory);
3455
- const registryPath = path10.join(registryDirectory, REGISTRY_FILENAME);
4791
+ const registryPath = path13.join(registryDirectory, REGISTRY_FILENAME);
3456
4792
  const document = await withExclusiveFileLock(registryPath, async () => {
3457
4793
  let current;
3458
4794
  try {
3459
- current = parseRegistry(await readFile6(registryPath, "utf8"));
4795
+ current = parseRegistry(await readFile9(registryPath, "utf8"));
3460
4796
  } catch (error) {
3461
4797
  if (isMissingFile2(error)) {
3462
4798
  current = { version: REGISTRY_VERSION, storageRoot: canonicalRoot, workspaces: {} };
@@ -3470,11 +4806,11 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
3470
4806
  if (current.storageRoot !== canonicalRoot) {
3471
4807
  throw corrupt2("Registry storage root does not match the selected storage root");
3472
4808
  }
3473
- await chmod3(registryPath, 384);
4809
+ await chmod5(registryPath, 384);
3474
4810
  await reconcileDocument(current, canonicalRoot);
3475
4811
  return current;
3476
4812
  });
3477
- await chmod3(registryPath, 384);
4813
+ await chmod5(registryPath, 384);
3478
4814
  return new _LocalWorkspaceRegistry(registryPath, canonicalRoot, document);
3479
4815
  }
3480
4816
  async resolve(spaceId, agentId) {
@@ -3521,23 +4857,23 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
3521
4857
  };
3522
4858
  var readRegistryDocument = async (registryPath) => {
3523
4859
  try {
3524
- return parseRegistry(await readFile6(registryPath, "utf8"));
4860
+ return parseRegistry(await readFile9(registryPath, "utf8"));
3525
4861
  } catch (error) {
3526
4862
  if (error instanceof WorkspaceRegistryError) throw error;
3527
4863
  throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
3528
4864
  }
3529
4865
  };
3530
4866
  var deriveWorkspace = (storageRoot, spaceId, agentId) => {
3531
- const spaceRoot = path10.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
3532
- const agentRoot = path10.join(spaceRoot, "agents", agentId);
4867
+ const spaceRoot = path13.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
4868
+ const agentRoot = path13.join(spaceRoot, "agents", agentId);
3533
4869
  return {
3534
4870
  workspaceKey: `${spaceId}:${agentId}`,
3535
4871
  spaceId,
3536
4872
  agentId,
3537
- privateVolume: path10.join(agentRoot, "workspace"),
3538
- sharedVolume: path10.join(spaceRoot, "shared"),
3539
- stateVolume: path10.join(agentRoot, "state"),
3540
- temporaryVolume: path10.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")
3541
4877
  };
3542
4878
  };
3543
4879
  var parseRegistry = (raw) => {
@@ -3619,12 +4955,12 @@ var assertNoCollision = (workspaces, candidate) => {
3619
4955
  }
3620
4956
  };
3621
4957
  var ensurePrivateDirectory2 = async (directory) => {
3622
- await mkdir8(directory, { recursive: true, mode: 448 });
4958
+ await mkdir11(directory, { recursive: true, mode: 448 });
3623
4959
  const info = await lstat4(directory);
3624
4960
  if (info.isSymbolicLink() || !info.isDirectory()) {
3625
4961
  throw corrupt2("Workspace registry path is not a real directory");
3626
4962
  }
3627
- await chmod3(directory, 448);
4963
+ await chmod5(directory, 448);
3628
4964
  };
3629
4965
  var canonicalDirectory = async (directory, label) => {
3630
4966
  const info = await lstat4(directory);
@@ -3658,10 +4994,10 @@ var writeRegistryAtomically = async (registryPath, document) => {
3658
4994
  await handle.close();
3659
4995
  }
3660
4996
  try {
3661
- await rename4(temporaryPath, registryPath);
3662
- await chmod3(registryPath, 384);
4997
+ await rename6(temporaryPath, registryPath);
4998
+ await chmod5(registryPath, 384);
3663
4999
  } catch (error) {
3664
- await unlink6(temporaryPath).catch(() => void 0);
5000
+ await unlink7(temporaryPath).catch(() => void 0);
3665
5001
  throw error;
3666
5002
  }
3667
5003
  };
@@ -3684,8 +5020,8 @@ var assertContained = (root, candidate) => {
3684
5020
  if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
3685
5021
  };
3686
5022
  var isPathInside2 = (parent, child) => {
3687
- const relative = path10.relative(parent, child);
3688
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path10.sep}`) && !path10.isAbsolute(relative);
5023
+ const relative = path13.relative(parent, child);
5024
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path13.sep}`) && !path13.isAbsolute(relative);
3689
5025
  };
3690
5026
  var toDescriptor = (workspace) => ({
3691
5027
  workspaceKey: workspace.workspaceKey,
@@ -3761,10 +5097,12 @@ async function runOnboard(launchConfig, options = {}) {
3761
5097
  await provisionWorkdirLayout(config2.workdirRoot);
3762
5098
  try {
3763
5099
  const registration = await registerConnector(config2, launchConfig, void 0, {
3764
- recoverMissingProfile: true
5100
+ recoverMissingProfile: false
3765
5101
  });
3766
5102
  config2.connectorId = registration.connectorId;
5103
+ config2.spaceId = registration.spaceId ?? config2.spaceId;
3767
5104
  relayProtocol = registration.relayProtocol;
5105
+ if (!config2.spaceId) throw new Error("Registration did not resolve a Space");
3768
5106
  await writeDeviceConfig(config2);
3769
5107
  } catch (error) {
3770
5108
  s2.stop("Registration failed");
@@ -3854,8 +5192,31 @@ async function runOnboard(launchConfig, options = {}) {
3854
5192
  }
3855
5193
 
3856
5194
  // src/runtime.ts
3857
- import { mkdir as mkdir9 } from "fs/promises";
3858
- import path11 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
+ }
3859
5220
  async function runNewConnector(launchConfig) {
3860
5221
  const existing = await readDeviceConfig(launchConfig);
3861
5222
  const config = await ensureDeviceConfig(launchConfig);
@@ -3864,27 +5225,41 @@ async function runNewConnector(launchConfig) {
3864
5225
  await provisionWorkdirLayout(config.workdirRoot);
3865
5226
  await LocalWorkspaceRegistry.open(config.workdirRoot);
3866
5227
  const registration = await registerConnector(config, launchConfig, void 0, {
3867
- recoverMissingProfile: !existing
5228
+ recoverMissingProfile: shouldRecoverMissingProfile(Boolean(existing), launchConfig)
3868
5229
  });
3869
5230
  config.connectorId = registration.connectorId;
5231
+ config.spaceId = registration.spaceId ?? config.spaceId;
3870
5232
  await writeDeviceConfig(config);
3871
5233
  await writeLog("info", `wrote ${paths.configPath}`);
3872
5234
  await writeLog("info", `workdir root ${config.workdirRoot}`);
3873
5235
  await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
3874
5236
  }
3875
- async function runConnectorDaemon(launchConfig) {
3876
- const existing = await readDeviceConfig(launchConfig);
3877
- const config = await ensureDeviceConfig(launchConfig);
3878
- const paths = getConnectorPaths(config.serverUrl);
3879
- const daemonLockTarget = getConnectorDaemonLockTarget(config.serverUrl, config.connectorId);
3880
- await mkdir9(path11.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 });
3881
5253
  let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
3882
5254
  throw new Error(
3883
5255
  `Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
3884
5256
  );
3885
5257
  });
5258
+ const restoreConsole = options.captureConsole ? installConnectorConsoleCapture() : void 0;
3886
5259
  let bunnyDaemon;
3887
5260
  let relayProtocol;
5261
+ let control;
5262
+ let lastHeartbeatAt;
3888
5263
  try {
3889
5264
  await migrateLegacyWorkspaceRoot(config.workdirRoot);
3890
5265
  await provisionWorkdirLayout(config.workdirRoot);
@@ -3892,15 +5267,22 @@ async function runConnectorDaemon(launchConfig) {
3892
5267
  bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
3893
5268
  const activeBunnyDaemon = bunnyDaemon;
3894
5269
  const registration = await registerConnector(config, launchConfig, activeBunnyDaemon, {
3895
- recoverMissingProfile: !existing
5270
+ recoverMissingProfile: shouldRecoverMissingProfile(Boolean(existing), launchConfig)
3896
5271
  });
3897
5272
  const connectorIdChanged = config.connectorId !== registration.connectorId;
3898
5273
  if (connectorIdChanged) {
5274
+ const registeredConfig = {
5275
+ ...config,
5276
+ connectorId: registration.connectorId,
5277
+ spaceId: registration.spaceId ?? config.spaceId
5278
+ };
5279
+ await stopRecoveredIdleDaemon(registeredConfig, configRoot);
3899
5280
  const registeredLockTarget = getConnectorDaemonLockTarget(
3900
5281
  config.serverUrl,
3901
- registration.connectorId
5282
+ registration.connectorId,
5283
+ configRoot
3902
5284
  );
3903
- await mkdir9(path11.dirname(registeredLockTarget), { recursive: true, mode: 448 });
5285
+ await mkdir12(path14.dirname(registeredLockTarget), { recursive: true, mode: 448 });
3904
5286
  const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
3905
5287
  timeoutMs: 0
3906
5288
  }).catch(() => {
@@ -3912,8 +5294,15 @@ async function runConnectorDaemon(launchConfig) {
3912
5294
  daemonLock = registeredLock;
3913
5295
  }
3914
5296
  config.connectorId = registration.connectorId;
5297
+ setConnectorLogContext({ connectorId: config.connectorId });
5298
+ config.spaceId = registration.spaceId ?? config.spaceId;
3915
5299
  relayProtocol = registration.relayProtocol;
3916
- 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
+ }
3917
5306
  const relay = startConnectorRelay(
3918
5307
  config,
3919
5308
  activeBunnyDaemon,
@@ -3928,6 +5317,7 @@ async function runConnectorDaemon(launchConfig) {
3928
5317
  await writeLog("info", `bunny daemon ${activeBunnyDaemon.baseUrl}`);
3929
5318
  const interval = setInterval(() => {
3930
5319
  heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
5320
+ lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
3931
5321
  relay.updateRelayProtocol(nextRelayProtocol);
3932
5322
  relay.resumeAfterAccessRestore();
3933
5323
  updateConnectorDashboardStatus({
@@ -3951,18 +5341,32 @@ async function runConnectorDaemon(launchConfig) {
3951
5341
  await unregisterConnector(config, launchConfig);
3952
5342
  await stopEmbeddedBunnyDaemon(activeBunnyDaemon);
3953
5343
  } finally {
5344
+ await control?.close();
3954
5345
  await daemonLock.release();
3955
- process.exit(0);
5346
+ await restoreConsole?.();
5347
+ if (options.exitOnShutdown !== false) process.exit(0);
3956
5348
  }
3957
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
+ });
3958
5360
  process.once("SIGINT", () => void shutdown());
3959
5361
  process.once("SIGTERM", () => void shutdown());
3960
5362
  } catch (error) {
3961
5363
  if (bunnyDaemon) await stopEmbeddedBunnyDaemon(bunnyDaemon);
3962
5364
  await daemonLock.release();
5365
+ await restoreConsole?.();
3963
5366
  throw error;
3964
5367
  }
3965
5368
  }
5369
+ var shouldRecoverMissingProfile = (hasExistingProfile, launchConfig) => !hasExistingProfile && Boolean(getConnectorToken(launchConfig)) && !launchConfig?.oauthToken?.trim();
3966
5370
 
3967
5371
  // src/cli.ts
3968
5372
  function printHelp() {
@@ -3980,6 +5384,7 @@ function readCliLaunchConfig() {
3980
5384
  const bunnyDaemonPort = readFlagValue("--bunny-daemon-port");
3981
5385
  return {
3982
5386
  serverUrl: readFlagValue("--server-url"),
5387
+ spaceId: readFlagValue("--space-id"),
3983
5388
  workdirRoot: readFlagValue("--workdir"),
3984
5389
  oauthToken: readFlagValue("--oauth-token"),
3985
5390
  token: readFlagValue("--token"),
@@ -4027,13 +5432,15 @@ async function main() {
4027
5432
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
4028
5433
  const defaultCommand = isTTY ? "onboard" : "daemon";
4029
5434
  const command = !rawCommand || rawCommand.startsWith("-") ? defaultCommand : rawCommand;
5435
+ const configRoot = readFlagValue("--config-root");
5436
+ if (configRoot) process.env.BUDA_CONNECTOR_CONFIG_ROOT = configRoot;
4030
5437
  process.on("unhandledRejection", (reason) => {
4031
5438
  console.error("[buda-connector] unhandledRejection:", reason);
4032
5439
  });
4033
5440
  process.on("uncaughtException", (error) => {
4034
5441
  console.error("[buda-connector] uncaughtException:", error);
4035
5442
  });
4036
- const launchConfig = await readLaunchConfig();
5443
+ const launchConfig = process.argv.includes("--internal-daemon") ? getInternalLaunchConfig() : await readLaunchConfig();
4037
5444
  const plain = process.argv.includes("--plain");
4038
5445
  if (command === "new") {
4039
5446
  await runNewConnector(launchConfig);
@@ -4043,11 +5450,31 @@ async function main() {
4043
5450
  await runOnboard(launchConfig, { plain });
4044
5451
  return;
4045
5452
  }
4046
- if (command === "dev" || command === "daemon") {
5453
+ if (command === "dev" || process.argv.includes("--foreground")) {
4047
5454
  startConnectorConsoleDashboard({ plain });
4048
5455
  await runConnectorDaemon(launchConfig);
4049
5456
  return;
4050
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
+ }
4051
5478
  printHelp();
4052
5479
  process.exit(1);
4053
5480
  }