@band-app/server 0.47.0 → 0.48.1

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.
Files changed (75) hide show
  1. package/dist/client/_shell.html +0 -0
  2. package/dist/client/assets/{TerminalPanel-CQGF9Qku.js → TerminalPanel-CfWUo3Nd.js} +1 -1
  3. package/dist/client/assets/{_basePickBy-D64wD5rn.js → _basePickBy-De_YMUJc.js} +1 -1
  4. package/dist/client/assets/{_baseUniq-pND9PMLH.js → _baseUniq-CF2c9fMp.js} +1 -1
  5. package/dist/client/assets/{arc-Bilmh-jP.js → arc-D-KYih_I.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-VXUJARFQ-KfIpeO-l.js → architectureDiagram-VXUJARFQ-i0rZW9Y9.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-VD42YOAC-DtuyRI-v.js → blockDiagram-VD42YOAC-CI-wLyQa.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-YG6GDRKO-CBuSt_Sq.js → c4Diagram-YG6GDRKO-C1kVgBMI.js} +1 -1
  9. package/dist/client/assets/channel-Hy94D-AA.js +1 -0
  10. package/dist/client/assets/{chunk-4BX2VUAB-CWIj0g_M.js → chunk-4BX2VUAB-BZ1Kpo6f.js} +1 -1
  11. package/dist/client/assets/{chunk-55IACEB6-YWkCYv9O.js → chunk-55IACEB6-CiRlX7k-.js} +1 -1
  12. package/dist/client/assets/{chunk-B4BG7PRW-muM18MGT.js → chunk-B4BG7PRW-BcgDiAdd.js} +1 -1
  13. package/dist/client/assets/{chunk-DI55MBZ5-DUKyYScZ.js → chunk-DI55MBZ5-RWxMQGLt.js} +1 -1
  14. package/dist/client/assets/{chunk-FMBD7UC4-YTfHsy8G.js → chunk-FMBD7UC4-BwIR0iJB.js} +1 -1
  15. package/dist/client/assets/{chunk-QN33PNHL-BdIATH5l.js → chunk-QN33PNHL-_ihIB9E1.js} +1 -1
  16. package/dist/client/assets/{chunk-QZHKN3VN-DqRnJnIm.js → chunk-QZHKN3VN-CktWNnxj.js} +1 -1
  17. package/dist/client/assets/{chunk-TZMSLE5B-DOL-jYtj.js → chunk-TZMSLE5B-Br831bNt.js} +1 -1
  18. package/dist/client/assets/classDiagram-2ON5EDUG-Cwx4pAtm.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-WZHVMYZB-Cwx4pAtm.js +1 -0
  20. package/dist/client/assets/clone-BKfvT5A4.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-S5V4N54A-Cffv5KWs.js → cose-bilkent-S5V4N54A-Ca6F1OCk.js} +1 -1
  22. package/dist/client/assets/{dagre-6UL2VRFP-lIXclAkR.js → dagre-6UL2VRFP-B0-4g4us.js} +1 -1
  23. package/dist/client/assets/{diagram-PSM6KHXK-BBQxBoNY.js → diagram-PSM6KHXK-Bio1qnrB.js} +1 -1
  24. package/dist/client/assets/{diagram-QEK2KX5R-BkC_Y-i9.js → diagram-QEK2KX5R-COoEbw7t.js} +1 -1
  25. package/dist/client/assets/{diagram-S2PKOQOG-Culf_egi.js → diagram-S2PKOQOG-D8lVMqVK.js} +1 -1
  26. package/dist/client/assets/{erDiagram-Q2GNP2WA-D8qVM8rI.js → erDiagram-Q2GNP2WA-t9P0TS6q.js} +1 -1
  27. package/dist/client/assets/{flowDiagram-NV44I4VS-D_CW6QJO.js → flowDiagram-NV44I4VS-qikZ7jp7.js} +1 -1
  28. package/dist/client/assets/{ganttDiagram-JELNMOA3-DeYroA3a.js → ganttDiagram-JELNMOA3--M9lMYuY.js} +1 -1
  29. package/dist/client/assets/{gitGraphDiagram-V2S2FVAM-a_sS5cSL.js → gitGraphDiagram-V2S2FVAM-B0JlDkQD.js} +1 -1
  30. package/dist/client/assets/{graph-BiyE3_7j.js → graph-DSNlIXAB.js} +1 -1
  31. package/dist/client/assets/{highlighted-body-B3W2YXNL-HVIXx0Ob.js → highlighted-body-B3W2YXNL-kAw1lMr_.js} +1 -1
  32. package/dist/client/assets/{index-CqPtl_El.js → index--3ZFGGnZ.js} +1 -1
  33. package/dist/client/assets/{index-DyvxDAAr.js → index-5WQX4UIw.js} +1 -1
  34. package/dist/client/assets/{index-BKkkwDGt.js → index-BlF06PC1.js} +1 -1
  35. package/dist/client/assets/{index-BuC5LY4k.js → index-CmkiQRLV.js} +1 -1
  36. package/dist/client/assets/{index-LKhS8iLe.js → index-CySzBB0C.js} +1 -1
  37. package/dist/client/assets/{index-NNpxvI8j.js → index-DFPad7x_.js} +1 -1
  38. package/dist/client/assets/{index-REiX7RF3.js → index-DMYwO0S-.js} +1 -1
  39. package/dist/client/assets/{index-DOFpEx7l.js → index-DYX4kWNp.js} +1 -1
  40. package/dist/client/assets/{index-Cnvsv3TN.js → index-Dga5q50h.js} +1 -1
  41. package/dist/client/assets/{index-CcH0k4fG.js → index-DrK2f7Cq.js} +1 -1
  42. package/dist/client/assets/{index-WgCV2JjC.js → index-VnrkP6Up.js} +1 -1
  43. package/dist/client/assets/{index-B1Qa5REm.js → index-bX18a8nz.js} +1 -1
  44. package/dist/client/assets/{index-CMHYIEyK.js → index-k-FABVhS.js} +1 -1
  45. package/dist/client/assets/{infoDiagram-HS3SLOUP-BfzBdZLM.js → infoDiagram-HS3SLOUP-CaK1_LNY.js} +1 -1
  46. package/dist/client/assets/{journeyDiagram-XKPGCS4Q-CUsUym-8.js → journeyDiagram-XKPGCS4Q-ahkgpOr3.js} +1 -1
  47. package/dist/client/assets/{kanban-definition-3W4ZIXB7-b54i6dX1.js → kanban-definition-3W4ZIXB7-DrsLcNHB.js} +1 -1
  48. package/dist/client/assets/{layout-ztlxnOA9.js → layout-kBr-ByLR.js} +1 -1
  49. package/dist/client/assets/main-BubyOwBK.css +1 -0
  50. package/dist/client/assets/{main-CCOutd_8.js → main-Tc53m1i8.js} +273 -273
  51. package/dist/client/assets/{mindmap-definition-VGOIOE7T-p7poZwfk.js → mindmap-definition-VGOIOE7T-CHMj9x7w.js} +1 -1
  52. package/dist/client/assets/{pieDiagram-ADFJNKIX-DNua1MUn.js → pieDiagram-ADFJNKIX-JtGh9CwE.js} +1 -1
  53. package/dist/client/assets/{quadrantDiagram-AYHSOK5B-CminxoD5.js → quadrantDiagram-AYHSOK5B-BtkeplJz.js} +1 -1
  54. package/dist/client/assets/{requirementDiagram-UZGBJVZJ-D3aBt82J.js → requirementDiagram-UZGBJVZJ-DSIYZLch.js} +1 -1
  55. package/dist/client/assets/{sankeyDiagram-TZEHDZUN-B65x-vl8.js → sankeyDiagram-TZEHDZUN-DQGg1SzY.js} +1 -1
  56. package/dist/client/assets/{sequenceDiagram-WL72ISMW-DswVb1N3.js → sequenceDiagram-WL72ISMW-BLkWIPyC.js} +1 -1
  57. package/dist/client/assets/{stateDiagram-FKZM4ZOC-BScRlCMH.js → stateDiagram-FKZM4ZOC-BuPqxfpf.js} +1 -1
  58. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-DSMlIvqa.js +1 -0
  59. package/dist/client/assets/{timeline-definition-IT6M3QCI-C5AHVri5.js → timeline-definition-IT6M3QCI-vh4dIOet.js} +1 -1
  60. package/dist/client/assets/{treemap-GDKQZRPO-B4F1SV97.js → treemap-GDKQZRPO-C9Mr454A.js} +1 -1
  61. package/dist/client/assets/{worktree._worktreeId-DOkSR4Iv.js → worktree._worktreeId-COUtiXXq.js} +1 -1
  62. package/dist/client/assets/{worktree._worktreeId-Da_9lTBD.js → worktree._worktreeId-DYrSBo4e.js} +1 -1
  63. package/dist/client/assets/{xychartDiagram-PRI3JC2R-dZFFrnv9.js → xychartDiagram-PRI3JC2R-DTTZebCh.js} +1 -1
  64. package/dist/migrations/20261012190000_agent_status_states/migration.sql +4 -0
  65. package/dist/migrations/20261012190000_agent_status_states/snapshot.json +4409 -0
  66. package/dist/openapi.json +622 -18
  67. package/dist/start-server.mjs +1661 -661
  68. package/dist/terminal-daemon.mjs +149 -55
  69. package/package.json +5 -5
  70. package/dist/client/assets/channel-CmV-Qsz8.js +0 -1
  71. package/dist/client/assets/classDiagram-2ON5EDUG-frHxX5yV.js +0 -1
  72. package/dist/client/assets/classDiagram-v2-WZHVMYZB-frHxX5yV.js +0 -1
  73. package/dist/client/assets/clone-6GXe8GJt.js +0 -1
  74. package/dist/client/assets/main-D4-4L0eG.css +0 -1
  75. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-B5QaGcyC.js +0 -1
@@ -1329,7 +1329,7 @@ var require_sonic_boom = __commonJS({
1329
1329
  var kContentModeUtf8 = "utf8";
1330
1330
  var [major, minor] = (process.versions.node || "0.0").split(".").map(Number);
1331
1331
  var kCopyBuffer = major >= 22 && minor >= 7;
1332
- function openFile(file2, sonic) {
1332
+ function openFile(file3, sonic) {
1333
1333
  sonic._opening = true;
1334
1334
  sonic._writing = true;
1335
1335
  sonic._asyncDrainScheduled = false;
@@ -1351,7 +1351,7 @@ var require_sonic_boom = __commonJS({
1351
1351
  }
1352
1352
  const reopening = sonic._reopening;
1353
1353
  sonic.fd = fd;
1354
- sonic.file = file2;
1354
+ sonic.file = file3;
1355
1355
  sonic._reopening = false;
1356
1356
  sonic._opening = false;
1357
1357
  sonic._writing = false;
@@ -1373,20 +1373,20 @@ var require_sonic_boom = __commonJS({
1373
1373
  const mode = sonic.mode;
1374
1374
  if (sonic.sync) {
1375
1375
  try {
1376
- if (sonic.mkdir) fs3.mkdirSync(path2.dirname(file2), { recursive: true });
1377
- const fd = fs3.openSync(file2, flags, mode);
1376
+ if (sonic.mkdir) fs3.mkdirSync(path2.dirname(file3), { recursive: true });
1377
+ const fd = fs3.openSync(file3, flags, mode);
1378
1378
  fileOpened(null, fd);
1379
1379
  } catch (err) {
1380
1380
  fileOpened(err);
1381
1381
  throw err;
1382
1382
  }
1383
1383
  } else if (sonic.mkdir) {
1384
- fs3.mkdir(path2.dirname(file2), { recursive: true }, (err) => {
1384
+ fs3.mkdir(path2.dirname(file3), { recursive: true }, (err) => {
1385
1385
  if (err) return fileOpened(err);
1386
- fs3.open(file2, flags, mode, fileOpened);
1386
+ fs3.open(file3, flags, mode, fileOpened);
1387
1387
  });
1388
1388
  } else {
1389
- fs3.open(file2, flags, mode, fileOpened);
1389
+ fs3.open(file3, flags, mode, fileOpened);
1390
1390
  }
1391
1391
  }
1392
1392
  function SonicBoom(opts) {
@@ -1692,13 +1692,13 @@ var require_sonic_boom = __commonJS({
1692
1692
  }
1693
1693
  this._actualWrite();
1694
1694
  }
1695
- SonicBoom.prototype.reopen = function(file2) {
1695
+ SonicBoom.prototype.reopen = function(file3) {
1696
1696
  if (this.destroyed) {
1697
1697
  throw new Error("SonicBoom destroyed");
1698
1698
  }
1699
1699
  if (this._opening) {
1700
1700
  this.once("ready", () => {
1701
- this.reopen(file2);
1701
+ this.reopen(file3);
1702
1702
  });
1703
1703
  return;
1704
1704
  }
@@ -1708,8 +1708,8 @@ var require_sonic_boom = __commonJS({
1708
1708
  if (!this.file) {
1709
1709
  throw new Error("Unable to reopen a file descriptor, you must pass a file to SonicBoom");
1710
1710
  }
1711
- if (file2) {
1712
- this.file = file2;
1711
+ if (file3) {
1712
+ this.file = file3;
1713
1713
  }
1714
1714
  this._reopening = true;
1715
1715
  if (this._writing) {
@@ -2326,7 +2326,7 @@ var require_thread_stream = __commonJS({
2326
2326
  }
2327
2327
  if (this[kImpl].flushing && this[kImpl].buf.length + data.length >= MAX_STRING) {
2328
2328
  try {
2329
- writeSync(this);
2329
+ writeSync2(this);
2330
2330
  this[kImpl].flushing = true;
2331
2331
  } catch (err) {
2332
2332
  destroy(this, err);
@@ -2336,7 +2336,7 @@ var require_thread_stream = __commonJS({
2336
2336
  this[kImpl].buf += data;
2337
2337
  if (this[kImpl].sync) {
2338
2338
  try {
2339
- writeSync(this);
2339
+ writeSync2(this);
2340
2340
  return true;
2341
2341
  } catch (err) {
2342
2342
  destroy(this, err);
@@ -2382,7 +2382,7 @@ var require_thread_stream = __commonJS({
2382
2382
  if (this[kImpl].destroyed) {
2383
2383
  return;
2384
2384
  }
2385
- writeSync(this);
2385
+ writeSync2(this);
2386
2386
  flushSync(this);
2387
2387
  }
2388
2388
  unref() {
@@ -2486,7 +2486,7 @@ var require_thread_stream = __commonJS({
2486
2486
  destroy(stream, err);
2487
2487
  }
2488
2488
  }
2489
- function writeSync(stream) {
2489
+ function writeSync2(stream) {
2490
2490
  const cb = () => {
2491
2491
  if (stream[kImpl].ending) {
2492
2492
  end(stream);
@@ -2987,11 +2987,11 @@ var require_tools = __commonJS({
2987
2987
  }
2988
2988
  }
2989
2989
  }
2990
- function buildFormatters(level, bindings, log77) {
2990
+ function buildFormatters(level, bindings, log78) {
2991
2991
  return {
2992
2992
  level,
2993
2993
  bindings,
2994
- log: log77
2994
+ log: log78
2995
2995
  };
2996
2996
  }
2997
2997
  function normalizeDestFileDescriptor(destination) {
@@ -3374,11 +3374,11 @@ var require_proto = __commonJS({
3374
3374
  }
3375
3375
  } else instance[serializersSym] = serializers;
3376
3376
  if (options.hasOwnProperty("formatters")) {
3377
- const { level, bindings: chindings, log: log77 } = options.formatters;
3377
+ const { level, bindings: chindings, log: log78 } = options.formatters;
3378
3378
  instance[formattersSym] = buildFormatters(
3379
3379
  level || formatters.level,
3380
3380
  chindings || resetChildingsFormatter,
3381
- log77 || formatters.log
3381
+ log78 || formatters.log
3382
3382
  );
3383
3383
  } else {
3384
3384
  instance[formattersSym] = buildFormatters(
@@ -4447,16 +4447,109 @@ var require_pino = __commonJS({
4447
4447
  });
4448
4448
 
4449
4449
  // ../../packages/logger/src/index.ts
4450
+ import {
4451
+ closeSync,
4452
+ existsSync,
4453
+ mkdirSync,
4454
+ openSync,
4455
+ renameSync,
4456
+ rmSync,
4457
+ statSync,
4458
+ writeSync
4459
+ } from "node:fs";
4460
+ import { dirname } from "node:path";
4461
+ function logFile() {
4462
+ if (file) return file;
4463
+ const path2 = process.env.BAND_LOG_FILE;
4464
+ if (!path2) return void 0;
4465
+ const max2 = Number(process.env.BAND_LOG_MAX_BYTES);
4466
+ const keep = Number(process.env.BAND_LOG_KEEP);
4467
+ const target = new RotatingFile(
4468
+ path2,
4469
+ max2 > 0 ? max2 : DEFAULT_LOG_MAX_BYTES,
4470
+ keep >= 1 ? keep : DEFAULT_LOG_KEEP
4471
+ );
4472
+ for (const stream of [process.stdout, process.stderr]) {
4473
+ stream.write = ((chunk, ...rest) => {
4474
+ target.write(chunk);
4475
+ const cb = rest.find((r) => typeof r === "function");
4476
+ cb?.();
4477
+ return true;
4478
+ });
4479
+ }
4480
+ file = target;
4481
+ return target;
4482
+ }
4450
4483
  function createLogger(name4) {
4451
- return (0, import_pino.default)({
4452
- name: name4,
4453
- level: process.env.LOG_LEVEL || "info"
4454
- });
4484
+ const options = { name: name4, level: process.env.LOG_LEVEL || "info" };
4485
+ const target = logFile();
4486
+ return target ? (0, import_pino.default)(options, { write: (line2) => target.write(line2) }) : (0, import_pino.default)(options);
4455
4487
  }
4456
- var import_pino;
4488
+ var import_pino, DEFAULT_LOG_MAX_BYTES, DEFAULT_LOG_KEEP, RotatingFile, file;
4457
4489
  var init_src = __esm({
4458
4490
  "../../packages/logger/src/index.ts"() {
4459
4491
  import_pino = __toESM(require_pino(), 1);
4492
+ DEFAULT_LOG_MAX_BYTES = 10 * 1024 * 1024;
4493
+ DEFAULT_LOG_KEEP = 3;
4494
+ RotatingFile = class {
4495
+ constructor(path2, maxBytes = DEFAULT_LOG_MAX_BYTES, keep = DEFAULT_LOG_KEEP) {
4496
+ this.path = path2;
4497
+ this.maxBytes = maxBytes;
4498
+ this.keep = keep;
4499
+ mkdirSync(dirname(path2), { recursive: true });
4500
+ if (existsSync(path2) && statSync(path2).size >= maxBytes) this.shift();
4501
+ this.open();
4502
+ }
4503
+ fd;
4504
+ size = 0;
4505
+ write(chunk) {
4506
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
4507
+ if (this.fd === void 0) return;
4508
+ if (this.size > 0 && this.size + buf.length > this.maxBytes) {
4509
+ this.close();
4510
+ this.shift();
4511
+ this.open();
4512
+ }
4513
+ if (this.fd === void 0) return;
4514
+ try {
4515
+ writeSync(this.fd, buf);
4516
+ this.size += buf.length;
4517
+ } catch {
4518
+ }
4519
+ }
4520
+ close() {
4521
+ if (this.fd === void 0) return;
4522
+ try {
4523
+ closeSync(this.fd);
4524
+ } catch {
4525
+ }
4526
+ this.fd = void 0;
4527
+ }
4528
+ open() {
4529
+ try {
4530
+ this.fd = openSync(this.path, "a", 384);
4531
+ this.size = statSync(this.path).size;
4532
+ } catch {
4533
+ this.fd = void 0;
4534
+ }
4535
+ }
4536
+ shift() {
4537
+ try {
4538
+ const oldest = this.keep - 1;
4539
+ if (oldest < 1) {
4540
+ rmSync(this.path, { force: true });
4541
+ return;
4542
+ }
4543
+ rmSync(`${this.path}.${oldest}`, { force: true });
4544
+ for (let i = oldest - 1; i >= 1; i--) {
4545
+ if (existsSync(`${this.path}.${i}`))
4546
+ renameSync(`${this.path}.${i}`, `${this.path}.${i + 1}`);
4547
+ }
4548
+ renameSync(this.path, `${this.path}.1`);
4549
+ } catch {
4550
+ }
4551
+ }
4552
+ };
4460
4553
  }
4461
4554
  });
4462
4555
 
@@ -4520,14 +4613,14 @@ import {
4520
4613
  chmodSync,
4521
4614
  linkSync,
4522
4615
  lstatSync,
4523
- mkdirSync,
4524
- renameSync,
4525
- statSync,
4616
+ mkdirSync as mkdirSync2,
4617
+ renameSync as renameSync2,
4618
+ statSync as statSync2,
4526
4619
  unlinkSync
4527
4620
  } from "node:fs";
4528
- import { dirname, join } from "node:path";
4621
+ import { dirname as dirname2, join } from "node:path";
4529
4622
  function ensurePrivateDir(dir) {
4530
- mkdirSync(dir, { recursive: true, mode: 448 });
4623
+ mkdirSync2(dir, { recursive: true, mode: 448 });
4531
4624
  const stats = lstatSync(dir);
4532
4625
  const uid = process.getuid?.();
4533
4626
  if (!stats.isDirectory()) throw new Error(`${dir} is not a directory`);
@@ -4536,7 +4629,7 @@ function ensurePrivateDir(dir) {
4536
4629
  }
4537
4630
  function checkEndpointOwner(socketPath) {
4538
4631
  const uid = process.getuid?.();
4539
- const dir = dirname(socketPath);
4632
+ const dir = dirname2(socketPath);
4540
4633
  let dirStats;
4541
4634
  try {
4542
4635
  dirStats = lstatSync(dir);
@@ -4584,7 +4677,7 @@ var init_endpoint = __esm({
4584
4677
  // ../../packages/host-local/src/terminals/daemon/protocol.ts
4585
4678
  import { createHash } from "node:crypto";
4586
4679
  import { readdirSync } from "node:fs";
4587
- import { basename, dirname as dirname2, join as join2 } from "node:path";
4680
+ import { basename, dirname as dirname3, join as join2 } from "node:path";
4588
4681
  function daemonPaths(runDir, version2 = PROTOCOL_VERSION) {
4589
4682
  const inRunDir = join2(runDir, `terminal-daemon-v${version2}.sock`);
4590
4683
  const socket = Buffer.byteLength(inRunDir) <= MAX_SOCKET_PATH_BYTES ? inRunDir : join2(
@@ -4602,7 +4695,7 @@ function daemonPaths(runDir, version2 = PROTOCOL_VERSION) {
4602
4695
  function retiredDaemonPaths(paths, tag) {
4603
4696
  return {
4604
4697
  ...paths,
4605
- socket: join2(dirname2(paths.socket), `.r${tag}`),
4698
+ socket: join2(dirname3(paths.socket), `.r${tag}`),
4606
4699
  token: paths.token.replace(/\.token$/, `.retired-${tag}.token`),
4607
4700
  pid: paths.pid.replace(/\.pid$/, `.retired-${tag}.pid`)
4608
4701
  };
@@ -4935,10 +5028,10 @@ var init_client = __esm({
4935
5028
 
4936
5029
  // ../../packages/host-local/src/terminals/daemon/exit-records.ts
4937
5030
  import {
4938
- mkdirSync as mkdirSync2,
5031
+ mkdirSync as mkdirSync3,
4939
5032
  readdirSync as readdirSync2,
4940
5033
  readFileSync,
4941
- renameSync as renameSync2,
5034
+ renameSync as renameSync3,
4942
5035
  unlinkSync as unlinkSync2,
4943
5036
  writeFileSync
4944
5037
  } from "node:fs";
@@ -4956,16 +5049,16 @@ function drainExitRecords(runDir) {
4956
5049
  }
4957
5050
  const events2 = [];
4958
5051
  for (const name4 of names) {
4959
- const file2 = join3(dir, name4);
5052
+ const file3 = join3(dir, name4);
4960
5053
  try {
4961
- const event = JSON.parse(readFileSync(file2, "utf8"));
5054
+ const event = JSON.parse(readFileSync(file3, "utf8"));
4962
5055
  if (typeof event.terminalId === "string" && typeof event.worktreeId === "string") {
4963
5056
  events2.push(event);
4964
5057
  }
4965
5058
  } catch {
4966
5059
  }
4967
5060
  try {
4968
- unlinkSync2(file2);
5061
+ unlinkSync2(file3);
4969
5062
  } catch {
4970
5063
  }
4971
5064
  }
@@ -4981,15 +5074,15 @@ var init_exit_records = __esm({
4981
5074
 
4982
5075
  // ../../packages/host-local/src/terminals/daemon/launch.ts
4983
5076
  import { fork } from "node:child_process";
4984
- import { closeSync, openSync, readFileSync as readFileSync2, renameSync as renameSync3, statSync as statSync2 } from "node:fs";
5077
+ import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync2, renameSync as renameSync4, statSync as statSync3 } from "node:fs";
4985
5078
  async function launchDaemon(options) {
4986
5079
  const { entry, paths, cwd, buildId, supersede, recordExits, execArgv } = options;
4987
5080
  ensurePrivateDir(paths.runDir);
4988
5081
  rotateLog(paths.log);
4989
- const logFd = openSync(paths.log, "a", 384);
5082
+ const logFd = openSync2(paths.log, "a", 384);
4990
5083
  let logSize = 0;
4991
5084
  try {
4992
- logSize = statSync2(paths.log).size;
5085
+ logSize = statSync3(paths.log).size;
4993
5086
  } catch {
4994
5087
  }
4995
5088
  const child = (() => {
@@ -5005,7 +5098,7 @@ async function launchDaemon(options) {
5005
5098
  env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" }
5006
5099
  });
5007
5100
  } finally {
5008
- closeSync(logFd);
5101
+ closeSync2(logFd);
5009
5102
  }
5010
5103
  })();
5011
5104
  child.on("error", () => {
@@ -5061,7 +5154,7 @@ async function retireOlderDaemons(runDir, buildId) {
5061
5154
  }
5062
5155
  function rotateLog(path2) {
5063
5156
  try {
5064
- if (statSync2(path2).size > LOG_ROTATE_BYTES) renameSync3(path2, `${path2}.1`);
5157
+ if (statSync3(path2).size > LOG_ROTATE_BYTES) renameSync4(path2, `${path2}.1`);
5065
5158
  } catch {
5066
5159
  }
5067
5160
  }
@@ -5087,7 +5180,7 @@ var init_launch = __esm({
5087
5180
  });
5088
5181
 
5089
5182
  // ../../packages/host-local/src/terminals/daemon/daemon-backend.ts
5090
- import { existsSync, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "node:fs";
5183
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "node:fs";
5091
5184
  function sameEntry(a, b2) {
5092
5185
  return a === b2 || a !== null && b2 !== null && a.dev === b2.dev && a.ino === b2.ino;
5093
5186
  }
@@ -5540,7 +5633,7 @@ var init_daemon_backend = __esm({
5540
5633
  if (client2.buildId !== this.options.buildId) return false;
5541
5634
  const record3 = readPidRecord(this.paths.pid);
5542
5635
  if (record3?.pid !== client2.pid || record3.entry === void 0) return true;
5543
- return existsSync(record3.entry);
5636
+ return existsSync2(record3.entry);
5544
5637
  }
5545
5638
  /** Stop starting sessions on `client`; keep using it for the ones it has. */
5546
5639
  demote(client2) {
@@ -5648,13 +5741,13 @@ var init_daemon_backend = __esm({
5648
5741
 
5649
5742
  // ../../packages/host-local/src/process/path.ts
5650
5743
  import { execFile } from "node:child_process";
5651
- import { existsSync as existsSync2 } from "node:fs";
5744
+ import { existsSync as existsSync3 } from "node:fs";
5652
5745
  import { delimiter } from "node:path";
5653
5746
  function defaultShell() {
5654
5747
  if (process.env.SHELL) return process.env.SHELL;
5655
5748
  if (process.platform === "win32") return process.env.ComSpec || "cmd.exe";
5656
5749
  for (const candidate of ["/bin/zsh", "/bin/bash", "/bin/sh"]) {
5657
- if (existsSync2(candidate)) return candidate;
5750
+ if (existsSync3(candidate)) return candidate;
5658
5751
  }
5659
5752
  return "/bin/sh";
5660
5753
  }
@@ -5665,7 +5758,7 @@ function extraBinDirs() {
5665
5758
  } else if (process.platform === "linux") {
5666
5759
  const dirs = [];
5667
5760
  const linuxbrew = "/home/linuxbrew/.linuxbrew/bin";
5668
- if (existsSync2(linuxbrew)) dirs.push(linuxbrew);
5761
+ if (existsSync3(linuxbrew)) dirs.push(linuxbrew);
5669
5762
  dirs.push("/usr/local/bin");
5670
5763
  cachedBinDirs = dirs;
5671
5764
  } else {
@@ -5738,7 +5831,7 @@ var init_path = __esm({
5738
5831
  });
5739
5832
 
5740
5833
  // ../../packages/host-local/src/terminals/claude-resume.ts
5741
- import { readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5834
+ import { readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
5742
5835
  import { homedir } from "node:os";
5743
5836
  import { join as join4 } from "node:path";
5744
5837
  function isResumableClaudeCommand(command2) {
@@ -5761,7 +5854,7 @@ function findLatestClaudeSessionId(cwd) {
5761
5854
  for (const name4 of names) {
5762
5855
  let mtimeMs;
5763
5856
  try {
5764
- mtimeMs = statSync3(join4(dir, name4)).mtimeMs;
5857
+ mtimeMs = statSync4(join4(dir, name4)).mtimeMs;
5765
5858
  } catch {
5766
5859
  continue;
5767
5860
  }
@@ -5818,7 +5911,7 @@ var init_pty_error_hints = __esm({
5818
5911
  });
5819
5912
 
5820
5913
  // ../../packages/host-local/src/terminals/terminal-history.ts
5821
- import { readdirSync as readdirSync4, readFileSync as readFileSync4, rmSync } from "node:fs";
5914
+ import { readdirSync as readdirSync4, readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
5822
5915
  import { mkdir, rename, writeFile } from "node:fs/promises";
5823
5916
  import { join as join5 } from "node:path";
5824
5917
  function readJson(path2) {
@@ -5884,7 +5977,7 @@ var init_terminal_history = __esm({
5884
5977
  /** Drop a terminal's saved history: its tab or worktree was actually closed or deleted. */
5885
5978
  removeSession(terminalId) {
5886
5979
  try {
5887
- rmSync(this.sessionDir(terminalId), { recursive: true, force: true });
5980
+ rmSync2(this.sessionDir(terminalId), { recursive: true, force: true });
5888
5981
  } catch (err) {
5889
5982
  log2.warn({ err, terminalId }, "failed to remove terminal history directory");
5890
5983
  }
@@ -5908,7 +6001,7 @@ var init_terminal_history = __esm({
5908
6001
  const meta = readJson(join5(dir, "meta.json"));
5909
6002
  if (meta?.worktreeId !== worktreeId4) continue;
5910
6003
  try {
5911
- rmSync(dir, { recursive: true, force: true });
6004
+ rmSync2(dir, { recursive: true, force: true });
5912
6005
  } catch (err) {
5913
6006
  log2.warn({ err, dir }, "failed to remove terminal history directory");
5914
6007
  }
@@ -10405,7 +10498,7 @@ var require_addon_unicode11 = __commonJS({
10405
10498
 
10406
10499
  // ../../packages/host-local/src/terminals/terminal-pool.ts
10407
10500
  import { randomUUID as randomUUID2 } from "node:crypto";
10408
- import { existsSync as existsSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
10501
+ import { existsSync as existsSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
10409
10502
  import { tmpdir } from "node:os";
10410
10503
  import { join as join6, sep } from "node:path";
10411
10504
  function resolveSafeCwd(candidate, worktreeRoot, label2, original) {
@@ -10413,7 +10506,7 @@ function resolveSafeCwd(candidate, worktreeRoot, label2, original) {
10413
10506
  log3.warn("Ignoring %s %s \u2014 resolves outside worktree root %s", label2, original, worktreeRoot);
10414
10507
  return null;
10415
10508
  }
10416
- if (!existsSync3(candidate)) {
10509
+ if (!existsSync4(candidate)) {
10417
10510
  log3.warn("Ignoring %s %s \u2014 directory does not exist", label2, original);
10418
10511
  return null;
10419
10512
  }
@@ -10574,10 +10667,10 @@ var init_terminal_pool = __esm({
10574
10667
  checkpoint.cwd
10575
10668
  ) ?? cwd;
10576
10669
  }
10577
- if (!existsSync3(cwd)) {
10670
+ if (!existsSync4(cwd)) {
10578
10671
  throw new Error(`Worktree directory does not exist: ${cwd}`);
10579
10672
  }
10580
- if (!existsSync3(shell)) {
10673
+ if (!existsSync4(shell)) {
10581
10674
  throw new Error(`Shell not found: ${shell}`);
10582
10675
  }
10583
10676
  log3.debug(
@@ -15807,6 +15900,15 @@ var init_wrapper = __esm({
15807
15900
  }
15808
15901
  });
15809
15902
 
15903
+ // ../../packages/shared/src/chat-events.ts
15904
+ var HISTORY_PAGE_SIZE, HISTORY_PAGE_MAX_ROWS;
15905
+ var init_chat_events = __esm({
15906
+ "../../packages/shared/src/chat-events.ts"() {
15907
+ HISTORY_PAGE_SIZE = 20;
15908
+ HISTORY_PAGE_MAX_ROWS = 300;
15909
+ }
15910
+ });
15911
+
15810
15912
  // src/server/services/_utils/queued-message-store.ts
15811
15913
  import { randomUUID as randomUUID3 } from "node:crypto";
15812
15914
  function toWireQueuedMessages(messages) {
@@ -25752,28 +25854,28 @@ var init_to_json_schema = __esm({
25752
25854
  }
25753
25855
  case "file": {
25754
25856
  const json3 = _json;
25755
- const file2 = {
25857
+ const file3 = {
25756
25858
  type: "string",
25757
25859
  format: "binary",
25758
25860
  contentEncoding: "binary"
25759
25861
  };
25760
25862
  const { minimum, maximum, mime } = schema._zod.bag;
25761
25863
  if (minimum !== void 0)
25762
- file2.minLength = minimum;
25864
+ file3.minLength = minimum;
25763
25865
  if (maximum !== void 0)
25764
- file2.maxLength = maximum;
25866
+ file3.maxLength = maximum;
25765
25867
  if (mime) {
25766
25868
  if (mime.length === 1) {
25767
- file2.contentMediaType = mime[0];
25768
- Object.assign(json3, file2);
25869
+ file3.contentMediaType = mime[0];
25870
+ Object.assign(json3, file3);
25769
25871
  } else {
25770
25872
  json3.anyOf = mime.map((m2) => {
25771
- const mFile = { ...file2, contentMediaType: m2 };
25873
+ const mFile = { ...file3, contentMediaType: m2 };
25772
25874
  return mFile;
25773
25875
  });
25774
25876
  }
25775
25877
  } else {
25776
- Object.assign(json3, file2);
25878
+ Object.assign(json3, file3);
25777
25879
  }
25778
25880
  break;
25779
25881
  }
@@ -26681,7 +26783,7 @@ function literal(value, params) {
26681
26783
  ...util_exports.normalizeParams(params)
26682
26784
  });
26683
26785
  }
26684
- function file(params) {
26786
+ function file2(params) {
26685
26787
  return _file(ZodFile, params);
26686
26788
  }
26687
26789
  function transform(fn) {
@@ -27539,7 +27641,7 @@ __export(external_exports, {
27539
27641
  emoji: () => emoji2,
27540
27642
  endsWith: () => _endsWith,
27541
27643
  enum: () => _enum2,
27542
- file: () => file,
27644
+ file: () => file2,
27543
27645
  flattenError: () => flattenError,
27544
27646
  float32: () => float32,
27545
27647
  float64: () => float64,
@@ -40205,7 +40307,7 @@ var init_node_sqlite = __esm({
40205
40307
 
40206
40308
  // ../../node_modules/.pnpm/drizzle-orm@1.0.0-rc.1_@opentelemetry+api@1.9.0_@types+better-sqlite3@7.6.13_better-sql_95478898f63cc8c3fbbd0c5f8c1e464f/node_modules/drizzle-orm/migrator.js
40207
40309
  import crypto2 from "node:crypto";
40208
- import fs2, { existsSync as existsSync6, readdirSync as readdirSync6 } from "node:fs";
40310
+ import fs2, { existsSync as existsSync7, readdirSync as readdirSync6 } from "node:fs";
40209
40311
  import { join as join10 } from "node:path";
40210
40312
  function readMigrationFiles(config2) {
40211
40313
  if (fs2.existsSync(`${config2.migrationsFolder}/meta/_journal.json`)) throw Error('We detected that you have old drizzle-kit migration folders. You must upgrade drizzle-kit and run "drizzle-kit up"');
@@ -40214,7 +40316,7 @@ function readMigrationFiles(config2) {
40214
40316
  const migrations = readdirSync6(migrationFolderTo).map((subdir) => ({
40215
40317
  path: join10(migrationFolderTo, subdir, "migration.sql"),
40216
40318
  name: subdir
40217
- })).filter((it) => existsSync6(it.path));
40319
+ })).filter((it) => existsSync7(it.path));
40218
40320
  migrations.sort((a, b2) => a.name.localeCompare(b2.name));
40219
40321
  for (const migration of migrations) {
40220
40322
  const migrationPath = migration.path;
@@ -40253,9 +40355,9 @@ var init_migrator2 = __esm({
40253
40355
 
40254
40356
  // src/server/infra/db/queries/settings.ts
40255
40357
  import { randomBytes } from "node:crypto";
40256
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync as renameSync4, statSync as statSync7, writeFileSync as writeFileSync3 } from "node:fs";
40358
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync5, statSync as statSync8, writeFileSync as writeFileSync3 } from "node:fs";
40257
40359
  import { homedir as homedir2 } from "node:os";
40258
- import { dirname as dirname3, join as join11 } from "node:path";
40360
+ import { dirname as dirname4, join as join11 } from "node:path";
40259
40361
  function bandHome() {
40260
40362
  if (process.env.BAND_HOME) return process.env.BAND_HOME;
40261
40363
  return join11(homedir2(), ".band");
@@ -40294,7 +40396,7 @@ var init_settings = __esm({
40294
40396
  const filePath = settingsFile();
40295
40397
  let mtimeMs;
40296
40398
  try {
40297
- mtimeMs = statSync7(filePath).mtimeMs;
40399
+ mtimeMs = statSync8(filePath).mtimeMs;
40298
40400
  } catch {
40299
40401
  settingsCache.delete(filePath);
40300
40402
  return {};
@@ -40337,11 +40439,11 @@ var init_settings = __esm({
40337
40439
  const merged = { ...existing, ...patch };
40338
40440
  const data = `${JSON.stringify(merged, null, 2)}
40339
40441
  `;
40340
- const dir = dirname3(filePath);
40341
- mkdirSync3(dir, { recursive: true });
40442
+ const dir = dirname4(filePath);
40443
+ mkdirSync4(dir, { recursive: true });
40342
40444
  const tmpPath = `${filePath}.tmp.${process.pid}`;
40343
40445
  writeFileSync3(tmpPath, data, "utf-8");
40344
- renameSync4(tmpPath, filePath);
40446
+ renameSync5(tmpPath, filePath);
40345
40447
  settingsCache.delete(filePath);
40346
40448
  }
40347
40449
  /**
@@ -41075,13 +41177,13 @@ var init_schema2 = __esm({
41075
41177
  });
41076
41178
 
41077
41179
  // src/server/infra/db/connection.ts
41078
- import { mkdirSync as mkdirSync4 } from "node:fs";
41180
+ import { mkdirSync as mkdirSync5 } from "node:fs";
41079
41181
  import { join as join12 } from "node:path";
41080
41182
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
41081
41183
  function getDb() {
41082
41184
  if (_db) return _db;
41083
41185
  const home = bandHome();
41084
- mkdirSync4(home, { recursive: true });
41186
+ mkdirSync5(home, { recursive: true });
41085
41187
  const dbPath = join12(home, "band.db");
41086
41188
  _sqlite = new DatabaseSync2(dbPath);
41087
41189
  _sqlite.exec("PRAGMA journal_mode = WAL");
@@ -41147,9 +41249,10 @@ function indexFields(event) {
41147
41249
  };
41148
41250
  }
41149
41251
  var ChatEventQueries;
41150
- var init_chat_events = __esm({
41252
+ var init_chat_events2 = __esm({
41151
41253
  "src/server/infra/db/queries/chat-events.ts"() {
41152
41254
  "use strict";
41255
+ init_chat_events();
41153
41256
  init_drizzle_orm();
41154
41257
  init_connection();
41155
41258
  init_schema2();
@@ -41185,8 +41288,9 @@ var init_chat_events = __esm({
41185
41288
  }
41186
41289
  /**
41187
41290
  * The last `turns` turns before `beforeId` (the whole revision when
41188
- * `beforeId` is undefined), oldest first. `hasOlder` says whether rows
41189
- * exist before the returned window.
41291
+ * `beforeId` is undefined), oldest first, shortened to the newest whole
41292
+ * turns that fit `HISTORY_PAGE_MAX_ROWS` (the newest turn is always kept).
41293
+ * `hasOlder` says whether rows exist before the returned window.
41190
41294
  */
41191
41295
  readTurns(sessionId, revision, turns, beforeId) {
41192
41296
  const db = getDb();
@@ -41196,7 +41300,18 @@ var init_chat_events = __esm({
41196
41300
  beforeId === void 0 ? void 0 : lt(chatEvents.id, beforeId)
41197
41301
  );
41198
41302
  const starts = db.select({ id: chatEvents.id }).from(chatEvents).where(and(scope3, eq(chatEvents.turnStart, true))).orderBy(desc(chatEvents.id)).limit(turns).all();
41199
- const from = starts.length < turns ? void 0 : starts[starts.length - 1].id;
41303
+ let kept = starts.length;
41304
+ if (starts.length > 1) {
41305
+ const ids = db.select({ id: chatEvents.id }).from(chatEvents).where(and(scope3, sql`${chatEvents.id} >= ${starts[starts.length - 1].id}`)).orderBy(desc(chatEvents.id)).limit(HISTORY_PAGE_MAX_ROWS + 1).all().map((r) => r.id);
41306
+ for (let i = 1; i < starts.length; i++) {
41307
+ const count2 = ids.findIndex((id) => id < starts[i].id);
41308
+ if ((count2 === -1 ? ids.length : count2) > HISTORY_PAGE_MAX_ROWS) {
41309
+ kept = i;
41310
+ break;
41311
+ }
41312
+ }
41313
+ }
41314
+ const from = kept === starts.length && starts.length < turns ? void 0 : starts[kept - 1]?.id;
41200
41315
  const rows = db.select().from(chatEvents).where(and(scope3, from === void 0 ? void 0 : sql`${chatEvents.id} >= ${from}`)).orderBy(chatEvents.id).all().map(toRow);
41201
41316
  if (from === void 0) return { rows, hasOlder: false };
41202
41317
  const older = db.select({ id: chatEvents.id }).from(chatEvents).where(
@@ -41411,16 +41526,63 @@ var init_cli_invocation = __esm({
41411
41526
  });
41412
41527
 
41413
41528
  // ../../packages/coding-agent/src/hook-status.ts
41529
+ function hasBackgroundWork(payload) {
41530
+ const crons = payload.session_crons;
41531
+ if (Array.isArray(crons) && crons.length > 0) return true;
41532
+ const tasks3 = payload.background_tasks;
41533
+ if (!Array.isArray(tasks3)) return false;
41534
+ return tasks3.some((task) => {
41535
+ if (typeof task !== "object" || task === null) return true;
41536
+ const { type, status } = task;
41537
+ if (typeof type === "string" && type.trim().toLowerCase() === "teammate") return false;
41538
+ const state2 = typeof status === "string" ? status.trim().toLowerCase() : "";
41539
+ return !TERMINAL_TASK_STATUSES.has(state2);
41540
+ });
41541
+ }
41414
41542
  function mapClaudeCodeHookStatus(payload) {
41415
41543
  const hookEvent = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
41416
41544
  const toolName2 = typeof payload.tool_name === "string" ? payload.tool_name : "";
41417
- if (hookEvent === "Stop") {
41418
- return "needs_attention";
41545
+ switch (hookEvent) {
41546
+ case "PermissionRequest":
41547
+ return "question";
41548
+ case "PreToolUse":
41549
+ return CLAUDE_CODE_INTERACTIVE_TOOLS.has(toolName2) ? "question" : "working";
41550
+ case "Stop":
41551
+ return hasBackgroundWork(payload) ? "monitoring" : "done";
41552
+ case "StopFailure":
41553
+ return "failed";
41554
+ case "PostCompact":
41555
+ return payload.trigger === "manual" ? "done" : "working";
41556
+ default:
41557
+ return "working";
41419
41558
  }
41420
- if ((hookEvent === "PreToolUse" || hookEvent === "PermissionRequest") && CLAUDE_CODE_INTERACTIVE_TOOLS.has(toolName2)) {
41421
- return "needs_attention";
41559
+ }
41560
+ function foldSubagents(hold, payload, mapped) {
41561
+ const hookEvent = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
41562
+ const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
41563
+ if (hookEvent === "SubagentStart") {
41564
+ if (agentId) hold.running.add(agentId);
41565
+ return hold.deferred ? "working" : mapped;
41566
+ }
41567
+ if (hookEvent === "SubagentStop") {
41568
+ hold.running.delete(agentId);
41569
+ if (hold.running.size === 0 && hold.deferred) {
41570
+ const settled = hold.deferred;
41571
+ hold.deferred = null;
41572
+ return settled;
41573
+ }
41574
+ return mapped;
41575
+ }
41576
+ if ((hookEvent === "Stop" || hookEvent === "StopFailure") && !agentId) {
41577
+ if (hold.running.size > 0 && (mapped === "done" || mapped === "monitoring")) {
41578
+ hold.deferred = mapped;
41579
+ return "working";
41580
+ }
41581
+ hold.deferred = null;
41582
+ return mapped;
41422
41583
  }
41423
- return "working";
41584
+ if (hookEvent === "UserPromptSubmit") hold.deferred = null;
41585
+ return mapped;
41424
41586
  }
41425
41587
  async function mapHookPayloadToStatus(agentType, payload) {
41426
41588
  switch (agentType) {
@@ -41441,10 +41603,31 @@ function isHookSessionEnd(agentType, payload) {
41441
41603
  function hookSessionId(payload) {
41442
41604
  return typeof payload.session_id === "string" && payload.session_id ? payload.session_id : null;
41443
41605
  }
41444
- var CLAUDE_CODE_INTERACTIVE_TOOLS;
41606
+ var CLAUDE_CODE_INTERACTIVE_TOOLS, TERMINAL_TASK_STATUSES;
41445
41607
  var init_hook_status = __esm({
41446
41608
  "../../packages/coding-agent/src/hook-status.ts"() {
41447
41609
  CLAUDE_CODE_INTERACTIVE_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
41610
+ TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
41611
+ "idle",
41612
+ "done",
41613
+ "success",
41614
+ "succeeded",
41615
+ "complete",
41616
+ "completed",
41617
+ "finished",
41618
+ "failed",
41619
+ "error",
41620
+ "terminated",
41621
+ "exited",
41622
+ "aborted",
41623
+ "expired",
41624
+ "skipped",
41625
+ "crashed",
41626
+ "killed",
41627
+ "cancelled",
41628
+ "canceled",
41629
+ "timed_out"
41630
+ ]);
41448
41631
  }
41449
41632
  });
41450
41633
 
@@ -41510,8 +41693,8 @@ var init_pricing = __esm({
41510
41693
  // ../../packages/coding-agent/src/usage/read-lines.ts
41511
41694
  import { createReadStream as createReadStream2 } from "node:fs";
41512
41695
  import { createInterface } from "node:readline";
41513
- async function* readLines(file2) {
41514
- const input = createReadStream2(file2);
41696
+ async function* readLines(file3) {
41697
+ const input = createReadStream2(file3);
41515
41698
  const rl = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
41516
41699
  try {
41517
41700
  for await (const line2 of rl) yield line2;
@@ -41568,10 +41751,10 @@ async function repoDirsFor(canonical) {
41568
41751
  }
41569
41752
  return dirs;
41570
41753
  }
41571
- async function readSessionCwd(file2) {
41754
+ async function readSessionCwd(file3) {
41572
41755
  let handle;
41573
41756
  try {
41574
- handle = await open(file2, "r");
41757
+ handle = await open(file3, "r");
41575
41758
  const buf = Buffer.alloc(SESSION_HEAD_BYTES);
41576
41759
  const { bytesRead } = await handle.read(buf, 0, SESSION_HEAD_BYTES, 0);
41577
41760
  if (bytesRead === 0) return null;
@@ -41617,13 +41800,13 @@ async function listSessions(dir) {
41617
41800
  if (!name4.endsWith(".jsonl")) return;
41618
41801
  const sessionId = name4.slice(0, -".jsonl".length);
41619
41802
  if (!SESSION_ID_RE.test(sessionId)) return;
41620
- const file2 = join14(repoDir, name4);
41621
- const cwd = await readSessionCwd(file2);
41803
+ const file3 = join14(repoDir, name4);
41804
+ const cwd = await readSessionCwd(file3);
41622
41805
  if (cwd === null) return;
41623
41806
  if (cwd !== void 0 && cwd !== dir && cwd !== canonical) return;
41624
41807
  let lastModified;
41625
41808
  try {
41626
- lastModified = (await stat(file2)).mtimeMs;
41809
+ lastModified = (await stat(file3)).mtimeMs;
41627
41810
  } catch {
41628
41811
  return;
41629
41812
  }
@@ -41637,9 +41820,9 @@ async function listSessions(dir) {
41637
41820
  }
41638
41821
  async function findSessionFile(sessionId, dir) {
41639
41822
  for (const repoDir of await repoDirsFor(await canonicalDir(dir))) {
41640
- const file2 = join14(repoDir, `${sessionId}.jsonl`);
41823
+ const file3 = join14(repoDir, `${sessionId}.jsonl`);
41641
41824
  try {
41642
- if ((await stat(file2)).size > 0) return file2;
41825
+ if ((await stat(file3)).size > 0) return file3;
41643
41826
  } catch {
41644
41827
  }
41645
41828
  }
@@ -41647,15 +41830,15 @@ async function findSessionFile(sessionId, dir) {
41647
41830
  }
41648
41831
  async function getSessionUsage(sessionId, dir) {
41649
41832
  if (!SESSION_ID_RE.test(sessionId)) return null;
41650
- const file2 = await findSessionFile(sessionId, dir);
41651
- if (!file2) return null;
41833
+ const file3 = await findSessionFile(sessionId, dir);
41834
+ if (!file3) return null;
41652
41835
  const turns = [];
41653
41836
  const turnByMessageId = /* @__PURE__ */ new Map();
41654
41837
  let conversationRecords = 0;
41655
41838
  let startedAt = Number.POSITIVE_INFINITY;
41656
41839
  let updatedAt = 0;
41657
41840
  let modelFallback = "";
41658
- for await (const line2 of readLines(file2)) {
41841
+ for await (const line2 of readLines(file3)) {
41659
41842
  if (!line2.trim()) continue;
41660
41843
  let record3;
41661
41844
  try {
@@ -41768,9 +41951,9 @@ async function findSessionFiles() {
41768
41951
  }
41769
41952
  return results;
41770
41953
  }
41771
- async function readSessionMeta(file2) {
41954
+ async function readSessionMeta(file3) {
41772
41955
  let first;
41773
- for await (const line2 of readLines(file2)) {
41956
+ for await (const line2 of readLines(file3)) {
41774
41957
  if (!line2.trim()) continue;
41775
41958
  first = line2;
41776
41959
  break;
@@ -41782,27 +41965,27 @@ async function readSessionMeta(file2) {
41782
41965
  }
41783
41966
  return void 0;
41784
41967
  }
41785
- async function getSessionMeta(file2) {
41786
- const cached2 = metaCache.get(file2.path);
41787
- if (cached2 && cached2.mtimeMs === file2.mtimeMs) return cached2.meta;
41968
+ async function getSessionMeta(file3) {
41969
+ const cached2 = metaCache.get(file3.path);
41970
+ if (cached2 && cached2.mtimeMs === file3.mtimeMs) return cached2.meta;
41788
41971
  try {
41789
- const meta = await readSessionMeta(file2.path);
41790
- metaCache.set(file2.path, { mtimeMs: file2.mtimeMs, meta });
41972
+ const meta = await readSessionMeta(file3.path);
41973
+ metaCache.set(file3.path, { mtimeMs: file3.mtimeMs, meta });
41791
41974
  return meta;
41792
41975
  } catch (err) {
41793
- metaCache.set(file2.path, { mtimeMs: file2.mtimeMs, meta: void 0 });
41976
+ metaCache.set(file3.path, { mtimeMs: file3.mtimeMs, meta: void 0 });
41794
41977
  throw err;
41795
41978
  }
41796
41979
  }
41797
41980
  async function listSessions2(dir) {
41798
41981
  const sessions = [];
41799
- for (const file2 of await findSessionFiles()) {
41982
+ for (const file3 of await findSessionFiles()) {
41800
41983
  try {
41801
- const meta = await getSessionMeta(file2);
41984
+ const meta = await getSessionMeta(file3);
41802
41985
  if (!meta || meta.cwd !== dir) continue;
41803
- sessions.push({ sessionId: meta.id, lastModified: file2.mtimeMs });
41986
+ sessions.push({ sessionId: meta.id, lastModified: file3.mtimeMs });
41804
41987
  } catch (err) {
41805
- log7.debug({ err, file: file2.path }, "failed to parse codex session file");
41988
+ log7.debug({ err, file: file3.path }, "failed to parse codex session file");
41806
41989
  }
41807
41990
  }
41808
41991
  return sessions.sort((a, b2) => b2.lastModified - a.lastModified);
@@ -42064,10 +42247,10 @@ var init_src3 = __esm({
42064
42247
  });
42065
42248
 
42066
42249
  // ../../packages/host-local/src/agents/acp-launch.ts
42067
- import { accessSync, constants, existsSync as existsSync7, statSync as statSync8 } from "node:fs";
42250
+ import { accessSync, constants, existsSync as existsSync8, statSync as statSync9 } from "node:fs";
42068
42251
  import { createRequire } from "node:module";
42069
42252
  import { homedir as homedir6 } from "node:os";
42070
- import { delimiter as delimiter2, dirname as dirname4, join as join16, resolve as resolvePath } from "node:path";
42253
+ import { delimiter as delimiter2, dirname as dirname5, join as join16, resolve as resolvePath } from "node:path";
42071
42254
  import { fileURLToPath } from "node:url";
42072
42255
  function findOnPath(name4, path2, extra = []) {
42073
42256
  for (const dir of [...path2.split(delimiter2), ...extra]) {
@@ -42075,24 +42258,24 @@ function findOnPath(name4, path2, extra = []) {
42075
42258
  const candidate = join16(dir, name4);
42076
42259
  try {
42077
42260
  accessSync(candidate, constants.X_OK);
42078
- if (statSync8(candidate).isFile()) return candidate;
42261
+ if (statSync9(candidate).isFile()) return candidate;
42079
42262
  } catch {
42080
42263
  }
42081
42264
  }
42082
42265
  return null;
42083
42266
  }
42084
42267
  function adapterScript(pkg, bundledName) {
42085
- const bundled = join16(dirname4(fileURLToPath(import.meta.url)), "agents", `${bundledName}.mjs`);
42086
- if (existsSync7(bundled)) return bundled;
42087
- return join16(dirname4(require2.resolve(`${pkg}/package.json`)), "dist", "index.js");
42268
+ const bundled = join16(dirname5(fileURLToPath(import.meta.url)), "agents", `${bundledName}.mjs`);
42269
+ if (existsSync8(bundled)) return bundled;
42270
+ return join16(dirname5(require2.resolve(`${pkg}/package.json`)), "dist", "index.js");
42088
42271
  }
42089
42272
  function nodeFor(path2) {
42090
42273
  return findOnPath("node", path2) ?? process.execPath;
42091
42274
  }
42092
- function isExecutableFile(file2) {
42275
+ function isExecutableFile(file3) {
42093
42276
  try {
42094
- accessSync(file2, constants.X_OK);
42095
- return statSync8(file2).isFile();
42277
+ accessSync(file3, constants.X_OK);
42278
+ return statSync9(file3).isFile();
42096
42279
  } catch {
42097
42280
  return false;
42098
42281
  }
@@ -42165,7 +42348,7 @@ var init_acp_launch = __esm({
42165
42348
 
42166
42349
  // ../../packages/host-local/src/agents/claude-defaults.ts
42167
42350
  import { execFile as execFile3 } from "node:child_process";
42168
- import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync6, readSync, realpathSync as realpathSync2, statSync as statSync9 } from "node:fs";
42351
+ import { closeSync as closeSync3, openSync as openSync3, readFileSync as readFileSync6, readSync, realpathSync as realpathSync2, statSync as statSync10 } from "node:fs";
42169
42352
  import { homedir as homedir7 } from "node:os";
42170
42353
  import { join as join17, resolve as resolvePath2 } from "node:path";
42171
42354
  function claudeConfigDir2(env) {
@@ -42258,16 +42441,16 @@ function transcriptPath(env, cwd, sessionId) {
42258
42441
  function readTail(path2, bytes) {
42259
42442
  let fd;
42260
42443
  try {
42261
- const size = statSync9(path2).size;
42444
+ const size = statSync10(path2).size;
42262
42445
  const length = Math.min(size, bytes);
42263
42446
  const buf = Buffer.alloc(length);
42264
- fd = openSync2(path2, "r");
42447
+ fd = openSync3(path2, "r");
42265
42448
  readSync(fd, buf, 0, length, size - length);
42266
42449
  return buf.toString("utf8");
42267
42450
  } catch {
42268
42451
  return null;
42269
42452
  } finally {
42270
- if (fd !== void 0) closeSync2(fd);
42453
+ if (fd !== void 0) closeSync3(fd);
42271
42454
  }
42272
42455
  }
42273
42456
  function reportedClaudeDefaults(opts) {
@@ -42368,113 +42551,225 @@ var init_claude_defaults = __esm({
42368
42551
  });
42369
42552
 
42370
42553
  // ../../packages/host-local/src/agents/hooks-install.ts
42371
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
42554
+ import {
42555
+ mkdirSync as mkdirSync6,
42556
+ readFileSync as readFileSync7,
42557
+ realpathSync as realpathSync3,
42558
+ renameSync as renameSync6,
42559
+ statSync as statSync11,
42560
+ unlinkSync as unlinkSync4,
42561
+ writeFileSync as writeFileSync4
42562
+ } from "node:fs";
42372
42563
  import { homedir as homedir8 } from "node:os";
42373
- import { dirname as dirname5, join as join18 } from "node:path";
42564
+ import { dirname as dirname6, join as join18 } from "node:path";
42374
42565
  function claudeSettingsPath() {
42375
42566
  return join18(homedir8(), ".claude", "settings.json");
42376
42567
  }
42377
42568
  function loadClaudeSettings() {
42569
+ let text3;
42378
42570
  try {
42379
- const data = readFileSync7(claudeSettingsPath(), "utf-8");
42380
- return JSON.parse(data);
42571
+ text3 = readFileSync7(claudeSettingsPath(), "utf-8");
42572
+ } catch (err) {
42573
+ if (err.code === "ENOENT") {
42574
+ return { settings: {}, indent: 2, trailingNewline: false };
42575
+ }
42576
+ throw err;
42577
+ }
42578
+ if (text3.trim() === "") return { settings: {}, indent: 2, trailingNewline: false };
42579
+ let parsed;
42580
+ try {
42581
+ parsed = JSON.parse(text3);
42381
42582
  } catch {
42382
- return {};
42583
+ throw new Error(`${claudeSettingsPath()} is not valid JSON; fix it before installing hooks.`);
42584
+ }
42585
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
42586
+ throw new Error(
42587
+ `${claudeSettingsPath()} is not a JSON object; fix it before installing hooks.`
42588
+ );
42589
+ }
42590
+ const indentMatch = /^([ \t]+)\S/m.exec(text3);
42591
+ return {
42592
+ settings: parsed,
42593
+ indent: indentMatch ? indentMatch[1] : 2,
42594
+ trailingNewline: text3.endsWith("\n")
42595
+ };
42596
+ }
42597
+ function saveClaudeSettings({ settings: settings3, indent, trailingNewline }) {
42598
+ let path2 = claudeSettingsPath();
42599
+ mkdirSync6(dirname6(path2), { recursive: true });
42600
+ let mode = 384;
42601
+ try {
42602
+ path2 = realpathSync3(path2);
42603
+ mode = statSync11(path2).mode & 511;
42604
+ } catch {
42605
+ }
42606
+ const temp = `${path2}.band-${process.pid}-${Date.now()}.tmp`;
42607
+ try {
42608
+ writeFileSync4(temp, JSON.stringify(settings3, null, indent) + (trailingNewline ? "\n" : ""), {
42609
+ encoding: "utf-8",
42610
+ mode,
42611
+ flag: "wx"
42612
+ });
42613
+ renameSync6(temp, path2);
42614
+ } catch (err) {
42615
+ try {
42616
+ unlinkSync4(temp);
42617
+ } catch {
42618
+ }
42619
+ throw err;
42383
42620
  }
42384
42621
  }
42385
42622
  function isBandHook(command2) {
42386
- return command2.includes("band") && command2.includes("notify");
42623
+ return BAND_NOTIFY.test(command2);
42387
42624
  }
42388
- async function checkHooks() {
42389
- const settings3 = loadClaudeSettings();
42625
+ function isBandEntry(entry) {
42626
+ const hooks = entry?.hooks;
42627
+ return Array.isArray(hooks) && hooks.some(
42628
+ (h) => h?.type === "command" && typeof h.command === "string" && isBandHook(h.command)
42629
+ );
42630
+ }
42631
+ function isCurrentCommand(hook) {
42632
+ const command2 = hook.command ?? "";
42633
+ return command2.includes("--agent claude-code") && command2.includes(">/dev/null 2>&1") && command2.trimEnd().endsWith("|| true") && hook.async === true;
42634
+ }
42635
+ function hooksOf(settings3) {
42390
42636
  const hooks = settings3.hooks;
42391
- if (!hooks) {
42392
- return { installed: false, other_hooks_exist: false };
42637
+ return hooks !== null && typeof hooks === "object" && !Array.isArray(hooks) ? hooks : {};
42638
+ }
42639
+ async function checkHooks() {
42640
+ let settings3;
42641
+ try {
42642
+ settings3 = loadClaudeSettings().settings;
42643
+ } catch {
42644
+ return statusOf(0, 0, false);
42393
42645
  }
42394
- let bandHookCount = 0;
42646
+ const hooks = hooksOf(settings3);
42647
+ let current3 = 0;
42648
+ let bandEvents = 0;
42395
42649
  let otherHookCount = 0;
42396
- for (const [, eventHooks] of Object.entries(hooks)) {
42650
+ for (const [event, eventHooks] of Object.entries(hooks)) {
42397
42651
  if (!Array.isArray(eventHooks)) continue;
42398
- for (const hook of eventHooks) {
42399
- const h = hook;
42400
- if (!h.hooks || !Array.isArray(h.hooks)) continue;
42401
- for (const inner of h.hooks) {
42402
- if (inner.type === "command" && inner.command) {
42403
- if (isBandHook(inner.command)) {
42404
- bandHookCount++;
42405
- } else {
42406
- otherHookCount++;
42407
- }
42652
+ let eventHasBand = false;
42653
+ let eventIsCurrent = false;
42654
+ for (const entry of eventHooks) {
42655
+ const inner = entry?.hooks;
42656
+ if (!Array.isArray(inner)) continue;
42657
+ for (const h of inner) {
42658
+ if (h?.type !== "command" || typeof h.command !== "string") continue;
42659
+ if (isBandHook(h.command)) {
42660
+ eventHasBand = true;
42661
+ if (HOOK_EVENTS.includes(event) && isCurrentCommand(h)) eventIsCurrent = true;
42662
+ } else {
42663
+ otherHookCount++;
42408
42664
  }
42409
42665
  }
42410
42666
  }
42667
+ if (eventHasBand) bandEvents++;
42668
+ if (eventIsCurrent) current3++;
42411
42669
  }
42670
+ return statusOf(bandEvents, current3, otherHookCount > 0);
42671
+ }
42672
+ function statusOf(bandEvents, currentEvents, otherHooksExist) {
42673
+ const installed = currentEvents >= HOOK_EVENTS.length && bandEvents === currentEvents;
42412
42674
  return {
42413
- installed: bandHookCount >= HOOK_EVENTS.length,
42414
- other_hooks_exist: otherHookCount > 0
42675
+ installed,
42676
+ outdated: !installed && bandEvents > 0,
42677
+ other_hooks_exist: otherHooksExist,
42678
+ version: HOOKS_VERSION
42415
42679
  };
42416
42680
  }
42417
- async function installHooks() {
42418
- let bandPath = null;
42681
+ function hookCommand(bandPath) {
42682
+ return `"${bandPath}" notify --agent claude-code >/dev/null 2>&1 || true`;
42683
+ }
42684
+ async function findBand() {
42419
42685
  if (process.platform !== "win32") {
42420
42686
  try {
42421
42687
  await import("node:fs/promises").then((m2) => m2.stat("/usr/local/bin/band"));
42422
- bandPath = "/usr/local/bin/band";
42688
+ return "/usr/local/bin/band";
42423
42689
  } catch {
42424
42690
  }
42425
42691
  }
42426
- if (!bandPath) {
42427
- bandPath = await whichBinary("band");
42428
- }
42429
- if (!bandPath) {
42430
- throw new Error("Could not find band CLI binary. Please install it first.");
42431
- }
42432
- const settingsPath = claudeSettingsPath();
42433
- mkdirSync5(dirname5(settingsPath), { recursive: true });
42434
- const settings3 = loadClaudeSettings();
42435
- const hooks = settings3.hooks || {};
42692
+ const found = await whichBinary("band");
42693
+ if (!found) throw new Error("Could not find band CLI binary. Please install it first.");
42694
+ return found;
42695
+ }
42696
+ function withoutBandEntries(hooks) {
42697
+ const result = {};
42436
42698
  for (const [event, eventHooks] of Object.entries(hooks)) {
42437
- if (HOOK_EVENTS.includes(event)) continue;
42438
- if (!Array.isArray(eventHooks)) continue;
42439
- const filtered = eventHooks.filter((entry) => {
42699
+ if (!Array.isArray(eventHooks)) {
42700
+ result[event] = eventHooks;
42701
+ continue;
42702
+ }
42703
+ const kept = [];
42704
+ for (const entry of eventHooks) {
42705
+ if (!isBandEntry(entry)) {
42706
+ kept.push(entry);
42707
+ continue;
42708
+ }
42440
42709
  const e = entry;
42441
- if (!e.hooks || !Array.isArray(e.hooks)) return true;
42442
- return !e.hooks.some((h) => h.type === "command" && h.command && isBandHook(h.command));
42443
- });
42444
- if (filtered.length > 0) {
42445
- hooks[event] = filtered;
42446
- } else {
42447
- delete hooks[event];
42710
+ const rest = (e.hooks ?? []).filter(
42711
+ (h) => !(h?.type === "command" && typeof h.command === "string" && isBandHook(h.command))
42712
+ );
42713
+ if (rest.length > 0) kept.push({ ...e, hooks: rest });
42448
42714
  }
42715
+ if (kept.length > 0 || eventHooks.length === 0) result[event] = kept;
42449
42716
  }
42717
+ return result;
42718
+ }
42719
+ async function installHooks() {
42720
+ const bandPath = await findBand();
42721
+ const loaded = loadClaudeSettings();
42722
+ const hooks = withoutBandEntries(hooksOf(loaded.settings));
42450
42723
  for (const event of HOOK_EVENTS) {
42451
- const existing = hooks[event] || [];
42452
- const filtered = existing.filter((entry) => {
42453
- const e = entry;
42454
- if (!e.hooks || !Array.isArray(e.hooks)) return true;
42455
- return !e.hooks.some((h) => h.type === "command" && h.command && isBandHook(h.command));
42456
- });
42457
- filtered.push({
42458
- hooks: [{ type: "command", command: `${bandPath} notify --agent claude-code` }]
42459
- });
42460
- hooks[event] = filtered;
42724
+ const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
42725
+ hooks[event] = [
42726
+ ...existing,
42727
+ {
42728
+ hooks: [
42729
+ {
42730
+ type: "command",
42731
+ command: hookCommand(bandPath),
42732
+ async: true,
42733
+ timeout: HOOK_TIMEOUT_SECONDS
42734
+ }
42735
+ ]
42736
+ }
42737
+ ];
42461
42738
  }
42462
- settings3.hooks = hooks;
42463
- writeFileSync4(settingsPath, JSON.stringify(settings3, null, 2), "utf-8");
42739
+ loaded.settings.hooks = hooks;
42740
+ saveClaudeSettings(loaded);
42464
42741
  }
42465
- var HOOK_EVENTS;
42742
+ async function removeHooks() {
42743
+ const loaded = loadClaudeSettings();
42744
+ if (!("hooks" in loaded.settings)) return;
42745
+ const before = hooksOf(loaded.settings);
42746
+ const after = withoutBandEntries(before);
42747
+ if (JSON.stringify(after) === JSON.stringify(before)) return;
42748
+ if (Object.keys(after).length > 0) loaded.settings.hooks = after;
42749
+ else delete loaded.settings.hooks;
42750
+ saveClaudeSettings(loaded);
42751
+ }
42752
+ var HOOK_EVENTS, HOOKS_VERSION, HOOK_TIMEOUT_SECONDS, BAND_NOTIFY;
42466
42753
  var init_hooks_install = __esm({
42467
42754
  "../../packages/host-local/src/agents/hooks-install.ts"() {
42468
42755
  "use strict";
42469
42756
  init_path();
42470
42757
  HOOK_EVENTS = [
42471
- "PreToolUse",
42472
- "PermissionRequest",
42473
42758
  "UserPromptSubmit",
42759
+ "PreToolUse",
42474
42760
  "PostToolUse",
42761
+ "PostToolUseFailure",
42762
+ "PermissionRequest",
42475
42763
  "Stop",
42764
+ "StopFailure",
42765
+ "SubagentStart",
42766
+ "SubagentStop",
42767
+ "PostCompact",
42476
42768
  "SessionEnd"
42477
42769
  ];
42770
+ HOOKS_VERSION = 2;
42771
+ HOOK_TIMEOUT_SECONDS = 5;
42772
+ BAND_NOTIFY = /(^|[\s"'/\\])band(\.exe|\.cmd)?["']?\s+notify(\s|$)/;
42478
42773
  }
42479
42774
  });
42480
42775
 
@@ -42553,11 +42848,11 @@ var init_mcp_stdio = __esm({
42553
42848
  });
42554
42849
 
42555
42850
  // ../../packages/host-local/src/process/cli-binary.ts
42556
- import { lstatSync as lstatSync3, realpathSync as realpathSync3 } from "node:fs";
42851
+ import { lstatSync as lstatSync3, realpathSync as realpathSync4 } from "node:fs";
42557
42852
  import { platform } from "node:os";
42558
42853
  import { delimiter as delimiter3, join as join19, resolve as resolve4 } from "node:path";
42559
42854
  function findCliBinaryAt(opts) {
42560
- const { cwd, dirname: dirname16 } = opts;
42855
+ const { cwd, dirname: dirname17 } = opts;
42561
42856
  const exe = platform() === "win32" ? "band.exe" : "band";
42562
42857
  const explicit = process.env.BAND_CLI_PATH;
42563
42858
  if (explicit) {
@@ -42573,7 +42868,7 @@ function findCliBinaryAt(opts) {
42573
42868
  // cwd = repo root (fallback)
42574
42869
  resolve4(cwd, "apps"),
42575
42870
  // From this source file in dev (packages/host-local/src/process/ → apps/)
42576
- resolve4(dirname16, "..", "..", "..", "..", "apps"),
42871
+ resolve4(dirname17, "..", "..", "..", "..", "apps"),
42577
42872
  // From bundled `dist/` file (<Resources>/web/dist/ → <Resources>/) only
42578
42873
  // — in dev mode this resolves to `packages/`, which has no
42579
42874
  // `cli/target/<profile>/band` and is harmless; the walk above is the
@@ -42581,7 +42876,7 @@ function findCliBinaryAt(opts) {
42581
42876
  // layout under <Resources>/cli/ would still resolve. Today's Electron
42582
42877
  // bundle ships the binary under `binaries/` (handled by Strategy B),
42583
42878
  // so this strategy never hits in production either.
42584
- resolve4(dirname16, "..", "..", "..")
42879
+ resolve4(dirname17, "..", "..", "..")
42585
42880
  ];
42586
42881
  for (const appsDir of appsStrategies) {
42587
42882
  for (const profile of ["release", "debug"]) {
@@ -42598,7 +42893,7 @@ function findCliBinaryAt(opts) {
42598
42893
  resolve4(cwd, "..", "binaries", exe),
42599
42894
  // From the bundled dist file (<Resources>/web/dist/start-server.mjs)
42600
42895
  // → <Resources>/binaries/band
42601
- resolve4(dirname16, "..", "..", "binaries", exe)
42896
+ resolve4(dirname17, "..", "..", "binaries", exe)
42602
42897
  ];
42603
42898
  for (const p2 of electronCandidates) {
42604
42899
  try {
@@ -42610,7 +42905,7 @@ function findCliBinaryAt(opts) {
42610
42905
  for (const dir of (process.env.PATH ?? "").split(delimiter3)) {
42611
42906
  if (!dir) continue;
42612
42907
  try {
42613
- return realpathSync3(join19(dir, exe));
42908
+ return realpathSync4(join19(dir, exe));
42614
42909
  } catch {
42615
42910
  }
42616
42911
  }
@@ -42627,21 +42922,21 @@ var init_cli_binary = __esm({
42627
42922
 
42628
42923
  // ../../packages/host-local/src/agents/skills-install.ts
42629
42924
  import { execFile as execFile4 } from "node:child_process";
42630
- import { statSync as statSync10 } from "node:fs";
42925
+ import { statSync as statSync12 } from "node:fs";
42631
42926
  import { homedir as homedir10 } from "node:os";
42632
42927
  import { join as join20 } from "node:path";
42633
42928
  async function findBandBinary() {
42634
42929
  const override = process.env.BAND_CLI_BIN;
42635
42930
  if (override) {
42636
42931
  try {
42637
- statSync10(override);
42932
+ statSync12(override);
42638
42933
  return override;
42639
42934
  } catch {
42640
42935
  }
42641
42936
  }
42642
42937
  if (process.platform !== "win32") {
42643
42938
  try {
42644
- statSync10("/usr/local/bin/band");
42939
+ statSync12("/usr/local/bin/band");
42645
42940
  return "/usr/local/bin/band";
42646
42941
  } catch {
42647
42942
  }
@@ -42743,7 +43038,7 @@ var init_skills_install = __esm({
42743
43038
 
42744
43039
  // ../../packages/host-local/src/browser/chromium.ts
42745
43040
  import { spawn as spawn2 } from "node:child_process";
42746
- import { closeSync as closeSync3, existsSync as existsSync8, openSync as openSync3, readdirSync as readdirSync7 } from "node:fs";
43041
+ import { closeSync as closeSync4, existsSync as existsSync9, openSync as openSync4, readdirSync as readdirSync7 } from "node:fs";
42747
43042
  import { mkdir as mkdir2, readFile as readFile2, rm } from "node:fs/promises";
42748
43043
  import { homedir as homedir11 } from "node:os";
42749
43044
  import { join as join21 } from "node:path";
@@ -42752,7 +43047,7 @@ function playwrightCandidates() {
42752
43047
  process.env.PLAYWRIGHT_BROWSERS_PATH,
42753
43048
  join21(homedir11(), "Library/Caches/ms-playwright"),
42754
43049
  join21(homedir11(), ".cache/ms-playwright")
42755
- ].filter((root) => !!root && existsSync8(root));
43050
+ ].filter((root) => !!root && existsSync9(root));
42756
43051
  const found = [];
42757
43052
  for (const root of roots) {
42758
43053
  const dirs = readdirSync7(root).filter((d) => /^chromium-\d+$/.test(d)).sort().reverse();
@@ -42782,7 +43077,7 @@ function playwrightCandidates() {
42782
43077
  }
42783
43078
  function findChromium() {
42784
43079
  const fromEnv = process.env.BAND_CHROMIUM_BIN;
42785
- if (fromEnv) return existsSync8(fromEnv) ? fromEnv : void 0;
43080
+ if (fromEnv) return existsSync9(fromEnv) ? fromEnv : void 0;
42786
43081
  const candidates = [
42787
43082
  "/usr/bin/chromium",
42788
43083
  "/usr/bin/chromium-browser",
@@ -42792,10 +43087,10 @@ function findChromium() {
42792
43087
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
42793
43088
  ...playwrightCandidates()
42794
43089
  ];
42795
- return candidates.find((c) => existsSync8(c));
43090
+ return candidates.find((c) => existsSync9(c));
42796
43091
  }
42797
43092
  async function readDevToolsPort(profileDir, proc, logPath, timeoutMs) {
42798
- const file2 = join21(profileDir, "DevToolsActivePort");
43093
+ const file3 = join21(profileDir, "DevToolsActivePort");
42799
43094
  const deadline = Date.now() + timeoutMs;
42800
43095
  while (Date.now() < deadline) {
42801
43096
  if (proc.exitCode !== null || proc.signalCode !== null) {
@@ -42805,7 +43100,7 @@ async function readDevToolsPort(profileDir, proc, logPath, timeoutMs) {
42805
43100
  throw new Error("Chromium hit a fatal error at startup");
42806
43101
  }
42807
43102
  try {
42808
- const [port, path2] = (await readFile2(file2, "utf8")).split("\n");
43103
+ const [port, path2] = (await readFile2(file3, "utf8")).split("\n");
42809
43104
  if (port && path2) return { port: Number(port), path: path2.trim() };
42810
43105
  } catch {
42811
43106
  }
@@ -42955,13 +43250,13 @@ var init_chromium = __esm({
42955
43250
  if (attempt > 1) {
42956
43251
  await Promise.all(SINGLETON_FILES.map((f2) => rm(join21(profileDir, f2), { force: true })));
42957
43252
  }
42958
- const logFd = openSync3(logPath, "w", 384);
43253
+ const logFd = openSync4(logPath, "w", 384);
42959
43254
  const proc = spawn2(bin, args, {
42960
43255
  stdio: ["ignore", "ignore", logFd],
42961
43256
  detached: true,
42962
43257
  env: { ...env, HOME: home }
42963
43258
  });
42964
- closeSync3(logFd);
43259
+ closeSync4(logFd);
42965
43260
  const exited = new Promise((resolve15) => {
42966
43261
  proc.once("exit", () => resolve15());
42967
43262
  proc.once("error", () => resolve15());
@@ -43380,7 +43675,7 @@ var init_git_client = __esm({
43380
43675
 
43381
43676
  // ../../packages/host-local/src/lsp/lsp-manager.ts
43382
43677
  import { spawn as spawn3 } from "node:child_process";
43383
- import { dirname as dirname6, join as join24, resolve as resolve5 } from "node:path";
43678
+ import { dirname as dirname7, join as join24, resolve as resolve5 } from "node:path";
43384
43679
  import { fileURLToPath as fileURLToPath2 } from "node:url";
43385
43680
  function toServerId(worktreeId4, lang) {
43386
43681
  return `${worktreeId4}:${lang}`;
@@ -43505,7 +43800,7 @@ var init_lsp_manager = __esm({
43505
43800
  "use strict";
43506
43801
  init_src();
43507
43802
  init_path();
43508
- __dirname2 = dirname6(fileURLToPath2(import.meta.url));
43803
+ __dirname2 = dirname7(fileURLToPath2(import.meta.url));
43509
43804
  log11 = createLogger("lsp");
43510
43805
  LANG_SERVER_CONFIG = {
43511
43806
  typescript: { command: "typescript-language-server", args: ["--stdio"] }
@@ -47803,27 +48098,27 @@ function parseEnvironment(text3) {
47803
48098
  if (parsed.success) return { ok: true, environment: parsed.data };
47804
48099
  return { ok: false, issues: zodIssues(parsed.error) };
47805
48100
  }
47806
- function repoRelative(file2) {
47807
- if (posix.isAbsolute(file2) || /^[A-Za-z]:[\\/]/.test(file2)) return null;
47808
- const normal = posix.normalize(file2.replaceAll("\\", "/"));
48101
+ function repoRelative(file3) {
48102
+ if (posix.isAbsolute(file3) || /^[A-Za-z]:[\\/]/.test(file3)) return null;
48103
+ const normal = posix.normalize(file3.replaceAll("\\", "/"));
47809
48104
  if (normal === ".." || normal.startsWith("../")) return null;
47810
48105
  return normal;
47811
48106
  }
47812
48107
  async function checkReferences(environment, exists3) {
47813
48108
  const issues = [];
47814
48109
  for (const key2 of ["devcontainer", "dockerfile"]) {
47815
- const file2 = environment.build?.[key2];
47816
- if (file2 === void 0) continue;
47817
- const relative2 = repoRelative(file2);
48110
+ const file3 = environment.build?.[key2];
48111
+ if (file3 === void 0) continue;
48112
+ const relative2 = repoRelative(file3);
47818
48113
  if (relative2 === null) {
47819
48114
  issues.push({
47820
48115
  path: `build.${key2}`,
47821
- message: `"${file2}" must be a path inside the repository`
48116
+ message: `"${file3}" must be a path inside the repository`
47822
48117
  });
47823
48118
  } else if (!await exists3(relative2)) {
47824
48119
  issues.push({
47825
48120
  path: `build.${key2}`,
47826
- message: `file "${file2}" does not exist in the repository`
48121
+ message: `file "${file3}" does not exist in the repository`
47827
48122
  });
47828
48123
  }
47829
48124
  }
@@ -47912,9 +48207,9 @@ function keyInputs(environment) {
47912
48207
  const inputs = [{ path: ENVIRONMENT_FILE, required: true }];
47913
48208
  const build = environment.build;
47914
48209
  for (const key2 of ["devcontainer", "dockerfile"]) {
47915
- const file2 = build?.[key2];
47916
- if (file2 === void 0) continue;
47917
- const relative2 = repoRelative(file2);
48210
+ const file3 = build?.[key2];
48211
+ if (file3 === void 0) continue;
48212
+ const relative2 = repoRelative(file3);
47918
48213
  if (relative2 === null) continue;
47919
48214
  inputs.push({ path: relative2, required: true });
47920
48215
  const dir = posix2.dirname(relative2);
@@ -48463,10 +48758,10 @@ var init_remote_url = __esm({
48463
48758
  // ../../packages/host-local/src/repos/host-repos.ts
48464
48759
  import { execFile as execFile10 } from "node:child_process";
48465
48760
  import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
48466
- import { existsSync as existsSync9 } from "node:fs";
48761
+ import { existsSync as existsSync10 } from "node:fs";
48467
48762
  import { mkdir as mkdir3, readdir as readdir4, readFile as readFile4, realpath as realpath2, rename as rename2, stat as stat5, writeFile as writeFile2 } from "node:fs/promises";
48468
48763
  import { homedir as homedir13 } from "node:os";
48469
- import { dirname as dirname7, join as join27, sep as sep3 } from "node:path";
48764
+ import { dirname as dirname8, join as join27, sep as sep3 } from "node:path";
48470
48765
  function defaultReposDir() {
48471
48766
  return process.env.BAND_REPOS_DIR?.trim() || join27(homedir13(), "band", "repos");
48472
48767
  }
@@ -48517,10 +48812,10 @@ async function browseFolder(path2) {
48517
48812
  let isDir = d.isDirectory();
48518
48813
  if (d.isSymbolicLink()) isDir = await isDirectory(full);
48519
48814
  if (!isDir) continue;
48520
- entries.push({ name: d.name, path: full, isGit: existsSync9(join27(full, ".git")) });
48815
+ entries.push({ name: d.name, path: full, isGit: existsSync10(join27(full, ".git")) });
48521
48816
  }
48522
48817
  entries.sort((a, b2) => a.name.localeCompare(b2.name));
48523
- const parent = dirname7(target);
48818
+ const parent = dirname8(target);
48524
48819
  return {
48525
48820
  path: target,
48526
48821
  parent: parent === target ? null : parent,
@@ -48556,13 +48851,13 @@ var init_host_repos = __esm({
48556
48851
  return this.cache;
48557
48852
  }
48558
48853
  save(mappings) {
48559
- const file2 = this.options.mappingsFile();
48854
+ const file3 = this.options.mappingsFile();
48560
48855
  this.writing = this.writing.then(async () => {
48561
- await mkdir3(dirname7(file2), { recursive: true, mode: 448 });
48562
- const tmp = `${file2}.${randomBytes2(4).toString("hex")}.tmp`;
48856
+ await mkdir3(dirname8(file3), { recursive: true, mode: 448 });
48857
+ const tmp = `${file3}.${randomBytes2(4).toString("hex")}.tmp`;
48563
48858
  await writeFile2(tmp, `${JSON.stringify(mappings, null, 2)}
48564
48859
  `, { mode: 384 });
48565
- await rename2(tmp, file2);
48860
+ await rename2(tmp, file3);
48566
48861
  });
48567
48862
  return this.writing;
48568
48863
  }
@@ -48619,12 +48914,12 @@ var init_host_repos = __esm({
48619
48914
  async doEnsure(key2, url2, owner, name4) {
48620
48915
  const mappings = await this.load();
48621
48916
  const mapped = mappings[key2];
48622
- if (mapped && existsSync9(mapped.path) && await isDirectory(mapped.path)) {
48917
+ if (mapped && existsSync10(mapped.path) && await isDirectory(mapped.path)) {
48623
48918
  return { path: mapped.path, cloned: false };
48624
48919
  }
48625
48920
  const base = join27(this.options.reposDir(), owner, name4);
48626
48921
  let dest = base;
48627
- if (existsSync9(dest)) {
48922
+ if (existsSync10(dest)) {
48628
48923
  const origin = await git(["remote", "get-url", "origin"], dest);
48629
48924
  const there = origin ? parseRemoteUrl(origin)?.key : void 0;
48630
48925
  if (there === key2) {
@@ -48634,7 +48929,7 @@ var init_host_repos = __esm({
48634
48929
  const empty = (await readdir4(dest).catch(() => ["x"])).length === 0;
48635
48930
  if (!empty) dest = `${base}-${createHash3("sha256").update(key2).digest("hex").slice(0, 6)}`;
48636
48931
  }
48637
- await mkdir3(dirname7(dest), { recursive: true });
48932
+ await mkdir3(dirname8(dest), { recursive: true });
48638
48933
  await clone2(url2, dest);
48639
48934
  const canonical = await realpath2(dest);
48640
48935
  await this.map(url2, canonical);
@@ -48696,9 +48991,9 @@ function createIterator(options) {
48696
48991
  if (!rawFile || typeof lineNumber !== "number" || rawContent === void 0) {
48697
48992
  continue;
48698
48993
  }
48699
- const file2 = rawFile.startsWith("./") ? rawFile.slice(2) : rawFile;
48994
+ const file3 = rawFile.startsWith("./") ? rawFile.slice(2) : rawFile;
48700
48995
  const content = rawContent.endsWith("\n") ? rawContent.slice(0, -1) : rawContent;
48701
- matches2.push({ file: file2, line: lineNumber, content });
48996
+ matches2.push({ file: file3, line: lineNumber, content });
48702
48997
  }
48703
48998
  signal();
48704
48999
  });
@@ -48804,15 +49099,15 @@ async function loadRepoConfig(host, worktreePath, repoPath) {
48804
49099
  }
48805
49100
  async function loadEnvironment(host, worktreePath, repoPath) {
48806
49101
  for (const base of [worktreePath, repoPath]) {
48807
- const file2 = join28(base, ENVIRONMENT_FILE);
48808
- const text3 = await readConfig(host, file2);
49102
+ const file3 = join28(base, ENVIRONMENT_FILE);
49103
+ const text3 = await readConfig(host, file3);
48809
49104
  if (text3 === null) continue;
48810
49105
  const exists3 = (relative2) => host.fs.stat(join28(base, relative2)).then(
48811
49106
  () => true,
48812
49107
  () => false
48813
49108
  );
48814
49109
  const result = await validateEnvironment(text3, exists3);
48815
- return result.ok ? { source: file2, environment: result.environment, issues: [] } : { source: file2, environment: null, issues: result.issues };
49110
+ return result.ok ? { source: file3, environment: result.environment, issues: [] } : { source: file3, environment: null, issues: result.issues };
48816
49111
  }
48817
49112
  return { source: null, environment: null, issues: [] };
48818
49113
  }
@@ -48949,7 +49244,7 @@ var init_script_run = __esm({
48949
49244
  });
48950
49245
 
48951
49246
  // ../../packages/host-local/src/setup/worktree-files.ts
48952
- import { dirname as dirname8, isAbsolute, join as join30, relative, resolve as resolve6, sep as sep4 } from "node:path";
49247
+ import { dirname as dirname9, isAbsolute, join as join30, relative, resolve as resolve6, sep as sep4 } from "node:path";
48953
49248
  async function copyWorktreeFiles(host, repoPath, worktreePath) {
48954
49249
  const byAbs = /* @__PURE__ */ new Map();
48955
49250
  const missingFromConfig = [];
@@ -48992,7 +49287,7 @@ async function copyWorktreeFiles(host, repoPath, worktreePath) {
48992
49287
  );
48993
49288
  continue;
48994
49289
  }
48995
- await host.fs.mkdir(dirname8(dest), { recursive: true });
49290
+ await host.fs.mkdir(dirname9(dest), { recursive: true });
48996
49291
  await host.fs.copy(absSource, dest);
48997
49292
  log14.debug({ source, file: rel }, "copied worktree file");
48998
49293
  copied.push(rel);
@@ -49329,7 +49624,8 @@ var init_local_host = __esm({
49329
49624
  usageReader: async (agent) => getUsageReader(agent.agentType, { command: agent.command }),
49330
49625
  installSkills: (options) => installSkills(options),
49331
49626
  hooksStatus: () => checkHooks(),
49332
- installHooks: () => installHooks()
49627
+ installHooks: () => installHooks(),
49628
+ removeHooks: () => removeHooks()
49333
49629
  };
49334
49630
  get pty() {
49335
49631
  return this.options.terminalBackend();
@@ -49505,11 +49801,11 @@ var init_src5 = __esm({
49505
49801
  });
49506
49802
 
49507
49803
  // src/server/infra/db/queries/repos.ts
49508
- import { existsSync as existsSync10 } from "node:fs";
49804
+ import { existsSync as existsSync11 } from "node:fs";
49509
49805
  import { join as join32 } from "node:path";
49510
49806
  function reconcileKindForRepo(repo) {
49511
- if (!repo.path || !existsSync10(repo.path)) return false;
49512
- const detectedKind = existsSync10(join32(repo.path, ".git")) ? "git" : "plain";
49807
+ if (!repo.path || !existsSync11(repo.path)) return false;
49808
+ const detectedKind = existsSync11(join32(repo.path, ".git")) ? "git" : "plain";
49513
49809
  if (detectedKind === repo.kind) return false;
49514
49810
  repo.kind = detectedKind;
49515
49811
  if (detectedKind === "plain") {
@@ -49852,12 +50148,18 @@ var init_repos = __esm({
49852
50148
  * Records where a worktree was started from, or clears it with `null`. Focused UPDATE, because
49853
50149
  * `saveAll` keeps the stored origin and never writes the one in its snapshot.
49854
50150
  */
49855
- setOrigin(repoName, name4, origin) {
50151
+ setOrigin(repoName, name4, origin, hostId2) {
49856
50152
  getDb().update(worktrees).set({
49857
50153
  originWorktreeId: origin?.worktreeId ?? null,
49858
50154
  originChatId: origin?.chatId ?? null,
49859
50155
  originTerminalId: origin?.terminalId ?? null
49860
- }).where(and(eq(worktrees.repoName, repoName), eq(worktrees.name, name4))).run();
50156
+ }).where(
50157
+ and(
50158
+ eq(worktrees.repoName, repoName),
50159
+ eq(worktrees.name, name4),
50160
+ ...hostId2 === void 0 ? [] : [eq(worktrees.hostId, hostId2)]
50161
+ )
50162
+ ).run();
49861
50163
  }
49862
50164
  /**
49863
50165
  * Path and kind of one repo, by primary key. For per-request lookups
@@ -50053,16 +50355,17 @@ var init_worktree_statuses = __esm({
50053
50355
  db.delete(worktreeStatuses).where(eq(worktreeStatuses.worktreeId, worktreeId4)).run();
50054
50356
  }
50055
50357
  /**
50056
- * Reset every "working" / "needs_attention" row back to "waiting" and
50358
+ * Reset every row that is not "done" or "failed" back to "idle" and
50057
50359
  * return the count of affected rows. Used at server startup to clean
50058
50360
  * up stale state from the previous run.
50059
50361
  */
50060
- resetActiveToWaiting(now) {
50362
+ resetActiveToIdle(now) {
50061
50363
  const db = getDb();
50062
- const result = db.update(worktreeStatuses).set({ agentStatus: "waiting", updatedAt: now }).where(
50364
+ const result = db.update(worktreeStatuses).set({ agentStatus: "idle", updatedAt: now }).where(
50063
50365
  or(
50064
50366
  eq(worktreeStatuses.agentStatus, "working"),
50065
- eq(worktreeStatuses.agentStatus, "needs_attention")
50367
+ eq(worktreeStatuses.agentStatus, "monitoring"),
50368
+ eq(worktreeStatuses.agentStatus, "question")
50066
50369
  )
50067
50370
  ).run();
50068
50371
  return Number(result.changes);
@@ -50108,22 +50411,22 @@ var init_worktree_statuses = __esm({
50108
50411
  const db = getDb();
50109
50412
  db.delete(worktreeStatusSources).where(eq(worktreeStatusSources.worktreeId, worktreeId4)).run();
50110
50413
  }
50111
- /** Delete every source. Used at server startup: no agent state survives it. */
50112
- removeAll() {
50113
- getDb().delete(worktreeStatusSources).run();
50114
- }
50115
50414
  /**
50116
- * Set the worktree's `needs_attention` sources to `waiting`, except the
50117
- * ones in `keep`.
50415
+ * Delete every source that is not `done` or `failed`. Used at server
50416
+ * startup: no running agent survives it, but a finished one stays.
50118
50417
  */
50119
- acknowledge(worktreeId4, keep, now) {
50120
- const db = getDb();
50121
- const conditions = [
50122
- eq(worktreeStatusSources.worktreeId, worktreeId4),
50123
- eq(worktreeStatusSources.status, "needs_attention")
50124
- ];
50125
- if (keep.length > 0) conditions.push(notInArray(worktreeStatusSources.sourceId, keep));
50126
- db.update(worktreeStatusSources).set({ status: "waiting", updatedAt: now }).where(and(...conditions)).run();
50418
+ removeActive() {
50419
+ getDb().delete(worktreeStatusSources).where(notInArray(worktreeStatusSources.status, ["done", "failed"])).run();
50420
+ }
50421
+ /** Set the worktree's `done` and `failed` sources to `idle`. Returns how many changed. */
50422
+ acknowledge(worktreeId4, now) {
50423
+ const result = getDb().update(worktreeStatusSources).set({ status: "idle", updatedAt: now }).where(
50424
+ and(
50425
+ eq(worktreeStatusSources.worktreeId, worktreeId4),
50426
+ inArray(worktreeStatusSources.status, ["done", "failed"])
50427
+ )
50428
+ ).run();
50429
+ return Number(result.changes);
50127
50430
  }
50128
50431
  };
50129
50432
  }
@@ -50183,8 +50486,8 @@ var init_isolation = __esm({
50183
50486
  });
50184
50487
 
50185
50488
  // src/server/services/_utils/runner-config.ts
50186
- import { existsSync as existsSync11 } from "node:fs";
50187
- import { dirname as dirname9, isAbsolute as isAbsolute2, join as join33, resolve as resolve7 } from "node:path";
50489
+ import { existsSync as existsSync12 } from "node:fs";
50490
+ import { dirname as dirname10, isAbsolute as isAbsolute2, join as join33, resolve as resolve7 } from "node:path";
50188
50491
  function parseRunners(raw) {
50189
50492
  if (raw === void 0 || raw === null) return { runners: [], errors: [] };
50190
50493
  if (!Array.isArray(raw)) return { runners: [], errors: ["runners must be a list"] };
@@ -50210,8 +50513,8 @@ function bundledRunnersDir() {
50210
50513
  let dir = import.meta.dirname;
50211
50514
  for (let i = 0; i < 10; i++) {
50212
50515
  const candidate = join33(dir, "runners");
50213
- if (existsSync11(join33(candidate, "local", "spawn.sh"))) return candidate;
50214
- const parent = dirname9(dir);
50516
+ if (existsSync12(join33(candidate, "local", "spawn.sh"))) return candidate;
50517
+ const parent = dirname10(dir);
50215
50518
  if (parent === dir) break;
50216
50519
  dir = parent;
50217
50520
  }
@@ -50691,16 +50994,20 @@ function upsertWorktreeStatus(worktreeId4, agent) {
50691
50994
  tabStatuses: listTabStatuses(worktreeId4)
50692
50995
  };
50693
50996
  }
50694
- function resolveWorktreeIdByCwd(cwd) {
50997
+ function resolveWorktreeIdByCwd(cwd, hostId2) {
50695
50998
  const state2 = loadState();
50999
+ let other = null;
50696
51000
  for (const proj of state2.repos) {
50697
51001
  for (const wt of proj.worktrees) {
50698
- if (cwd === wt.path || cwd.startsWith(`${wt.path}/`)) {
50699
- return toWorktreeId(proj.name, wt.name, wt.hostId);
50700
- }
51002
+ const wtHost = wt.hostId ?? LOCAL_HOST_ID3;
51003
+ if (hostId2 !== void 0 && wtHost !== hostId2) continue;
51004
+ if (cwd !== wt.path && !cwd.startsWith(`${wt.path}/`)) continue;
51005
+ const id = toWorktreeId(proj.name, wt.name, wt.hostId);
51006
+ if (wtHost === LOCAL_HOST_ID3) return id;
51007
+ other ??= id;
50701
51008
  }
50702
51009
  }
50703
- return null;
51010
+ return other;
50704
51011
  }
50705
51012
  function chatStatusSource(chatId) {
50706
51013
  return `${CHAT_SOURCE_PREFIX}${chatId}`;
@@ -50718,20 +51025,21 @@ function deriveWorktreeStatus(worktreeId4) {
50718
51025
  best = status;
50719
51026
  }
50720
51027
  }
50721
- return best ?? "waiting";
51028
+ return best ?? "idle";
50722
51029
  }
50723
51030
  function toTabStatuses(sources) {
50724
51031
  const staleBefore = Date.now() - STALE_HOOK_WORKING_MS;
50725
51032
  const byTab = /* @__PURE__ */ new Map();
50726
51033
  for (const source of sources) {
50727
51034
  const { status } = source;
50728
- if (status !== "working" && status !== "needs_attention") continue;
51035
+ if (status === "idle" || !(status in STATUS_PRIORITY)) continue;
50729
51036
  if (isStaleSource(source, staleBefore)) continue;
51037
+ const tabStatus = status;
50730
51038
  let tab;
50731
51039
  if (source.sourceId.startsWith(CHAT_SOURCE_PREFIX)) {
50732
- tab = { chatId: source.sourceId.slice(CHAT_SOURCE_PREFIX.length), status };
51040
+ tab = { chatId: source.sourceId.slice(CHAT_SOURCE_PREFIX.length), status: tabStatus };
50733
51041
  } else if (source.terminalId) {
50734
- tab = { terminalId: source.terminalId, status };
51042
+ tab = { terminalId: source.terminalId, status: tabStatus };
50735
51043
  } else {
50736
51044
  continue;
50737
51045
  }
@@ -50765,10 +51073,10 @@ function removeWorktreeSource(worktreeId4, sourceId) {
50765
51073
  if (!statusSourceQueries.remove(worktreeId4, sourceId)) return null;
50766
51074
  return rederiveWorktreeStatus(worktreeId4);
50767
51075
  }
50768
- function acknowledgeWorktreeAttention(worktreeId4, pendingChatIds) {
51076
+ function acknowledgeWorktreeAttention(worktreeId4) {
50769
51077
  const existing = getWorktreeStatus(worktreeId4);
50770
- if (existing?.agent?.status !== "needs_attention") return existing;
50771
- statusSourceQueries.acknowledge(worktreeId4, pendingChatIds.map(chatStatusSource), Date.now());
51078
+ if (!existing) return null;
51079
+ if (statusSourceQueries.acknowledge(worktreeId4, Date.now()) === 0) return existing;
50772
51080
  return upsertWorktreeStatus(worktreeId4, { status: deriveWorktreeStatus(worktreeId4) });
50773
51081
  }
50774
51082
  function startStatusSourceCleanup() {
@@ -50786,17 +51094,33 @@ function startStatusSourceCleanup() {
50786
51094
  for (const status of changed) emit({ kind: "update", status });
50787
51095
  });
50788
51096
  }
51097
+ function subagentHold(worktreeId4, sourceId) {
51098
+ const key2 = `${worktreeId4}\0${sourceId}`;
51099
+ let hold = subagentHolds.get(key2);
51100
+ if (!hold) {
51101
+ hold = { running: /* @__PURE__ */ new Set(), deferred: null };
51102
+ subagentHolds.set(key2, hold);
51103
+ }
51104
+ return hold;
51105
+ }
51106
+ function dropSubagentHold(worktreeId4, sourceId) {
51107
+ subagentHolds.delete(`${worktreeId4}\0${sourceId}`);
51108
+ }
50789
51109
  async function applyHookNotification(notification) {
50790
51110
  const { payload } = notification;
50791
51111
  if (notification.dispatch === "chat") return null;
50792
- const worktreeId4 = resolveWorktreeIdByCwd(notification.cwd);
51112
+ const worktreeId4 = resolveWorktreeIdByCwd(notification.cwd, notification.hostId ?? LOCAL_HOST_ID3);
50793
51113
  if (!worktreeId4) return null;
50794
51114
  const agentType = notification.agent || detectHookAgentType(payload) || settingsService.getAgentDefinition(getWorktreeStatus(worktreeId4)?.agent?.codingAgentId).type;
50795
51115
  const sourceId = `hook:${hookSessionId(payload) ?? agentType}`;
50796
51116
  if (isHookSessionEnd(agentType, payload)) {
51117
+ dropSubagentHold(worktreeId4, sourceId);
50797
51118
  return removeWorktreeSource(worktreeId4, sourceId);
50798
51119
  }
50799
- const status = await mapHookPayloadToStatus(agentType, payload);
51120
+ let status = await mapHookPayloadToStatus(agentType, payload);
51121
+ if (agentType === "claude-code") {
51122
+ status = foldSubagents(subagentHold(worktreeId4, sourceId), payload, status);
51123
+ }
50800
51124
  return setWorktreeSourceStatus(worktreeId4, sourceId, {
50801
51125
  status,
50802
51126
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -50804,8 +51128,9 @@ async function applyHookNotification(notification) {
50804
51128
  });
50805
51129
  }
50806
51130
  function resetAgentStatuses() {
50807
- statusSourceQueries.removeAll();
50808
- return worktreeStatusQueries.resetActiveToWaiting(Date.now());
51131
+ subagentHolds.clear();
51132
+ statusSourceQueries.removeActive();
51133
+ return worktreeStatusQueries.resetActiveToIdle(Date.now());
50809
51134
  }
50810
51135
  function resolveWorktreeIdentity(worktreeId4) {
50811
51136
  return worktreeQueriesForIdentity.findIdentity(worktreeId4);
@@ -50814,7 +51139,7 @@ function deleteWorktreeStatus(worktreeId4) {
50814
51139
  worktreeStatusQueries.remove(worktreeId4);
50815
51140
  statusSourceQueries.removeForWorktree(worktreeId4);
50816
51141
  }
50817
- var worktreeStatusQueries, statusSourceQueries, worktreeQueriesForIdentity, repoQueries, STALE_HOOK_WORKING_MS, STATUS_PRIORITY, CHAT_SOURCE_PREFIX, MANUAL_STATUS_SOURCE;
51142
+ var worktreeStatusQueries, statusSourceQueries, worktreeQueriesForIdentity, repoQueries, STALE_HOOK_WORKING_MS, LOCAL_HOST_ID3, STATUS_PRIORITY, CHAT_SOURCE_PREFIX, MANUAL_STATUS_SOURCE, subagentHolds;
50818
51143
  var init_state = __esm({
50819
51144
  "src/server/services/state.ts"() {
50820
51145
  "use strict";
@@ -50831,9 +51156,18 @@ var init_state = __esm({
50831
51156
  worktreeQueriesForIdentity = new WorktreeQueries();
50832
51157
  repoQueries = new RepoQueries();
50833
51158
  STALE_HOOK_WORKING_MS = 15 * 6e4;
50834
- STATUS_PRIORITY = { needs_attention: 3, working: 2, waiting: 1 };
51159
+ LOCAL_HOST_ID3 = "local";
51160
+ STATUS_PRIORITY = {
51161
+ question: 6,
51162
+ failed: 5,
51163
+ working: 4,
51164
+ monitoring: 3,
51165
+ done: 2,
51166
+ idle: 1
51167
+ };
50835
51168
  CHAT_SOURCE_PREFIX = "chat:";
50836
51169
  MANUAL_STATUS_SOURCE = "manual";
51170
+ subagentHolds = /* @__PURE__ */ new Map();
50837
51171
  }
50838
51172
  });
50839
51173
 
@@ -51733,8 +52067,8 @@ var init_map_limited = __esm({
51733
52067
 
51734
52068
  // src/server/infra/subscriptions/github.ts
51735
52069
  import { createHmac, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
51736
- import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
51737
- import { dirname as dirname10, join as join36 } from "node:path";
52070
+ import { chmodSync as chmodSync2, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
52071
+ import { dirname as dirname11, join as join36 } from "node:path";
51738
52072
  function secretPath() {
51739
52073
  return join36(bandHome(), "github-webhook-secret");
51740
52074
  }
@@ -51752,7 +52086,7 @@ function ensureWebhookSecret() {
51752
52086
  const existing = readWebhookSecret();
51753
52087
  if (existing) return existing;
51754
52088
  const path2 = secretPath();
51755
- if (!existsSync12(dirname10(path2))) mkdirSync6(dirname10(path2), { recursive: true });
52089
+ if (!existsSync13(dirname11(path2))) mkdirSync7(dirname11(path2), { recursive: true });
51756
52090
  const secret = randomBytes3(32).toString("hex");
51757
52091
  writeFileSync5(path2, `${secret}
51758
52092
  `, { mode: 384 });
@@ -52485,7 +52819,7 @@ var init_oauth_client = __esm({
52485
52819
 
52486
52820
  // src/server/services/_utils/vault-crypto.ts
52487
52821
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes5 } from "node:crypto";
52488
- import { chmodSync as chmodSync3, existsSync as existsSync13, readFileSync as readFileSync9, renameSync as renameSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
52822
+ import { chmodSync as chmodSync3, existsSync as existsSync14, readFileSync as readFileSync9, renameSync as renameSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
52489
52823
  import { join as join37 } from "node:path";
52490
52824
  function vaultKeyFile() {
52491
52825
  return join37(bandHome(), "vault.key");
@@ -52507,21 +52841,21 @@ function generateKey() {
52507
52841
  function loadKey() {
52508
52842
  const fromEnv = process.env.BAND_VAULT_KEY?.trim();
52509
52843
  if (fromEnv) return parseKey(fromEnv);
52510
- const file2 = vaultKeyFile();
52511
- if (existsSync13(file2)) return parseKey(readFileSync9(file2, "utf8"));
52844
+ const file3 = vaultKeyFile();
52845
+ if (existsSync14(file3)) return parseKey(readFileSync9(file3, "utf8"));
52512
52846
  const key2 = generateKey();
52513
- writeFileSync6(file2, `${key2.toString("base64")}
52847
+ writeFileSync6(file3, `${key2.toString("base64")}
52514
52848
  `, { mode: 384, flag: "wx" });
52515
- chmodSync3(file2, 384);
52849
+ chmodSync3(file3, 384);
52516
52850
  return key2;
52517
52851
  }
52518
52852
  function stageKeyFile(key2) {
52519
- const file2 = vaultKeyFile();
52520
- const temp = `${file2}.${process.pid}.next`;
52853
+ const file3 = vaultKeyFile();
52854
+ const temp = `${file3}.${process.pid}.next`;
52521
52855
  writeFileSync6(temp, `${key2.toString("base64")}
52522
52856
  `, { mode: 384 });
52523
52857
  chmodSync3(temp, 384);
52524
- return { commit: () => renameSync5(temp, file2), discard: () => rmSync2(temp, { force: true }) };
52858
+ return { commit: () => renameSync7(temp, file3), discard: () => rmSync3(temp, { force: true }) };
52525
52859
  }
52526
52860
  function encrypt(key2, aad, plaintext) {
52527
52861
  const nonce = randomBytes5(NONCE_BYTES);
@@ -53736,9 +54070,15 @@ var init_mime_types = __esm({
53736
54070
  ".txt": "text/plain",
53737
54071
  ".md": "text/markdown",
53738
54072
  ".csv": "text/csv",
53739
- ".html": "text/html",
53740
- ".css": "text/css",
53741
- ".js": "application/javascript",
54073
+ ".html": "text/html; charset=utf-8",
54074
+ ".htm": "text/html; charset=utf-8",
54075
+ ".css": "text/css; charset=utf-8",
54076
+ ".js": "text/javascript; charset=utf-8",
54077
+ ".mjs": "text/javascript; charset=utf-8",
54078
+ ".woff": "font/woff",
54079
+ ".woff2": "font/woff2",
54080
+ ".ttf": "font/ttf",
54081
+ ".otf": "font/otf",
53742
54082
  ".ts": "application/typescript",
53743
54083
  ".xml": "application/xml",
53744
54084
  ".yaml": "application/x-yaml",
@@ -53765,6 +54105,132 @@ var init_turn_events = __esm({
53765
54105
  }
53766
54106
  });
53767
54107
 
54108
+ // src/server/services/chat-status-service.ts
54109
+ function quietMs() {
54110
+ const override = Number(process.env.BAND_AGENT_QUIET_MS);
54111
+ return Number.isFinite(override) && override > 0 ? override : QUIET_MS;
54112
+ }
54113
+ function setChatStatus(worktreeId4, chatId, status) {
54114
+ if (status === "monitoring") monitoringChats.add(chatId);
54115
+ else monitoringChats.delete(chatId);
54116
+ const updated = setWorktreeSourceStatus(worktreeId4, chatStatusSource(chatId), { status });
54117
+ emit({ kind: "update", status: updated });
54118
+ }
54119
+ function restingStatus(chatId) {
54120
+ if (agentSessionService.hasPendingRequest(chatId)) return "question";
54121
+ if (agentSessionService.nextWorkDeadline(chatId) !== null) return "monitoring";
54122
+ return "done";
54123
+ }
54124
+ function beginBandTurn(worktreeId4, chatId) {
54125
+ endAutonomous(chatId);
54126
+ clearMonitorTimer(chatId);
54127
+ setChatStatus(worktreeId4, chatId, "working");
54128
+ }
54129
+ function endBandTurn(worktreeId4, chatId, result) {
54130
+ const status = result === "done" ? restingStatus(chatId) : result;
54131
+ setChatStatus(worktreeId4, chatId, status);
54132
+ if (status === "monitoring") watchMonitoring(worktreeId4, chatId);
54133
+ }
54134
+ function endAutonomous(chatId) {
54135
+ const timer = autonomous.get(chatId);
54136
+ if (timer) clearTimeout(timer);
54137
+ autonomous.delete(chatId);
54138
+ }
54139
+ function clearMonitorTimer(chatId) {
54140
+ const timer = monitorTimers.get(chatId);
54141
+ if (timer) clearTimeout(timer);
54142
+ monitorTimers.delete(chatId);
54143
+ }
54144
+ function watchMonitoring(worktreeId4, chatId) {
54145
+ clearMonitorTimer(chatId);
54146
+ const deadline = agentSessionService.nextWorkDeadline(chatId);
54147
+ if (deadline === null) return;
54148
+ const timer = setTimeout(
54149
+ () => {
54150
+ monitorTimers.delete(chatId);
54151
+ if (runningTurn(chatId) || autonomous.has(chatId)) return;
54152
+ reconsiderMonitoring(worktreeId4, chatId);
54153
+ },
54154
+ Math.min(Math.max(deadline - Date.now(), 0) + 100, MAX_TIMER_MS)
54155
+ );
54156
+ timer.unref?.();
54157
+ monitorTimers.set(chatId, timer);
54158
+ }
54159
+ function reconsiderMonitoring(worktreeId4, chatId) {
54160
+ if (!chatService.get(chatId) || !monitoringChats.has(chatId)) return;
54161
+ const status = restingStatus(chatId);
54162
+ if (status === "monitoring") {
54163
+ watchMonitoring(worktreeId4, chatId);
54164
+ return;
54165
+ }
54166
+ setChatStatus(worktreeId4, chatId, status);
54167
+ }
54168
+ function settleAutonomous(worktreeId4, chatId) {
54169
+ autonomous.delete(chatId);
54170
+ if (!chatService.get(chatId) || runningTurn(chatId)) return;
54171
+ const status = restingStatus(chatId);
54172
+ setChatStatus(worktreeId4, chatId, status);
54173
+ if (status === "monitoring") watchMonitoring(worktreeId4, chatId);
54174
+ }
54175
+ function armQuietTimer(worktreeId4, chatId) {
54176
+ const previous = autonomous.get(chatId);
54177
+ if (previous) clearTimeout(previous);
54178
+ const timer = setTimeout(() => settleAutonomous(worktreeId4, chatId), quietMs());
54179
+ timer.unref?.();
54180
+ autonomous.set(chatId, timer);
54181
+ }
54182
+ function observeAgentStatus(hasRunningTurn) {
54183
+ if (observing) return;
54184
+ observing = true;
54185
+ runningTurn = hasRunningTurn;
54186
+ agentSessionService.observePending((chatId, worktreeId4) => {
54187
+ const waiting = agentSessionService.hasPendingRequest(chatId);
54188
+ if (runningTurn(chatId)) {
54189
+ setChatStatus(worktreeId4, chatId, waiting ? "question" : "working");
54190
+ return;
54191
+ }
54192
+ if (waiting) {
54193
+ const timer = autonomous.get(chatId);
54194
+ if (timer) clearTimeout(timer);
54195
+ autonomous.set(chatId, null);
54196
+ setChatStatus(worktreeId4, chatId, "question");
54197
+ } else if (autonomous.has(chatId)) {
54198
+ setChatStatus(worktreeId4, chatId, "working");
54199
+ armQuietTimer(worktreeId4, chatId);
54200
+ }
54201
+ });
54202
+ agentSessionService.observeActivity((chatId, worktreeId4, kind) => {
54203
+ if (runningTurn(chatId) || !chatService.get(chatId)) return;
54204
+ if (kind === "update") {
54205
+ if (agentSessionService.hasPendingRequest(chatId)) return;
54206
+ const wasWorking = autonomous.has(chatId);
54207
+ armQuietTimer(worktreeId4, chatId);
54208
+ clearMonitorTimer(chatId);
54209
+ if (!wasWorking) setChatStatus(worktreeId4, chatId, "working");
54210
+ return;
54211
+ }
54212
+ if (autonomous.has(chatId)) return;
54213
+ reconsiderMonitoring(worktreeId4, chatId);
54214
+ });
54215
+ }
54216
+ var QUIET_MS, MAX_TIMER_MS, autonomous, monitorTimers, monitoringChats, runningTurn, observing;
54217
+ var init_chat_status_service = __esm({
54218
+ "src/server/services/chat-status-service.ts"() {
54219
+ "use strict";
54220
+ init_agent_session_service();
54221
+ init_chat_service();
54222
+ init_state();
54223
+ init_watcher_service();
54224
+ QUIET_MS = 1e4;
54225
+ MAX_TIMER_MS = 2 ** 31 - 1;
54226
+ autonomous = /* @__PURE__ */ new Map();
54227
+ monitorTimers = /* @__PURE__ */ new Map();
54228
+ monitoringChats = /* @__PURE__ */ new Set();
54229
+ runningTurn = () => false;
54230
+ observing = false;
54231
+ }
54232
+ });
54233
+
53768
54234
  // ../../packages/host-local/src/git/git-refusals.ts
53769
54235
  async function pullRefusal(err, cwd, execGit2) {
53770
54236
  const stderr = errorText(err);
@@ -54212,7 +54678,7 @@ var init_worktree_checkpoints = __esm({
54212
54678
  });
54213
54679
 
54214
54680
  // src/server/services/_utils/checkpoint-store.ts
54215
- import { rmSync as rmSync3 } from "node:fs";
54681
+ import { rmSync as rmSync4 } from "node:fs";
54216
54682
  import { join as join38 } from "node:path";
54217
54683
  function worktreeCheckpointDir(worktreeId4) {
54218
54684
  return join38(bandHome(), "checkpoints", worktreeId4);
@@ -54222,7 +54688,7 @@ function checkpointDir(worktreeId4, checkpointId2) {
54222
54688
  }
54223
54689
  function forgetCheckpoints(worktreeId4) {
54224
54690
  queries2.deleteForWorktree(worktreeId4);
54225
- rmSync3(worktreeCheckpointDir(worktreeId4), { recursive: true, force: true });
54691
+ rmSync4(worktreeCheckpointDir(worktreeId4), { recursive: true, force: true });
54226
54692
  }
54227
54693
  var queries2;
54228
54694
  var init_checkpoint_store = __esm({
@@ -54342,13 +54808,13 @@ var init_remote_worktrees = __esm({
54342
54808
  // ../../packages/shared/src/repo-hosts.ts
54343
54809
  function isHostAllowed(allowed2, hostId2) {
54344
54810
  if (!allowed2) return true;
54345
- return allowed2.includes(hostId2 || LOCAL_HOST_ID3);
54811
+ return allowed2.includes(hostId2 || LOCAL_HOST_ID4);
54346
54812
  }
54347
54813
  function effectivePrimaryHost(primaryHostId, allowed2, hostsWithDefaultCheckout) {
54348
54814
  if (primaryHostId && isHostAllowed(allowed2, primaryHostId)) return primaryHostId;
54349
54815
  const candidates = hostsWithDefaultCheckout.filter((id) => isHostAllowed(allowed2, id));
54350
54816
  if (candidates.length === 0) return null;
54351
- if (candidates.includes(LOCAL_HOST_ID3)) return LOCAL_HOST_ID3;
54817
+ if (candidates.includes(LOCAL_HOST_ID4)) return LOCAL_HOST_ID4;
54352
54818
  return [...candidates].sort()[0] ?? null;
54353
54819
  }
54354
54820
  function onlyAvailableOnMessage(repo, hostNames) {
@@ -54358,10 +54824,10 @@ function checkpointsEnabledFor(stored, allowed2) {
54358
54824
  if (stored != null) return stored;
54359
54825
  return !allowed2;
54360
54826
  }
54361
- var LOCAL_HOST_ID3;
54827
+ var LOCAL_HOST_ID4;
54362
54828
  var init_repo_hosts = __esm({
54363
54829
  "../../packages/shared/src/repo-hosts.ts"() {
54364
- LOCAL_HOST_ID3 = "local";
54830
+ LOCAL_HOST_ID4 = "local";
54365
54831
  }
54366
54832
  });
54367
54833
 
@@ -56246,6 +56712,7 @@ var init_rpc = __esm({
56246
56712
  "scripts.copyFiles": 2 * 6e4,
56247
56713
  "agentEnv.installSkills": 2 * 6e4,
56248
56714
  "agentEnv.installHooks": 6e4,
56715
+ "agentEnv.removeHooks": 6e4,
56249
56716
  exec: 10 * 6e4
56250
56717
  };
56251
56718
  RemoteRpc = class {
@@ -56519,7 +56986,8 @@ var init_remote_host = __esm({
56519
56986
  // The worker takes no `home` override. It is for tests of the local install.
56520
56987
  installSkills: () => this.rpc.call("agentEnv.installSkills"),
56521
56988
  hooksStatus: () => this.rpc.call("agentEnv.hooksStatus"),
56522
- installHooks: () => this.rpc.call("agentEnv.installHooks")
56989
+ installHooks: () => this.rpc.call("agentEnv.installHooks"),
56990
+ removeHooks: () => this.rpc.call("agentEnv.removeHooks")
56523
56991
  };
56524
56992
  // ---- fs -----------------------------------------------------------------
56525
56993
  async writeFile(path2, data, options) {
@@ -56931,7 +57399,7 @@ function stringRecord(value) {
56931
57399
  function stringList2(value) {
56932
57400
  return Array.isArray(value) ? value.filter((v2) => typeof v2 === "string") : [];
56933
57401
  }
56934
- var log20, repoQueries4, LOCAL_HOST_ID4, DEFAULT_TIMEOUT_MS, DEFAULT_LEASE_MS, MAX_LEASE_MS, SWEEP_MS, leaseFilterInput, HostRequestError, PlacementService, placementService;
57402
+ var log20, repoQueries4, LOCAL_HOST_ID5, DEFAULT_TIMEOUT_MS, DEFAULT_LEASE_MS, MAX_LEASE_MS, SWEEP_MS, leaseFilterInput, HostRequestError, PlacementService, placementService;
56935
57403
  var init_placement_service = __esm({
56936
57404
  "src/server/services/placement-service.ts"() {
56937
57405
  "use strict";
@@ -56955,7 +57423,7 @@ var init_placement_service = __esm({
56955
57423
  init_worktree_service();
56956
57424
  log20 = createLogger("placement");
56957
57425
  repoQueries4 = new RepoQueries();
56958
- LOCAL_HOST_ID4 = "local";
57426
+ LOCAL_HOST_ID5 = "local";
56959
57427
  DEFAULT_TIMEOUT_MS = 10 * 6e4;
56960
57428
  DEFAULT_LEASE_MS = 6e4;
56961
57429
  MAX_LEASE_MS = 15 * 6e4;
@@ -57003,7 +57471,7 @@ var init_placement_service = __esm({
57003
57471
  for (const row of tokenService.listHosts(1e3)) {
57004
57472
  if (row.status !== "online" || !row.usable) continue;
57005
57473
  if (row.info?.mode === "ephemeral") continue;
57006
- const info = row.id === LOCAL_HOST_ID4 ? await hostRegistry.local.info().catch(() => null) : null;
57474
+ const info = row.id === LOCAL_HOST_ID5 ? await hostRegistry.local.info().catch(() => null) : null;
57007
57475
  const stored = row.info ?? {};
57008
57476
  const versions = stringRecord(info?.versions ?? stored.versions);
57009
57477
  const tools = stringRecord(info?.tools ?? stored.tools);
@@ -57035,7 +57503,7 @@ var init_placement_service = __esm({
57035
57503
  const fits = (await this.candidates()).filter(
57036
57504
  (c) => !isExclusiveHost(c.labels) && (onlyHosts === void 0 || onlyHosts.includes(c.id)) && // A worker must report the agent installed and logged in. The hub's own machine is
57037
57505
  // described live elsewhere, so it is not held to a report here.
57038
- (agent === void 0 || c.id === LOCAL_HOST_ID4 || c.agents.includes(agent)) && matches(c, placement)
57506
+ (agent === void 0 || c.id === LOCAL_HOST_ID5 || c.agents.includes(agent)) && matches(c, placement)
57039
57507
  );
57040
57508
  if (fits.length === 0) return null;
57041
57509
  const load = this.queries.worktreeCounts();
@@ -57452,8 +57920,8 @@ var init_environment_builds = __esm({
57452
57920
  get(id) {
57453
57921
  return getDb().select().from(environmentBuilds).where(eq(environmentBuilds.id, id)).get();
57454
57922
  }
57455
- setLog(id, log77) {
57456
- getDb().update(environmentBuilds).set({ log: log77 }).where(eq(environmentBuilds.id, id)).run();
57923
+ setLog(id, log78) {
57924
+ getDb().update(environmentBuilds).set({ log: log78 }).where(eq(environmentBuilds.id, id)).run();
57457
57925
  }
57458
57926
  finish(id, result) {
57459
57927
  getDb().update(environmentBuilds).set({
@@ -57821,48 +58289,48 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57821
58289
  }
57822
58290
  // ---- running ---------------------------------------------------------------
57823
58291
  async run(row, plan) {
57824
- const log77 = new BuildLog(row.id, (id, text3) => this.queries.setLog(id, text3));
58292
+ const log78 = new BuildLog(row.id, (id, text3) => this.queries.setLog(id, text3));
57825
58293
  let tmp = null;
57826
58294
  const container = `band-env-${row.id.slice(0, 12)}`;
57827
- let failure = null;
58295
+ let failure2 = null;
57828
58296
  try {
57829
- log77.add(`Building ${plan.tag} for ${plan.repo} at ${plan.ref} (${plan.commit.slice(0, 12)})`);
57830
- log77.flush();
58297
+ log78.add(`Building ${plan.tag} for ${plan.repo} at ${plan.ref} (${plan.commit.slice(0, 12)})`);
58298
+ log78.flush();
57831
58299
  tmp = await plan.host.fs.mkdtemp("band-env-");
57832
- await this.exportCheckout(plan, tmp, log77);
57833
- const toolchain = await this.buildToolchain(plan, tmp, log77);
57834
- const layered = await this.addWorkerLayer(plan, tmp, toolchain, log77);
57835
- await this.install(plan, tmp, layered, container, log77);
57836
- if (plan.registry) await this.push(plan, log77);
57837
- log77.add(`Done: ${plan.tag}`);
58300
+ await this.exportCheckout(plan, tmp, log78);
58301
+ const toolchain = await this.buildToolchain(plan, tmp, log78);
58302
+ const layered = await this.addWorkerLayer(plan, tmp, toolchain, log78);
58303
+ await this.install(plan, tmp, layered, container, log78);
58304
+ if (plan.registry) await this.push(plan, log78);
58305
+ log78.add(`Done: ${plan.tag}`);
57838
58306
  } catch (err) {
57839
- failure = err.message;
57840
- log77.add(`FAILED: ${failure}`);
58307
+ failure2 = err.message;
58308
+ log78.add(`FAILED: ${failure2}`);
57841
58309
  }
57842
- await this.exec(plan.host, "docker", ["rm", "--force", container], log77, { quiet: true }).catch(
58310
+ await this.exec(plan.host, "docker", ["rm", "--force", container], log78, { quiet: true }).catch(
57843
58311
  () => void 0
57844
58312
  );
57845
58313
  await this.exec(
57846
58314
  plan.host,
57847
58315
  "docker",
57848
58316
  ["rmi", toolchainTag(plan.repo, plan.key), layeredTag(plan.repo, plan.key)],
57849
- log77,
58317
+ log78,
57850
58318
  { quiet: true }
57851
58319
  ).catch(() => void 0);
57852
58320
  if (tmp) await plan.host.fs.rm(tmp, { recursive: true, force: true }).catch(() => void 0);
57853
58321
  try {
57854
- if (failure === null) {
58322
+ if (failure2 === null) {
57855
58323
  this.queries.finish(row.id, {
57856
58324
  status: "ready",
57857
58325
  image: plan.tag,
57858
- log: log77.toString(),
58326
+ log: log78.toString(),
57859
58327
  at: Date.now()
57860
58328
  });
57861
58329
  } else {
57862
58330
  this.queries.finish(row.id, {
57863
58331
  status: "failed",
57864
- error: failure.slice(0, 2e3),
57865
- log: log77.toString(),
58332
+ error: failure2.slice(0, 2e3),
58333
+ log: log78.toString(),
57866
58334
  at: Date.now()
57867
58335
  });
57868
58336
  }
@@ -57871,9 +58339,9 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57871
58339
  }
57872
58340
  }
57873
58341
  /** Runs a command on the builder host, adding it and its output to the log. */
57874
- async exec(host, tool, args, log77, options = {}) {
58342
+ async exec(host, tool, args, log78, options = {}) {
57875
58343
  const bin = tool === "docker" ? this.dockerBin(host) : tool === "devcontainer" ? this.devcontainerBin(host) : "git";
57876
- if (!options.quiet) log77.add(`$ ${tool} ${args.join(" ")}`);
58344
+ if (!options.quiet) log78.add(`$ ${tool} ${args.join(" ")}`);
57877
58345
  try {
57878
58346
  const out = await host.exec(bin, args, {
57879
58347
  cwd: options.cwd,
@@ -57881,35 +58349,35 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57881
58349
  timeoutMs: envNumber("BAND_ENVIRONMENT_BUILD_TIMEOUT_MS", DEFAULT_STEP_TIMEOUT_MS)
57882
58350
  });
57883
58351
  if (!options.quiet) {
57884
- if (out.stdout.trim()) log77.add(out.stdout.trimEnd());
57885
- if (out.stderr.trim()) log77.add(out.stderr.trimEnd());
57886
- log77.flush();
58352
+ if (out.stdout.trim()) log78.add(out.stdout.trimEnd());
58353
+ if (out.stderr.trim()) log78.add(out.stderr.trimEnd());
58354
+ log78.flush();
57887
58355
  }
57888
58356
  return out.stdout;
57889
58357
  } catch (err) {
57890
58358
  if (!options.quiet) {
57891
- log77.add(err.message);
57892
- log77.flush();
58359
+ log78.add(err.message);
58360
+ log78.flush();
57893
58361
  }
57894
58362
  throw new Error(`${tool} ${args[0] ?? ""} failed: ${firstLine(err.message)}`);
57895
58363
  }
57896
58364
  }
57897
58365
  /** Writes the default branch's files, with no `.git`, to `<tmp>/src`. */
57898
- async exportCheckout(plan, tmp, log77) {
58366
+ async exportCheckout(plan, tmp, log78) {
57899
58367
  const src = posix4.join(tmp, "src");
57900
58368
  await plan.host.fs.mkdir(src);
57901
58369
  const index2 = posix4.join(tmp, "index");
57902
- await this.exec(plan.host, "git", ["read-tree", plan.ref], log77, {
58370
+ await this.exec(plan.host, "git", ["read-tree", plan.ref], log78, {
57903
58371
  cwd: plan.repoPath,
57904
58372
  env: { GIT_INDEX_FILE: index2 }
57905
58373
  });
57906
- await this.exec(plan.host, "git", ["checkout-index", "--all", `--prefix=${src}/`], log77, {
58374
+ await this.exec(plan.host, "git", ["checkout-index", "--all", `--prefix=${src}/`], log78, {
57907
58375
  cwd: plan.repoPath,
57908
58376
  env: { GIT_INDEX_FILE: index2 }
57909
58377
  });
57910
58378
  }
57911
58379
  /** Layer 2: returns the tag of an image holding the repo's toolchain. */
57912
- async buildToolchain(plan, tmp, log77) {
58380
+ async buildToolchain(plan, tmp, log78) {
57913
58381
  const build = plan.environment.build;
57914
58382
  const src = posix4.join(tmp, "src");
57915
58383
  const tag = toolchainTag(plan.repo, plan.key);
@@ -57918,7 +58386,7 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57918
58386
  plan.host,
57919
58387
  "docker",
57920
58388
  dockerBuildArgs({ checkout: src, dockerfile: build.dockerfile, tag }),
57921
- log77
58389
+ log78
57922
58390
  );
57923
58391
  return tag;
57924
58392
  }
@@ -57928,7 +58396,7 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57928
58396
  plan.host,
57929
58397
  "devcontainer",
57930
58398
  devcontainerBuildArgs({ checkout: src, config: build.devcontainer, tag }),
57931
- log77
58399
+ log78
57932
58400
  );
57933
58401
  } catch (err) {
57934
58402
  throw new Error(
@@ -57938,11 +58406,11 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57938
58406
  return tag;
57939
58407
  }
57940
58408
  const image = build?.image;
57941
- await this.exec(plan.host, "docker", ["pull", image], log77);
58409
+ await this.exec(plan.host, "docker", ["pull", image], log78);
57942
58410
  return image;
57943
58411
  }
57944
58412
  /** Layer 1 on top of layer 2: copies the worker from the worker base image. */
57945
- async addWorkerLayer(plan, tmp, toolchain, log77) {
58413
+ async addWorkerLayer(plan, tmp, toolchain, log78) {
57946
58414
  const dir = posix4.join(tmp, "layer");
57947
58415
  await plan.host.fs.mkdir(dir);
57948
58416
  await plan.host.fs.writeFile(
@@ -57954,38 +58422,38 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57954
58422
  });
57955
58423
  await plan.host.fs.mkdir(posix4.join(dir, "root", "work"), { recursive: true });
57956
58424
  const tag = layeredTag(plan.repo, plan.key);
57957
- await this.exec(plan.host, "docker", ["build", "--tag", tag, dir], log77);
58425
+ await this.exec(plan.host, "docker", ["build", "--tag", tag, dir], log78);
57958
58426
  return tag;
57959
58427
  }
57960
58428
  /** Runs `install` at the default branch inside the image and commits the result as the final tag. */
57961
- async install(plan, tmp, layered, container, log77) {
58429
+ async install(plan, tmp, layered, container, log78) {
57962
58430
  const install = plan.environment.install;
57963
58431
  if (install === void 0) {
57964
- await this.exec(plan.host, "docker", ["tag", layered, plan.tag], log77);
58432
+ await this.exec(plan.host, "docker", ["tag", layered, plan.tag], log78);
57965
58433
  return;
57966
58434
  }
57967
58435
  await this.exec(
57968
58436
  plan.host,
57969
58437
  "docker",
57970
58438
  installCreateArgs({ name: container, image: layered, install }),
57971
- log77
58439
+ log78
57972
58440
  );
57973
58441
  await this.exec(
57974
58442
  plan.host,
57975
58443
  "docker",
57976
58444
  ["cp", `${posix4.join(tmp, "src")}/.`, `${container}:${IMAGE_WORKDIR}`],
57977
- log77
58445
+ log78
57978
58446
  );
57979
- await this.exec(plan.host, "docker", ["start", "--attach", container], log77);
58447
+ await this.exec(plan.host, "docker", ["start", "--attach", container], log78);
57980
58448
  await this.exec(
57981
58449
  plan.host,
57982
58450
  "docker",
57983
58451
  commitArgs({ container, tag: plan.tag, key: plan.key, commit: plan.commit }),
57984
- log77
58452
+ log78
57985
58453
  );
57986
58454
  }
57987
- async push(plan, log77) {
57988
- await this.exec(plan.host, "docker", ["push", plan.tag], log77);
58455
+ async push(plan, log78) {
58456
+ await this.exec(plan.host, "docker", ["push", plan.tag], log78);
57989
58457
  }
57990
58458
  // ---- auto trigger ----------------------------------------------------------
57991
58459
  /**
@@ -58204,8 +58672,8 @@ var init_git_credential_service = __esm({
58204
58672
  // src/server/services/runner-service.ts
58205
58673
  import { spawn as spawn6 } from "node:child_process";
58206
58674
  import { randomBytes as randomBytes10 } from "node:crypto";
58207
- import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync11 } from "node:fs";
58208
- import { dirname as dirname11, join as join39 } from "node:path";
58675
+ import { appendFileSync, mkdirSync as mkdirSync8, readFileSync as readFileSync10, rmSync as rmSync5, statSync as statSync13 } from "node:fs";
58676
+ import { dirname as dirname12, join as join39 } from "node:path";
58209
58677
  function snapshotSweepMs() {
58210
58678
  const raw = Number(process.env.BAND_SNAPSHOT_SWEEP_MS);
58211
58679
  return Number.isFinite(raw) && raw > 0 ? raw : SNAPSHOT_SWEEP_MS;
@@ -58294,9 +58762,9 @@ var init_runner_service = __esm({
58294
58762
  "SHELL"
58295
58763
  ];
58296
58764
  RunLog = class {
58297
- constructor(file2) {
58298
- this.file = file2;
58299
- mkdirSync7(dirname11(file2), { recursive: true, mode: 448 });
58765
+ constructor(file3) {
58766
+ this.file = file3;
58767
+ mkdirSync8(dirname12(file3), { recursive: true, mode: 448 });
58300
58768
  }
58301
58769
  lines = [];
58302
58770
  bytes = 0;
@@ -58721,7 +59189,7 @@ ${tail}` : head;
58721
59189
  () => false
58722
59190
  );
58723
59191
  if (exists3) return;
58724
- await host.git.exec(["clone", "--quiet", "--", url2, dest], dirname11(dest));
59192
+ await host.git.exec(["clone", "--quiet", "--", url2, dest], dirname12(dest));
58725
59193
  }
58726
59194
  /** Records the machine of an attempt. An older machine of the same worker id is gone by now, so it is retired. */
58727
59195
  recordMachine(runner, row, workerId) {
@@ -58796,7 +59264,7 @@ ${tail}` : head;
58796
59264
  } else if (!runner.destroy) {
58797
59265
  outcome = { state: "lost", error: `runner "${runner.id}" has no destroy hook` };
58798
59266
  } else {
58799
- let failure = null;
59267
+ let failure2 = null;
58800
59268
  try {
58801
59269
  const code = await this.runDestroy(
58802
59270
  runner,
@@ -58805,17 +59273,17 @@ ${tail}` : head;
58805
59273
  machine.handle,
58806
59274
  runLog
58807
59275
  );
58808
- if (code !== 0) failure = `destroy exited with code ${code}`;
59276
+ if (code !== 0) failure2 = `destroy exited with code ${code}`;
58809
59277
  } catch (err) {
58810
- failure = `destroy failed: ${err instanceof Error ? err.message : err}`;
59278
+ failure2 = `destroy failed: ${err instanceof Error ? err.message : err}`;
58811
59279
  }
58812
- if (failure === null) {
59280
+ if (failure2 === null) {
58813
59281
  outcome = { state: "destroyed", destroyedAt: Date.now(), error: reason };
58814
59282
  } else {
58815
59283
  const attempts = machine.destroyAttempts + 1;
58816
- outcome = attempts >= DESTROY_ATTEMPTS ? { state: "lost", destroyAttempts: attempts, error: failure } : {
59284
+ outcome = attempts >= DESTROY_ATTEMPTS ? { state: "lost", destroyAttempts: attempts, error: failure2 } : {
58817
59285
  destroyAttempts: attempts,
58818
- error: `${failure} (try ${attempts} of ${DESTROY_ATTEMPTS})`
59286
+ error: `${failure2} (try ${attempts} of ${DESTROY_ATTEMPTS})`
58819
59287
  };
58820
59288
  }
58821
59289
  }
@@ -58829,12 +59297,12 @@ ${tail}` : head;
58829
59297
  /** Runs `destroy` for a machine the hub has no record of, named only by the handle `status` printed. */
58830
59298
  async destroyHandle(runner, handle, reason) {
58831
59299
  if (!runner.destroy) return false;
58832
- const file2 = join39(bandHome(), "runners", "logs", "reaper.log");
59300
+ const file3 = join39(bandHome(), "runners", "logs", "reaper.log");
58833
59301
  try {
58834
- if (statSync11(file2).size > LOG_FILE_LIMIT) rmSync4(file2, { force: true });
59302
+ if (statSync13(file3).size > LOG_FILE_LIMIT) rmSync5(file3, { force: true });
58835
59303
  } catch {
58836
59304
  }
58837
- const runLog = new RunLog(file2);
59305
+ const runLog = new RunLog(file3);
58838
59306
  runLog.write("hub", `destroying machine ${handle} of runner ${runner.id}: ${reason}`);
58839
59307
  try {
58840
59308
  const code = await this.runDestroy(runner, null, null, handle, runLog);
@@ -59078,7 +59546,7 @@ ${tail}` : head;
59078
59546
  runHook(opts) {
59079
59547
  const { runner, name: name4, path: path2, env, timeoutMs, runLog, watch, onStdout, onStderr } = opts;
59080
59548
  const cwd = join39(bandHome(), "runners", runner.id);
59081
- mkdirSync7(cwd, { recursive: true, mode: 448 });
59549
+ mkdirSync8(cwd, { recursive: true, mode: 448 });
59082
59550
  return new Promise((resolve15, reject) => {
59083
59551
  let child;
59084
59552
  try {
@@ -59136,7 +59604,7 @@ ${tail}` : head;
59136
59604
  });
59137
59605
 
59138
59606
  // src/server/services/ephemeral-lifecycle-service.ts
59139
- import { rmSync as rmSync5 } from "node:fs";
59607
+ import { rmSync as rmSync6 } from "node:fs";
59140
59608
  import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
59141
59609
  import { join as join40, posix as posix5 } from "node:path";
59142
59610
  function idleTimeoutMs() {
@@ -59146,7 +59614,7 @@ function idleTimeoutMs() {
59146
59614
  function sleepDir(worktreeId4) {
59147
59615
  return join40(bandHome(), "sleep", worktreeId4);
59148
59616
  }
59149
- var log23, LOCAL_HOST_ID5, WAKE_POLL_MS, WAKE_GRACE_MS, EXIT_GRACE_MS, SleepError, errorText2, EphemeralLifecycleService, ephemeralLifecycleService;
59617
+ var log23, LOCAL_HOST_ID6, WAKE_POLL_MS, WAKE_GRACE_MS, EXIT_GRACE_MS, SleepError, errorText2, EphemeralLifecycleService, ephemeralLifecycleService;
59150
59618
  var init_ephemeral_lifecycle_service = __esm({
59151
59619
  "src/server/services/ephemeral-lifecycle-service.ts"() {
59152
59620
  "use strict";
@@ -59168,7 +59636,7 @@ var init_ephemeral_lifecycle_service = __esm({
59168
59636
  init_state();
59169
59637
  init_task_service();
59170
59638
  log23 = createLogger("ephemeral-lifecycle");
59171
- LOCAL_HOST_ID5 = "local";
59639
+ LOCAL_HOST_ID6 = "local";
59172
59640
  WAKE_POLL_MS = 500;
59173
59641
  WAKE_GRACE_MS = 5 * 6e4;
59174
59642
  EXIT_GRACE_MS = 6e4;
@@ -59499,7 +59967,7 @@ var init_ephemeral_lifecycle_service = __esm({
59499
59967
  }
59500
59968
  forget(row) {
59501
59969
  this.sleeps.delete(row.worktreeId);
59502
- rmSync5(sleepDir(row.worktreeId), { recursive: true, force: true });
59970
+ rmSync6(sleepDir(row.worktreeId), { recursive: true, force: true });
59503
59971
  }
59504
59972
  // ---- wake ---------------------------------------------------------------
59505
59973
  /**
@@ -59511,7 +59979,7 @@ var init_ephemeral_lifecycle_service = __esm({
59511
59979
  async ensureAwake(worktreeId4) {
59512
59980
  for (let i = 0; i < 3; i++) {
59513
59981
  const hostId2 = this.worktrees.findHostId(worktreeId4);
59514
- if (!hostId2 || hostId2 === LOCAL_HOST_ID5) return;
59982
+ if (!hostId2 || hostId2 === LOCAL_HOST_ID6) return;
59515
59983
  const draining = this.draining.get(hostId2);
59516
59984
  if (draining) {
59517
59985
  await draining;
@@ -59680,10 +60148,10 @@ var init_ephemeral_lifecycle_service = __esm({
59680
60148
  }
59681
60149
  const stage = posix5.join(wipDir, "sessions", row.worktreeId);
59682
60150
  await host.fs.rm(stage, { recursive: true, force: true });
59683
- for (const file2 of files) {
59684
- const target = posix5.join(stage, file2.root, file2.rel);
60151
+ for (const file3 of files) {
60152
+ const target = posix5.join(stage, file3.root, file3.rel);
59685
60153
  await host.fs.mkdir(posix5.dirname(target), { recursive: true });
59686
- await host.fs.writeFile(target, Buffer.from(file2.data, "base64"));
60154
+ await host.fs.writeFile(target, Buffer.from(file3.data, "base64"));
59687
60155
  }
59688
60156
  await rpc.call(METHOD_LIFECYCLE_IMPORT_SESSIONS, { dir: stage });
59689
60157
  }
@@ -60843,6 +61311,8 @@ var init_client_state_keys = __esm({
60843
61311
  exact("band.repos-list.collapsed-pinned", [jsonPart("all")]),
60844
61312
  exact("band.repos-list.label-filter", [rawPart("all")]),
60845
61313
  exact("band.repos-list.label-last-worktree", [jsonPart("all")]),
61314
+ // The hooks version whose install banner the user dismissed.
61315
+ exact("band:agent-hooks-banner-dismissed", [rawPart("all")]),
60846
61316
  perWorktree(CENTER_TABS_PREFIX, [jsonPart("all")]),
60847
61317
  perWorktreeLeaf("band:term-split:", [jsonPart("all")]),
60848
61318
  perWorktree("band:diff-compare-branch:", [rawPart("all")]),
@@ -61069,11 +61539,11 @@ var init_cronjobs = __esm({
61069
61539
  * incomplete state (some jobs missing) — the legacy `cronjob-store.ts`
61070
61540
  * had the same gap; the migration is the appropriate moment to fix it.
61071
61541
  */
61072
- saveFile(key2, file2) {
61542
+ saveFile(key2, file3) {
61073
61543
  const db = getDb();
61074
61544
  db.transaction((tx) => {
61075
61545
  tx.delete(cronjobs).where(eq(cronjobs.fileKey, key2)).run();
61076
- for (const job of file2.jobs) {
61546
+ for (const job of file3.jobs) {
61077
61547
  tx.insert(cronjobs).values({
61078
61548
  id: job.id,
61079
61549
  fileKey: key2,
@@ -61265,14 +61735,14 @@ var init_cronjob_service = __esm({
61265
61735
  list(filter) {
61266
61736
  if (filter?.repo) {
61267
61737
  const { repo } = filter;
61268
- const file2 = this.queries.loadFile(repo);
61269
- return { jobs: file2.jobs.map((j) => ({ ...j, fileKey: repo })) };
61738
+ const file3 = this.queries.loadFile(repo);
61739
+ return { jobs: file3.jobs.map((j) => ({ ...j, fileKey: repo })) };
61270
61740
  }
61271
61741
  if (filter?.worktreeId) {
61272
61742
  const { worktreeId: worktreeId4 } = filter;
61273
- const file2 = this.queries.loadFile(worktreeId4);
61743
+ const file3 = this.queries.loadFile(worktreeId4);
61274
61744
  return {
61275
- jobs: file2.jobs.map((j) => ({ ...j, fileKey: worktreeId4 }))
61745
+ jobs: file3.jobs.map((j) => ({ ...j, fileKey: worktreeId4 }))
61276
61746
  };
61277
61747
  }
61278
61748
  return { jobs: this.queries.listAll() };
@@ -61300,7 +61770,7 @@ var init_cronjob_service = __esm({
61300
61770
  if (input.scope === "worktree" && !input.worktreeId) {
61301
61771
  throw new CronjobWorktreeMissingError();
61302
61772
  }
61303
- const file2 = this.queries.loadFile(input.key);
61773
+ const file3 = this.queries.loadFile(input.key);
61304
61774
  const job = {
61305
61775
  id: generateCronjobId(),
61306
61776
  name: input.name,
@@ -61314,8 +61784,8 @@ var init_cronjob_service = __esm({
61314
61784
  enabled: input.enabled,
61315
61785
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
61316
61786
  };
61317
- file2.jobs.push(job);
61318
- this.queries.saveFile(input.key, file2);
61787
+ file3.jobs.push(job);
61788
+ this.queries.saveFile(input.key, file3);
61319
61789
  this.reloadSchedules();
61320
61790
  return { job };
61321
61791
  }
@@ -61330,14 +61800,14 @@ var init_cronjob_service = __esm({
61330
61800
  if (input.cronExpression !== void 0) {
61331
61801
  this.assertValidCron(input.cronExpression);
61332
61802
  }
61333
- const file2 = this.queries.loadFile(input.key);
61334
- const job = file2.jobs.find((j) => j.id === input.id);
61803
+ const file3 = this.queries.loadFile(input.key);
61804
+ const job = file3.jobs.find((j) => j.id === input.id);
61335
61805
  if (!job) throw new CronjobNotFoundError();
61336
61806
  if (input.name !== void 0) job.name = input.name;
61337
61807
  if (input.prompt !== void 0) job.prompt = input.prompt;
61338
61808
  if (input.cronExpression !== void 0) job.cronExpression = input.cronExpression;
61339
61809
  if (input.enabled !== void 0) job.enabled = input.enabled;
61340
- this.queries.saveFile(input.key, file2);
61810
+ this.queries.saveFile(input.key, file3);
61341
61811
  this.reloadSchedules();
61342
61812
  return { job };
61343
61813
  }
@@ -61347,11 +61817,11 @@ var init_cronjob_service = __esm({
61347
61817
  * Throws `CronjobNotFoundError` if the job is already gone.
61348
61818
  */
61349
61819
  delete(key2, id) {
61350
- const file2 = this.queries.loadFile(key2);
61351
- const index2 = file2.jobs.findIndex((j) => j.id === id);
61820
+ const file3 = this.queries.loadFile(key2);
61821
+ const index2 = file3.jobs.findIndex((j) => j.id === id);
61352
61822
  if (index2 === -1) throw new CronjobNotFoundError();
61353
- const [removed] = file2.jobs.splice(index2, 1);
61354
- this.queries.saveFile(key2, file2);
61823
+ const [removed] = file3.jobs.splice(index2, 1);
61824
+ this.queries.saveFile(key2, file3);
61355
61825
  if (removed?.lastTerminalId) {
61356
61826
  const terminalId = removed.lastTerminalId;
61357
61827
  void terminalService.kill(terminalId).catch((err) => {
@@ -61469,8 +61939,8 @@ var init_cronjob_service = __esm({
61469
61939
  */
61470
61940
  stopJobsForKey(key2) {
61471
61941
  const state2 = schedulerState();
61472
- const file2 = this.queries.loadFile(key2);
61473
- for (const job of file2.jobs) {
61942
+ const file3 = this.queries.loadFile(key2);
61943
+ for (const job of file3.jobs) {
61474
61944
  const cron = state2.jobs.get(job.id);
61475
61945
  if (cron) {
61476
61946
  cron.stop();
@@ -61621,8 +62091,8 @@ exit`;
61621
62091
  // Internals
61622
62092
  // -------------------------------------------------------------------------
61623
62093
  findJob(key2, id) {
61624
- const file2 = this.queries.loadFile(key2);
61625
- return file2.jobs.find((j) => j.id === id);
62094
+ const file3 = this.queries.loadFile(key2);
62095
+ return file3.jobs.find((j) => j.id === id);
61626
62096
  }
61627
62097
  assertValidCron(expression) {
61628
62098
  try {
@@ -61744,28 +62214,28 @@ exit`;
61744
62214
  // src/server/services/local-host-policy.ts
61745
62215
  function resolveWorktreeHostId(requested) {
61746
62216
  if (!isLocalHostEnabled()) {
61747
- if (requested === LOCAL_HOST_ID6) throw new LocalHostDisabledError();
62217
+ if (requested === LOCAL_HOST_ID7) throw new LocalHostDisabledError();
61748
62218
  if (requested) return requested;
61749
62219
  const configured = process.env.BAND_DEFAULT_HOST?.trim();
61750
62220
  if (configured) {
61751
- if (configured === LOCAL_HOST_ID6) throw new LocalHostDisabledError();
62221
+ if (configured === LOCAL_HOST_ID7) throw new LocalHostDisabledError();
61752
62222
  return configured;
61753
62223
  }
61754
- const online = tokenService.listHosts().filter((h) => h.id !== LOCAL_HOST_ID6 && h.status === "online");
62224
+ const online = tokenService.listHosts().filter((h) => h.id !== LOCAL_HOST_ID7 && h.status === "online");
61755
62225
  if (online.length === 1) return online[0].id;
61756
62226
  throw new NoDefaultHostError(
61757
62227
  online.length === 0 ? "No worker is online." : "More than one worker is online."
61758
62228
  );
61759
62229
  }
61760
- return requested ?? LOCAL_HOST_ID6;
62230
+ return requested ?? LOCAL_HOST_ID7;
61761
62231
  }
61762
- var LOCAL_HOST_ID6, LocalHostDisabledError, NoDefaultHostError;
62232
+ var LOCAL_HOST_ID7, LocalHostDisabledError, NoDefaultHostError;
61763
62233
  var init_local_host_policy = __esm({
61764
62234
  "src/server/services/local-host-policy.ts"() {
61765
62235
  "use strict";
61766
62236
  init_local_host_enabled();
61767
62237
  init_token_service();
61768
- LOCAL_HOST_ID6 = "local";
62238
+ LOCAL_HOST_ID7 = "local";
61769
62239
  LocalHostDisabledError = class extends Error {
61770
62240
  constructor() {
61771
62241
  super(
@@ -61964,7 +62434,7 @@ async function reconcileOneRepo(repo) {
61964
62434
  repo.worktrees.filter((wt) => !isRemote(wt)).map((wt) => [wt.path, wt])
61965
62435
  );
61966
62436
  diskWorktrees = gitWorktrees.filter(
61967
- (wt) => !wt.isBare && !remotePaths.has(wt.path) && (!removingWorktrees.has(wt.path) || existingByPath.has(wt.path) && !removedWorktrees.has(wt.path))
62437
+ (wt) => !wt.isBare && (!remotePaths.has(wt.path) || existingByPath.has(wt.path)) && (!removingWorktrees.has(wt.path) || existingByPath.has(wt.path) && !removedWorktrees.has(wt.path))
61968
62438
  ).map((wt) => {
61969
62439
  const existing = existingByPath.get(wt.path);
61970
62440
  return {
@@ -62225,6 +62695,7 @@ var init_worktree_script_service = __esm({
62225
62695
  // src/server/services/worktree-service.ts
62226
62696
  var worktree_service_exports = {};
62227
62697
  __export(worktree_service_exports, {
62698
+ InvalidOriginError: () => InvalidOriginError,
62228
62699
  PlainRepoError: () => PlainRepoError,
62229
62700
  RepoNotFoundError: () => RepoNotFoundError,
62230
62701
  WorktreeNotFoundError: () => WorktreeNotFoundError,
@@ -62236,11 +62707,12 @@ __export(worktree_service_exports, {
62236
62707
  worktreeRemoveInput: () => worktreeRemoveInput,
62237
62708
  worktreeRunScriptInput: () => worktreeRunScriptInput,
62238
62709
  worktreeService: () => worktreeService,
62710
+ worktreeSetOriginInput: () => worktreeSetOriginInput,
62239
62711
  worktreeSetPinnedInput: () => worktreeSetPinnedInput,
62240
62712
  worktreeVia: () => worktreeVia
62241
62713
  });
62242
62714
  import { randomUUID as randomUUID10 } from "node:crypto";
62243
- import { unlinkSync as unlinkSync4 } from "node:fs";
62715
+ import { unlinkSync as unlinkSync5 } from "node:fs";
62244
62716
  import { join as join41, posix as posix6 } from "node:path";
62245
62717
  function findWorktreeRow(worktrees2, name4, hostId2) {
62246
62718
  const named = worktrees2.filter((wt) => wt.name === name4);
@@ -62282,7 +62754,7 @@ function expandHome(given, home, hostId2) {
62282
62754
  }
62283
62755
  return path2;
62284
62756
  }
62285
- var TEARDOWN_TIMEOUT_MS, log33, worktreeVia, worktreeCreateInput, worktreeRemoveInput, worktreeSetPinnedInput, worktreeGitInput, worktreeRunScriptInput, RepoNotFoundError, PlainRepoError, WorktreeService, worktreeService;
62757
+ var TEARDOWN_TIMEOUT_MS, log33, worktreeVia, worktreeCreateInput, worktreeRemoveInput, worktreeSetPinnedInput, worktreeSetOriginInput, worktreeGitInput, worktreeRunScriptInput, RepoNotFoundError, InvalidOriginError, PlainRepoError, WorktreeService, worktreeService;
62286
62758
  var init_worktree_service = __esm({
62287
62759
  "src/server/services/worktree-service.ts"() {
62288
62760
  "use strict";
@@ -62383,6 +62855,14 @@ var init_worktree_service = __esm({
62383
62855
  hostId: external_exports2.string().optional(),
62384
62856
  pinned: external_exports2.boolean()
62385
62857
  });
62858
+ worktreeSetOriginInput = external_exports2.object({
62859
+ repo: external_exports2.string(),
62860
+ // Worktree identity (immutable `name`), not the live git branch.
62861
+ name: external_exports2.string(),
62862
+ hostId: external_exports2.string().optional(),
62863
+ // The parent worktree's id, or null to remove the parent.
62864
+ origin: external_exports2.string().min(1).nullable()
62865
+ });
62386
62866
  worktreeGitInput = external_exports2.object({
62387
62867
  repo: external_exports2.string(),
62388
62868
  // Worktree identity (immutable `name`), not the live git branch.
@@ -62399,6 +62879,12 @@ var init_worktree_service = __esm({
62399
62879
  this.name = "RepoNotFoundError";
62400
62880
  }
62401
62881
  };
62882
+ InvalidOriginError = class extends Error {
62883
+ constructor(message) {
62884
+ super(message);
62885
+ this.name = "InvalidOriginError";
62886
+ }
62887
+ };
62402
62888
  PlainRepoError = class extends Error {
62403
62889
  constructor(message) {
62404
62890
  super(message);
@@ -62913,7 +63399,7 @@ var init_worktree_service = __esm({
62913
63399
  saveState(state2);
62914
63400
  removal.commit();
62915
63401
  try {
62916
- unlinkSync4(join41(bandHome(), "workspace-prompts", `${worktreeId4}.json`));
63402
+ unlinkSync5(join41(bandHome(), "workspace-prompts", `${worktreeId4}.json`));
62917
63403
  } catch {
62918
63404
  }
62919
63405
  deleteWorktreeStatus(worktreeId4);
@@ -63075,6 +63561,52 @@ var init_worktree_service = __esm({
63075
63561
  saveState(state2);
63076
63562
  return { ok: true };
63077
63563
  }
63564
+ /**
63565
+ * Sets a worktree's parent (its origin) by hand, or removes it with `null`. A manual parent
63566
+ * carries no chat or terminal. Rejects the worktree itself and any descendant of it, since
63567
+ * either would make a cycle.
63568
+ */
63569
+ setOrigin(input) {
63570
+ const state2 = loadState();
63571
+ const repo = state2.repos.find((p2) => p2.name === input.repo);
63572
+ if (!repo) throw new RepoNotFoundError(input.repo);
63573
+ const target = findWorktreeRow(repo.worktrees, input.name, input.hostId);
63574
+ if (!target) throw new WorktreeNotFoundError(input.name);
63575
+ const targetHostId = target.hostId ?? "local";
63576
+ const worktreeId4 = toWorktreeId(repo.name, target.name, target.hostId);
63577
+ if (input.origin !== null) {
63578
+ const origins = /* @__PURE__ */ new Map();
63579
+ for (const r of state2.repos) {
63580
+ for (const wt of r.worktrees) {
63581
+ origins.set(toWorktreeId(r.name, wt.name, wt.hostId), wt.origin?.worktreeId);
63582
+ }
63583
+ }
63584
+ if (!origins.has(input.origin)) {
63585
+ throw new InvalidOriginError(`Unknown origin worktree: ${input.origin}`);
63586
+ }
63587
+ if (input.origin === worktreeId4) {
63588
+ throw new InvalidOriginError("A worktree cannot be its own parent.");
63589
+ }
63590
+ const seen = /* @__PURE__ */ new Set();
63591
+ for (let id = input.origin; id && !seen.has(id); ) {
63592
+ if (id === worktreeId4) {
63593
+ throw new InvalidOriginError(
63594
+ "That worktree is a descendant of this one, so it cannot be its parent."
63595
+ );
63596
+ }
63597
+ seen.add(id);
63598
+ id = origins.get(id);
63599
+ }
63600
+ }
63601
+ this.repoQueries.setOrigin(
63602
+ repo.name,
63603
+ target.name,
63604
+ input.origin === null ? null : { worktreeId: input.origin },
63605
+ targetHostId
63606
+ );
63607
+ emit({ kind: "worktree-origin-changed", worktreeId: worktreeId4 });
63608
+ return { ok: true };
63609
+ }
63078
63610
  pruning = /* @__PURE__ */ new Set();
63079
63611
  /**
63080
63612
  * Removes the records of worktrees whose folder is gone from their host (`missing` after a scan),
@@ -63375,8 +63907,8 @@ var init_worktree_service = __esm({
63375
63907
  throw new Error(`Script "${input.scriptType}" not found`);
63376
63908
  }
63377
63909
  try {
63378
- const { file: file2, args } = scriptInvocation(scriptPath);
63379
- await host.exec(file2, args, { cwd: input.path });
63910
+ const { file: file3, args } = scriptInvocation(scriptPath);
63911
+ await host.exec(file3, args, { cwd: input.path });
63380
63912
  } catch (err) {
63381
63913
  throw new Error(err instanceof Error ? err.message : String(err));
63382
63914
  }
@@ -63388,15 +63920,6 @@ var init_worktree_service = __esm({
63388
63920
  });
63389
63921
 
63390
63922
  // src/server/services/task-service.ts
63391
- function observePending() {
63392
- if (observingPending) return;
63393
- observingPending = true;
63394
- agentSessionService.observePending((chatId, worktreeId4) => {
63395
- if (tasks2.get(chatId)?.status !== "running") return;
63396
- const waiting = agentSessionService.hasPendingRequest(chatId);
63397
- setScopeStatus(worktreeId4, chatId, waiting ? "needs_attention" : "working");
63398
- });
63399
- }
63400
63923
  function persistTask(task) {
63401
63924
  const worktree = worktreeService.resolve(task.worktreeId);
63402
63925
  try {
@@ -63429,19 +63952,19 @@ function fileUri(path2) {
63429
63952
  async function promptBlocks(task, firstTurn, sharedDir) {
63430
63953
  const caps = agentSessionService.promptCapabilities(task.chatId);
63431
63954
  const blocks = [{ type: "text", text: task.prompt }];
63432
- for (const file2 of task.attachments) {
63433
- const name4 = file2.filename ?? file2.path.split("/").pop() ?? file2.path;
63434
- if (caps.image && file2.mediaType.startsWith("image/")) {
63955
+ for (const file3 of task.attachments) {
63956
+ const name4 = file3.filename ?? file3.path.split("/").pop() ?? file3.path;
63957
+ if (caps.image && file3.mediaType.startsWith("image/")) {
63435
63958
  try {
63436
63959
  const host = hostRegistry.hostFor(task.worktreeId);
63437
- const data = Buffer.from(await host.fs.readFile(file2.path)).toString("base64");
63438
- blocks.push({ type: "image", mimeType: file2.mediaType, data, uri: fileUri(file2.path) });
63960
+ const data = Buffer.from(await host.fs.readFile(file3.path)).toString("base64");
63961
+ blocks.push({ type: "image", mimeType: file3.mediaType, data, uri: fileUri(file3.path) });
63439
63962
  continue;
63440
63963
  } catch (err) {
63441
- log34.warn({ path: file2.path, err }, "could not read image attachment; linking it instead");
63964
+ log34.warn({ path: file3.path, err }, "could not read image attachment; linking it instead");
63442
63965
  }
63443
63966
  }
63444
- blocks.push({ type: "resource_link", uri: fileUri(file2.path), name: name4, mimeType: file2.mediaType });
63967
+ blocks.push({ type: "resource_link", uri: fileUri(file3.path), name: name4, mimeType: file3.mediaType });
63445
63968
  }
63446
63969
  if (firstTurn) {
63447
63970
  blocks.push({
@@ -63486,10 +64009,6 @@ function turnUsage(chatId, res, previousCost) {
63486
64009
  });
63487
64010
  return { ...usage, model, costUsd: (previousCost ?? 0) + cost };
63488
64011
  }
63489
- function setScopeStatus(scope3, chatId, status) {
63490
- const updated = setWorktreeSourceStatus(scope3, chatStatusSource(chatId), { status });
63491
- emit({ kind: "update", status: updated });
63492
- }
63493
64012
  function submitTask(options) {
63494
64013
  const { worktreeId: worktreeId4, chatId, prompt, sessionId, mode, model, codingAgentId } = options;
63495
64014
  if (worktreeService.resolve(worktreeId4) === null) {
@@ -63499,7 +64018,7 @@ function submitTask(options) {
63499
64018
  if (existing?.status === "running") {
63500
64019
  throw new TaskConflictError(chatId);
63501
64020
  }
63502
- observePending();
64021
+ observeAgentStatus(hasRunningTask);
63503
64022
  const task = {
63504
64023
  id: generateTaskId(),
63505
64024
  worktreeId: worktreeId4,
@@ -63543,7 +64062,7 @@ function submitOrQueueTask(options) {
63543
64062
  async function runTask(task) {
63544
64063
  const { chatId } = task;
63545
64064
  chatService.updateStatus(chatId, "running");
63546
- setScopeStatus(task.worktreeId, chatId, "working");
64065
+ beginBandTurn(task.worktreeId, chatId);
63547
64066
  const chat = chatService.get(chatId);
63548
64067
  if (task.codingAgentId && chat && task.codingAgentId !== chat.agent) {
63549
64068
  agentSessionService.stop(chatId);
@@ -63643,7 +64162,7 @@ async function runTask(task) {
63643
64162
  durationMs: Date.now() - task.startedAt,
63644
64163
  usage: turnUsage(chatId, res, previousCost)
63645
64164
  });
63646
- finishTask(task, res.stopReason === "cancelled" ? "cancelled" : "completed");
64165
+ finishTask(task, res.stopReason === "cancelled" ? "cancelled" : "completed", res.stopReason);
63647
64166
  } catch (err) {
63648
64167
  const message = err instanceof Error ? err.message : String(err);
63649
64168
  agentSessionService.record(chatId, {
@@ -63667,7 +64186,7 @@ async function applyTurnChoice(chatId, category, value) {
63667
64186
  log34.warn({ chatId, category, value, err }, "could not apply the task's setting");
63668
64187
  }
63669
64188
  }
63670
- function finishTask(task, outcome) {
64189
+ function finishTask(task, outcome, stopReason) {
63671
64190
  if (task.status !== "running") return;
63672
64191
  const status = outcome === "completed" ? "completed" : "failed";
63673
64192
  task.status = status;
@@ -63678,7 +64197,8 @@ function finishTask(task, outcome) {
63678
64197
  if (status === "completed" && drainQueue(task.worktreeId, task.chatId)) return;
63679
64198
  chatService.updateStatus(task.chatId, status === "completed" ? "idle" : "error");
63680
64199
  const stopped = outcome === "cancelled" || task.cancelRequested === true;
63681
- setScopeStatus(task.worktreeId, task.chatId, stopped ? "waiting" : "needs_attention");
64200
+ const failed = outcome === "failed" || stopReason !== void 0 && FAILED_STOP_REASONS.has(stopReason);
64201
+ endBandTurn(task.worktreeId, task.chatId, stopped ? "idle" : failed ? "failed" : "done");
63682
64202
  }
63683
64203
  function drainQueue(worktreeId4, chatId) {
63684
64204
  const queued = peekQueuedMessage(chatId);
@@ -63721,7 +64241,7 @@ function cancelTask(taskId) {
63721
64241
  const record3 = taskQueries.markFailed(taskId);
63722
64242
  if (record3) {
63723
64243
  if (record3.chatId) {
63724
- setScopeStatus(record3.worktreeId, record3.chatId, "waiting");
64244
+ setChatStatus(record3.worktreeId, record3.chatId, "idle");
63725
64245
  }
63726
64246
  log34.info({ taskId, worktreeId: record3.worktreeId }, "orphaned task cancelled");
63727
64247
  return { cancelled: true, worktreeId: record3.worktreeId };
@@ -63735,7 +64255,7 @@ function getTask(chatId) {
63735
64255
  const task = tasks2.get(chatId);
63736
64256
  return task ? toTaskInfo(task) : null;
63737
64257
  }
63738
- var log34, taskQueries, TASKS_KEY, g4, tasks2, observingPending, TaskConflictError, TaskService, taskService;
64258
+ var log34, taskQueries, TASKS_KEY, g4, tasks2, TaskConflictError, FAILED_STOP_REASONS, TaskService, taskService;
63739
64259
  var init_task_service = __esm({
63740
64260
  "src/server/services/task-service.ts"() {
63741
64261
  "use strict";
@@ -63750,8 +64270,8 @@ var init_task_service = __esm({
63750
64270
  init_turn_events();
63751
64271
  init_agent_session_service();
63752
64272
  init_chat_service();
64273
+ init_chat_status_service();
63753
64274
  init_state();
63754
- init_watcher_service();
63755
64275
  init_worktree_service();
63756
64276
  log34 = createLogger("task-service");
63757
64277
  taskQueries = new TaskQueries();
@@ -63759,13 +64279,13 @@ var init_task_service = __esm({
63759
64279
  g4 = globalThis;
63760
64280
  if (!g4[TASKS_KEY]) g4[TASKS_KEY] = /* @__PURE__ */ new Map();
63761
64281
  tasks2 = g4[TASKS_KEY];
63762
- observingPending = false;
63763
64282
  TaskConflictError = class extends Error {
63764
64283
  constructor(chatId) {
63765
64284
  super(`Task already running for chat ${chatId}`);
63766
64285
  this.name = "TaskConflictError";
63767
64286
  }
63768
64287
  };
64288
+ FAILED_STOP_REASONS = /* @__PURE__ */ new Set(["refusal", "max_tokens", "max_turn_requests"]);
63769
64289
  TaskService = class {
63770
64290
  constructor(queries3 = taskQueries) {
63771
64291
  this.queries = queries3;
@@ -63785,13 +64305,10 @@ var init_task_service = __esm({
63785
64305
  getTask(chatId) {
63786
64306
  return getTask(chatId);
63787
64307
  }
63788
- /** Clears the worktree's attention status, except for chats whose agent
63789
- * still waits on the user (a permission or elicitation request). */
64308
+ /** Sets the worktree's finished chats (`done`, `failed`) back to `idle`. A
64309
+ * chat whose agent still waits on the user keeps its `question`. */
63790
64310
  acknowledgeAttention(worktreeId4) {
63791
- return acknowledgeWorktreeAttention(
63792
- worktreeId4,
63793
- agentSessionService.chatsWithPendingRequest(worktreeId4)
63794
- );
64311
+ return acknowledgeWorktreeAttention(worktreeId4);
63795
64312
  }
63796
64313
  listTaskRecords(filters) {
63797
64314
  return this.queries.list(filters);
@@ -65365,9 +65882,88 @@ function parseReview(data, branch, defaultBranch) {
65365
65882
  updatedAt: pr.updatedAt,
65366
65883
  reviewDecision: reviewDecision(pr.reviewDecision),
65367
65884
  mergeState: mergeState(pr.mergeStateStatus),
65368
- checks: report(head?.oid ?? null, checks)
65885
+ checks: report(head?.oid ?? null, checks),
65886
+ reviewers: parseReviewers(pr),
65887
+ discussion: parseDiscussion(pr)
65369
65888
  };
65370
65889
  }
65890
+ function parseReviewers(pr) {
65891
+ const byLogin = /* @__PURE__ */ new Map();
65892
+ const order = [...pr.reviews?.nodes ?? []].sort(
65893
+ (a, b2) => (a.submittedAt ?? "").localeCompare(b2.submittedAt ?? "")
65894
+ );
65895
+ for (const review of order) {
65896
+ const state2 = REVIEWER_STATES[review.state];
65897
+ if (!state2 || !review.author) continue;
65898
+ const existing = byLogin.get(review.author.login);
65899
+ if (existing && state2 === "commented" && existing.state !== "commented") continue;
65900
+ byLogin.set(review.author.login, {
65901
+ ...actorOf(review.author),
65902
+ state: state2,
65903
+ submittedAt: review.submittedAt
65904
+ });
65905
+ }
65906
+ for (const request of pr.reviewRequests?.nodes ?? []) {
65907
+ const who = request.requestedReviewer;
65908
+ if (!who) continue;
65909
+ const actor = who.__typename === "Team" ? { login: who.slug, avatarUrl: null } : actorOf({ login: who.login, avatarUrl: who.avatarUrl });
65910
+ const key2 = who.__typename === "Team" ? `team:${actor.login}` : actor.login;
65911
+ const previous = byLogin.get(key2);
65912
+ byLogin.set(key2, {
65913
+ ...actor,
65914
+ state: "pending",
65915
+ submittedAt: previous?.submittedAt ?? null,
65916
+ ...request.asCodeOwner ? { required: true } : {}
65917
+ });
65918
+ }
65919
+ return [...byLogin.values()];
65920
+ }
65921
+ function fromCommentNode(node) {
65922
+ return {
65923
+ id: node.id,
65924
+ author: actorOf(node.author),
65925
+ body: node.body,
65926
+ createdAt: node.createdAt,
65927
+ url: webUrl(node.url)
65928
+ };
65929
+ }
65930
+ function parseDiscussion(pr) {
65931
+ const items = [];
65932
+ for (const node of pr.comments?.nodes ?? []) {
65933
+ items.push({ ...fromCommentNode(node), kind: "comment" });
65934
+ }
65935
+ for (const review of pr.reviews?.nodes ?? []) {
65936
+ const state2 = REVIEWER_STATES[review.state];
65937
+ if (!state2 || !review.body.trim() || !review.submittedAt) continue;
65938
+ items.push({
65939
+ id: review.id,
65940
+ kind: "review",
65941
+ reviewState: state2,
65942
+ author: actorOf(review.author),
65943
+ body: review.body,
65944
+ createdAt: review.submittedAt,
65945
+ url: webUrl(review.url)
65946
+ });
65947
+ }
65948
+ items.sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
65949
+ const threads = [];
65950
+ for (const node of pr.reviewThreads?.nodes ?? []) {
65951
+ const comments = node.comments.nodes.map(fromCommentNode);
65952
+ if (comments.length === 0) continue;
65953
+ threads.push({
65954
+ id: node.id,
65955
+ path: node.path,
65956
+ line: node.line ?? node.originalLine,
65957
+ isResolved: node.isResolved,
65958
+ isOutdated: node.isOutdated,
65959
+ comments,
65960
+ ...node.comments.totalCount > comments.length ? { truncated: true } : {}
65961
+ });
65962
+ }
65963
+ threads.sort((a, b2) => a.comments[0].createdAt.localeCompare(b2.comments[0].createdAt));
65964
+ const truncated = (pr.comments?.totalCount ?? 0) > (pr.comments?.nodes.length ?? 0) || (pr.reviews?.totalCount ?? 0) > (pr.reviews?.nodes.length ?? 0) || (pr.reviewThreads?.totalCount ?? 0) > (pr.reviewThreads?.nodes.length ?? 0);
65965
+ return { items, threads, ...truncated ? { truncated: true } : {} };
65966
+ }
65371
65967
  function parseBranchChecks(data) {
65372
65968
  const target = data.repository?.ref?.target;
65373
65969
  const latest = /* @__PURE__ */ new Map();
@@ -65484,10 +66080,16 @@ function reviewDecision(value) {
65484
66080
  function mergeState(value) {
65485
66081
  return value && MERGE_STATES[value] || "unknown";
65486
66082
  }
65487
- var CHECK_RUN_FIELDS, REVIEW_QUERY, LIST_ORDER, AGGREGATE_PRIORITY, MERGE_STATES;
66083
+ var MAX_COMMENTS, MAX_REVIEWS, MAX_THREADS, MAX_THREAD_COMMENTS, MAX_REVIEW_REQUESTS, ACTOR, CHECK_RUN_FIELDS, REVIEW_QUERY, REVIEWER_STATES, actorOf, LIST_ORDER, AGGREGATE_PRIORITY, MERGE_STATES;
65488
66084
  var init_checks_query = __esm({
65489
66085
  "../../plugins/github/src/server/checks-query.ts"() {
65490
66086
  "use strict";
66087
+ MAX_COMMENTS = 50;
66088
+ MAX_REVIEWS = 50;
66089
+ MAX_THREADS = 50;
66090
+ MAX_THREAD_COMMENTS = 20;
66091
+ MAX_REVIEW_REQUESTS = 30;
66092
+ ACTOR = "login avatarUrl";
65491
66093
  CHECK_RUN_FIELDS = `
65492
66094
  databaseId
65493
66095
  name
@@ -65512,6 +66114,41 @@ var init_checks_query = __esm({
65512
66114
  updatedAt
65513
66115
  mergeStateStatus
65514
66116
  reviewDecision
66117
+ reviewRequests(first: ${MAX_REVIEW_REQUESTS}) {
66118
+ nodes {
66119
+ asCodeOwner
66120
+ requestedReviewer {
66121
+ __typename
66122
+ ... on User { ${ACTOR} }
66123
+ ... on Bot { ${ACTOR} }
66124
+ ... on Mannequin { ${ACTOR} }
66125
+ ... on Team { slug }
66126
+ }
66127
+ }
66128
+ }
66129
+ reviews(last: ${MAX_REVIEWS}) {
66130
+ totalCount
66131
+ nodes { id state body submittedAt url author { ${ACTOR} } }
66132
+ }
66133
+ comments(last: ${MAX_COMMENTS}) {
66134
+ totalCount
66135
+ nodes { id body createdAt url author { ${ACTOR} } }
66136
+ }
66137
+ reviewThreads(first: ${MAX_THREADS}) {
66138
+ totalCount
66139
+ nodes {
66140
+ id
66141
+ isResolved
66142
+ isOutdated
66143
+ path
66144
+ line
66145
+ originalLine
66146
+ comments(first: ${MAX_THREAD_COMMENTS}) {
66147
+ totalCount
66148
+ nodes { id body createdAt url author { ${ACTOR} } }
66149
+ }
66150
+ }
66151
+ }
65515
66152
  commits(last: 1) {
65516
66153
  nodes {
65517
66154
  commit {
@@ -65549,6 +66186,16 @@ var init_checks_query = __esm({
65549
66186
  }
65550
66187
  }
65551
66188
  }`;
66189
+ REVIEWER_STATES = {
66190
+ APPROVED: "approved",
66191
+ CHANGES_REQUESTED: "changes_requested",
66192
+ COMMENTED: "commented",
66193
+ DISMISSED: "dismissed"
66194
+ };
66195
+ actorOf = (author) => ({
66196
+ login: author?.login ?? "ghost",
66197
+ avatarUrl: webUrl(author?.avatarUrl ?? null)
66198
+ });
65552
66199
  LIST_ORDER = {
65553
66200
  failure: 0,
65554
66201
  running: 1,
@@ -68049,11 +68696,14 @@ function stopRuntime(rt) {
68049
68696
  }
68050
68697
  function idleTimeoutMs2() {
68051
68698
  const override = Number(process.env.BAND_AGENT_IDLE_TIMEOUT_MS);
68052
- return Number.isFinite(override) && override > 0 ? override : IDLE_TIMEOUT_MS;
68699
+ return Number.isFinite(override) && override > 0 ? override : null;
68053
68700
  }
68054
- function scheduleIdle(rt, delayMs = idleTimeoutMs2()) {
68701
+ function scheduleIdle(rt, delayMs) {
68055
68702
  if (rt.idleTimer) clearTimeout(rt.idleTimer);
68056
- const delay2 = Math.min(delayMs, MAX_TIMER_MS);
68703
+ rt.idleTimer = null;
68704
+ const timeout = idleTimeoutMs2();
68705
+ if (timeout === null) return;
68706
+ const delay2 = Math.min(delayMs ?? timeout, MAX_TIMER_MS2);
68057
68707
  rt.idleTimer = setTimeout(() => {
68058
68708
  rt.idleTimer = null;
68059
68709
  if (rt.inTurn || rt.pending.size > 0 || rt.attaching) return;
@@ -68068,7 +68718,7 @@ function scheduleIdle(rt, delayMs = idleTimeoutMs2()) {
68068
68718
  },
68069
68719
  "idle agent has pending work; not stopping it"
68070
68720
  );
68071
- scheduleIdle(rt, until - now + idleTimeoutMs2());
68721
+ scheduleIdle(rt, until - now + timeout);
68072
68722
  return;
68073
68723
  }
68074
68724
  log46.info({ chatId: rt.chatId }, "stopping idle agent");
@@ -68208,6 +68858,9 @@ function routeUpdate(rt, notification) {
68208
68858
  if (!rt.attaching) {
68209
68859
  rt.work.observeUpdate(update, rt.inTurn);
68210
68860
  agentActive(rt);
68861
+ if (!rt.inTurn && rt.routing === "log" && AGENT_WORK_UPDATES.has(update.sessionUpdate)) {
68862
+ activityObserver?.(rt.chatId, rt.worktreeId, "update");
68863
+ }
68211
68864
  }
68212
68865
  if (rt.routing === "drop") return;
68213
68866
  if (update.sessionUpdate === "user_message_chunk") {
@@ -68283,6 +68936,7 @@ function handlersFor(rt, generation) {
68283
68936
  if (n.sessionId !== rt.sessionId || rt.attaching) return;
68284
68937
  rt.work.observeAsyncTask(n.update);
68285
68938
  agentActive(rt);
68939
+ activityObserver?.(rt.chatId, rt.worktreeId, "work");
68286
68940
  },
68287
68941
  onPermission: (req, signal) => requestPermission(rt, req, signal),
68288
68942
  onElicitation: (req, signal) => requestElicitation(rt, req, signal),
@@ -68582,14 +69236,14 @@ function sessionCost(sessionId) {
68582
69236
  const row = events.latest(sessionId, revision, { kind: "turn-ended" });
68583
69237
  return row?.event.type === "turn-ended" ? row.event.usage?.costUsd ?? null : null;
68584
69238
  }
68585
- var log46, IDLE_TIMEOUT_MS, CANCEL_GRACE_MS, MAX_TIMER_MS, PROBE_SETTLE_MS, events, settings, RUNTIMES_KEY, LISTENERS_KEY2, CATALOG_KEY, DEFAULTS_CHANGED_KEY, g6, runtimes, listeners2, catalog, defaultsChangedAt, transientId, pendingObserver, ChatNotFoundError, AgentSessionService, agentSessionService;
69239
+ var log46, CANCEL_GRACE_MS, MAX_TIMER_MS2, PROBE_SETTLE_MS, events, settings, RUNTIMES_KEY, LISTENERS_KEY2, CATALOG_KEY, DEFAULTS_CHANGED_KEY, g6, runtimes, listeners2, catalog, defaultsChangedAt, transientId, pendingObserver, AGENT_WORK_UPDATES, activityObserver, ChatNotFoundError, AgentSessionService, agentSessionService;
68586
69240
  var init_agent_session_service = __esm({
68587
69241
  "src/server/services/agent-session-service.ts"() {
68588
69242
  "use strict";
68589
69243
  init_src2();
68590
69244
  init_src();
68591
69245
  init_acp_agent_process();
68592
- init_chat_events();
69246
+ init_chat_events2();
68593
69247
  init_settings();
68594
69248
  init_registry();
68595
69249
  init_agent_pending_work();
@@ -68600,9 +69254,8 @@ var init_agent_session_service = __esm({
68600
69254
  init_mcp_proxy_service();
68601
69255
  init_worktree_service();
68602
69256
  log46 = createLogger("agent-sessions");
68603
- IDLE_TIMEOUT_MS = 15 * 6e4;
68604
69257
  CANCEL_GRACE_MS = 15e3;
68605
- MAX_TIMER_MS = 2 ** 31 - 1;
69258
+ MAX_TIMER_MS2 = 2 ** 31 - 1;
68606
69259
  PROBE_SETTLE_MS = 1500;
68607
69260
  events = new ChatEventQueries();
68608
69261
  settings = new SettingsQueries();
@@ -68621,6 +69274,15 @@ var init_agent_session_service = __esm({
68621
69274
  defaultsChangedAt = g6[DEFAULTS_CHANGED_KEY];
68622
69275
  transientId = -1e9;
68623
69276
  pendingObserver = null;
69277
+ AGENT_WORK_UPDATES = /* @__PURE__ */ new Set([
69278
+ "user_message_chunk",
69279
+ "agent_message_chunk",
69280
+ "agent_thought_chunk",
69281
+ "tool_call",
69282
+ "tool_call_update",
69283
+ "plan"
69284
+ ]);
69285
+ activityObserver = null;
68624
69286
  ChatNotFoundError = class extends Error {
68625
69287
  constructor(chatId) {
68626
69288
  super(`Chat not found: ${chatId}`);
@@ -68647,6 +69309,16 @@ var init_agent_session_service = __esm({
68647
69309
  observePending(observer) {
68648
69310
  pendingObserver = observer;
68649
69311
  }
69312
+ /** Called when the agent acts outside a turn Band started: an update from
69313
+ * a turn of its own, or a background task starting or ending. */
69314
+ observeActivity(observer) {
69315
+ activityObserver = observer;
69316
+ }
69317
+ /** The soonest time one of the chat's pending-work holds lapses, or null with none. */
69318
+ nextWorkDeadline(chatId) {
69319
+ const holds = runtimes.get(chatId)?.work.outstanding() ?? [];
69320
+ return holds.length > 0 ? Math.min(...holds.map((h) => h.until)) : null;
69321
+ }
68650
69322
  /**
68651
69323
  * Makes sure the chat's agent process runs and is attached to the chat's
68652
69324
  * session, starting a new session when the chat has none. For `view`,
@@ -73424,11 +74096,11 @@ var require_core = __commonJS({
73424
74096
  Ajv2.ValidationError = validation_error_1.default;
73425
74097
  Ajv2.MissingRefError = ref_error_1.default;
73426
74098
  exports.default = Ajv2;
73427
- function checkOptions(checkOpts, options, msg, log77 = "error") {
74099
+ function checkOptions(checkOpts, options, msg, log78 = "error") {
73428
74100
  for (const key2 in checkOpts) {
73429
74101
  const opt = key2;
73430
74102
  if (opt in options)
73431
- this.logger[log77](`${msg}: option ${key2}. ${checkOpts[opt]}`);
74103
+ this.logger[log78](`${msg}: option ${key2}. ${checkOpts[opt]}`);
73432
74104
  }
73433
74105
  }
73434
74106
  function getSchEnv(keyRef) {
@@ -75914,10 +76586,10 @@ init_agent_spawn();
75914
76586
  import {
75915
76587
  appendFileSync as appendFileSync2,
75916
76588
  createReadStream as createReadStream5,
75917
- existsSync as existsSync16,
75918
- mkdirSync as mkdirSync9,
76589
+ existsSync as existsSync17,
76590
+ mkdirSync as mkdirSync10,
75919
76591
  readFileSync as readFileSync12,
75920
- statSync as statSync13
76592
+ statSync as statSync15
75921
76593
  } from "node:fs";
75922
76594
  import { createServer as createServer3 } from "node:http";
75923
76595
  import { basename as basename7, join as join55, resolve as resolve14, sep as sep10 } from "node:path";
@@ -75928,7 +76600,7 @@ import { parseArgs } from "node:util";
75928
76600
  init_src();
75929
76601
  init_daemon_backend();
75930
76602
  init_in_process_backend();
75931
- import { existsSync as existsSync4, statSync as statSync4 } from "node:fs";
76603
+ import { existsSync as existsSync5, statSync as statSync5 } from "node:fs";
75932
76604
  import { join as join7 } from "node:path";
75933
76605
  var log4 = createLogger("terminal-backend");
75934
76606
  function createTerminalBackend(serverRoot, bandHome2) {
@@ -75937,7 +76609,7 @@ function createTerminalBackend(serverRoot, bandHome2) {
75937
76609
  const entry = [
75938
76610
  join7(serverRoot, "terminal-daemon.mjs"),
75939
76611
  join7(serverRoot, "terminal-daemon.ts")
75940
- ].find((candidate) => existsSync4(candidate));
76612
+ ].find((candidate) => existsSync5(candidate));
75941
76613
  if (!entry) {
75942
76614
  log4.error(
75943
76615
  { serverRoot },
@@ -75945,7 +76617,7 @@ function createTerminalBackend(serverRoot, bandHome2) {
75945
76617
  );
75946
76618
  return new InProcessTerminalBackend();
75947
76619
  }
75948
- const { size, mtimeMs } = statSync4(entry);
76620
+ const { size, mtimeMs } = statSync5(entry);
75949
76621
  return new DaemonTerminalBackend({
75950
76622
  entry,
75951
76623
  runDir: join7(bandHome2, "run"),
@@ -78243,14 +78915,14 @@ import { join as join9, normalize, resolve as resolve2, sep as sep2 } from "node
78243
78915
 
78244
78916
  // ../../node_modules/.pnpm/totalist@3.0.1/node_modules/totalist/sync/index.mjs
78245
78917
  import { join as join8, resolve } from "path";
78246
- import { readdirSync as readdirSync5, statSync as statSync5 } from "fs";
78918
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
78247
78919
  function totalist(dir, callback, pre = "") {
78248
78920
  dir = resolve(".", dir);
78249
78921
  let arr = readdirSync5(dir);
78250
78922
  let i = 0, abs, stats;
78251
78923
  for (; i < arr.length; i++) {
78252
78924
  abs = join8(dir, arr[i]);
78253
- stats = statSync5(abs);
78925
+ stats = statSync6(abs);
78254
78926
  stats.isDirectory() ? totalist(abs, callback, join8(pre, arr[i])) : callback(join8(pre, arr[i]), abs, stats);
78255
78927
  }
78256
78928
  }
@@ -78774,7 +79446,7 @@ function viaLocal(dir, isEtag, uri, extns) {
78774
79446
  function is404(req, res) {
78775
79447
  return res.statusCode = 404, res.end();
78776
79448
  }
78777
- function send(req, res, file2, stats, headers) {
79449
+ function send(req, res, file3, stats, headers) {
78778
79450
  let code = 200, tmp, opts = {};
78779
79451
  headers = { ...headers };
78780
79452
  for (let key2 in headers) {
@@ -78802,7 +79474,7 @@ function send(req, res, file2, stats, headers) {
78802
79474
  headers["Accept-Ranges"] = "bytes";
78803
79475
  }
78804
79476
  res.writeHead(code, headers);
78805
- fs.createReadStream(file2, opts).pipe(res);
79477
+ fs.createReadStream(file3, opts).pipe(res);
78806
79478
  }
78807
79479
  var ENCODING = {
78808
79480
  ".br": "br",
@@ -79071,11 +79743,7 @@ function createCorsMiddleware(getAllowedOrigins) {
79071
79743
 
79072
79744
  // src/api/chat-events.ts
79073
79745
  init_src();
79074
-
79075
- // ../../packages/shared/src/chat-events.ts
79076
- var HISTORY_PAGE_SIZE = 20;
79077
-
79078
- // src/api/chat-events.ts
79746
+ init_chat_events();
79079
79747
  init_queued_message_store();
79080
79748
 
79081
79749
  // src/server/services/_utils/sse-writer.ts
@@ -79290,6 +79958,7 @@ async function handleChatEvents(req, res, chatId) {
79290
79958
  }
79291
79959
 
79292
79960
  // src/api/chat-history.ts
79961
+ init_chat_events();
79293
79962
  init_agent_session_service();
79294
79963
  init_chat_service();
79295
79964
  var MAX_CHAT_ID_LENGTH = 200;
@@ -79524,7 +80193,7 @@ init_map_limited();
79524
80193
  init_remote_worktrees();
79525
80194
  init_repo_host_policy();
79526
80195
  init_ephemeral_lifecycle_service();
79527
- import { existsSync as existsSync14 } from "node:fs";
80196
+ import { existsSync as existsSync15 } from "node:fs";
79528
80197
  import { basename as basename2, join as join44, posix as posix7, resolve as resolve9 } from "node:path";
79529
80198
 
79530
80199
  // src/server/services/repo-avatar-service.ts
@@ -79916,7 +80585,7 @@ var RepoService = class {
79916
80585
  }
79917
80586
  }
79918
80587
  const resolvedPath = resolve9(path2);
79919
- const kind = existsSync14(join44(resolvedPath, ".git")) ? "git" : "plain";
80588
+ const kind = existsSync15(join44(resolvedPath, ".git")) ? "git" : "plain";
79920
80589
  let defaultBranch = "main";
79921
80590
  let worktrees2 = [];
79922
80591
  if (kind === "git") {
@@ -80253,15 +80922,15 @@ var RepoService = class {
80253
80922
  ...isLocalHostEnabled() ? ["local"] : [],
80254
80923
  ...tokenService.listHosts().filter((h) => h.id !== "local" && h.status === "online").map((h) => h.id)
80255
80924
  ];
80256
- let failure = "no host is available to ask";
80925
+ let failure2 = "no host is available to ask";
80257
80926
  for (const hostId2 of hostIds2) {
80258
80927
  try {
80259
80928
  const { stdout } = await hostRegistry.hostById(hostId2).exec("git", ["ls-remote", "--symref", "--", url2, "HEAD"], { timeoutMs: 6e4 });
80260
80929
  const match = stdout.match(/^ref:\s+refs\/heads\/(\S+)\s+HEAD$/m);
80261
80930
  if (match) return match[1];
80262
- failure = "the remote did not report a default branch";
80931
+ failure2 = "the remote did not report a default branch";
80263
80932
  } catch (err) {
80264
- failure = err instanceof Error ? err.message : String(err);
80933
+ failure2 = err instanceof Error ? err.message : String(err);
80265
80934
  }
80266
80935
  }
80267
80936
  if (asked?.host) {
@@ -80271,7 +80940,7 @@ var RepoService = class {
80271
80940
  if (viaApi) return viaApi;
80272
80941
  }
80273
80942
  throw new RepoInputError(
80274
- `Could not read the default branch of ${asked?.url ?? url2} (${failure}). Pass the branch.`
80943
+ `Could not read the default branch of ${asked?.url ?? url2} (${failure2}). Pass the branch.`
80275
80944
  );
80276
80945
  }
80277
80946
  /**
@@ -80294,7 +80963,7 @@ var RepoService = class {
80294
80963
  message: `Repo "${name4}" is already a git repo`
80295
80964
  });
80296
80965
  }
80297
- if (!existsSync14(repo.path)) {
80966
+ if (!existsSync15(repo.path)) {
80298
80967
  throw new TRPCError({
80299
80968
  code: "NOT_FOUND",
80300
80969
  message: `Repo path "${repo.path}" no longer exists. Remove the repo and re-add it.`
@@ -80511,7 +81180,7 @@ var DEFAULT_MAX_BYTES = 200 * 1024 * 1024;
80511
81180
  var DEFAULT_QUIET_MS = 3 * 6e4;
80512
81181
  var DEFAULT_POLL_MS2 = 6e4;
80513
81182
  var MAX_VIEW_BYTES = 1024 * 1024;
80514
- var LOCAL_HOST_ID7 = "local";
81183
+ var LOCAL_HOST_ID8 = "local";
80515
81184
  var errorText3 = (err) => err instanceof Error ? err.message : String(err);
80516
81185
  function envNumber2(name4, fallback) {
80517
81186
  const raw = Number(process.env[name4]);
@@ -80519,7 +81188,7 @@ function envNumber2(name4, fallback) {
80519
81188
  }
80520
81189
  var checkpointKeep = () => Math.floor(envNumber2("BAND_CHECKPOINT_KEEP", DEFAULT_KEEP));
80521
81190
  var checkpointMaxBytes = () => envNumber2("BAND_CHECKPOINT_MAX_BYTES", DEFAULT_MAX_BYTES);
80522
- var quietMs = () => envNumber2("BAND_CHECKPOINT_QUIET_MS", DEFAULT_QUIET_MS);
81191
+ var quietMs2 = () => envNumber2("BAND_CHECKPOINT_QUIET_MS", DEFAULT_QUIET_MS);
80523
81192
  var pollMs = () => envNumber2("BAND_CHECKPOINT_POLL_MS", DEFAULT_POLL_MS2);
80524
81193
  function toView2(row) {
80525
81194
  return {
@@ -80536,11 +81205,11 @@ function toView2(row) {
80536
81205
  sessions: row.sessionIds.length
80537
81206
  };
80538
81207
  }
80539
- function isSafeSessionFile(file2) {
80540
- if (!/^[A-Za-z0-9_-]+$/.test(file2.root)) return false;
80541
- if (typeof file2.rel !== "string" || file2.rel === "" || file2.rel.includes("\0")) return false;
80542
- if (posix8.isAbsolute(file2.rel)) return false;
80543
- const clean = posix8.normalize(file2.rel);
81208
+ function isSafeSessionFile(file3) {
81209
+ if (!/^[A-Za-z0-9_-]+$/.test(file3.root)) return false;
81210
+ if (typeof file3.rel !== "string" || file3.rel === "" || file3.rel.includes("\0")) return false;
81211
+ if (posix8.isAbsolute(file3.rel)) return false;
81212
+ const clean = posix8.normalize(file3.rel);
80544
81213
  return clean !== ".." && !clean.startsWith("../") && clean !== ".";
80545
81214
  }
80546
81215
  function sessionsHashOf(files) {
@@ -80629,8 +81298,8 @@ var CheckpointService = class {
80629
81298
  ineligible(worktreeId4) {
80630
81299
  const found = worktreeService.resolve(worktreeId4);
80631
81300
  if (!found) return "unknown worktree";
80632
- const hostId2 = found.worktree.hostId ?? LOCAL_HOST_ID7;
80633
- if (hostId2 === LOCAL_HOST_ID7) return "the worktree is on the hub's own machine";
81301
+ const hostId2 = found.worktree.hostId ?? LOCAL_HOST_ID8;
81302
+ if (hostId2 === LOCAL_HOST_ID8) return "the worktree is on the hub's own machine";
80634
81303
  if (found.repo.kind !== "git") return "not a git repo";
80635
81304
  if (found.worktree.supersededBy) return "superseded";
80636
81305
  if (this.busy.has(worktreeId4)) return "busy";
@@ -80673,7 +81342,7 @@ var CheckpointService = class {
80673
81342
  const out = [];
80674
81343
  for (const repo of loadState().repos) {
80675
81344
  for (const wt of repo.worktrees) {
80676
- if ((wt.hostId ?? LOCAL_HOST_ID7) === hostId2) {
81345
+ if ((wt.hostId ?? LOCAL_HOST_ID8) === hostId2) {
80677
81346
  out.push({
80678
81347
  repo: repo.name,
80679
81348
  name: wt.name,
@@ -80687,8 +81356,8 @@ var CheckpointService = class {
80687
81356
  async doTake(worktreeId4, trigger, opts) {
80688
81357
  const found = worktreeService.resolve(worktreeId4);
80689
81358
  if (!found) throw new CheckpointError("invalid", `Unknown worktree "${worktreeId4}"`);
80690
- const hostId2 = found.worktree.hostId ?? LOCAL_HOST_ID7;
80691
- if (hostId2 === LOCAL_HOST_ID7) {
81359
+ const hostId2 = found.worktree.hostId ?? LOCAL_HOST_ID8;
81360
+ if (hostId2 === LOCAL_HOST_ID8) {
80692
81361
  throw new CheckpointError(
80693
81362
  "invalid",
80694
81363
  "Checkpoints are for worktrees on a worker, not on the hub's own machine."
@@ -80875,7 +81544,7 @@ var CheckpointService = class {
80875
81544
  if (this.ineligible(worktreeId4)) continue;
80876
81545
  live.add(worktreeId4);
80877
81546
  try {
80878
- await this.sweepOne(worktreeId4, wt.hostId ?? LOCAL_HOST_ID7, wt.path);
81547
+ await this.sweepOne(worktreeId4, wt.hostId ?? LOCAL_HOST_ID8, wt.path);
80879
81548
  } catch (err) {
80880
81549
  log49.debug({ worktreeId: worktreeId4, err: errorText3(err) }, "checkpoint sweep skipped a worktree");
80881
81550
  }
@@ -80899,7 +81568,7 @@ var CheckpointService = class {
80899
81568
  this.watches.set(worktreeId4, { signature, changedAt: now, checkpointed: watch.checkpointed });
80900
81569
  return;
80901
81570
  }
80902
- if (watch.checkpointed === signature || now - watch.changedAt < quietMs()) return;
81571
+ if (watch.checkpointed === signature || now - watch.changedAt < quietMs2()) return;
80903
81572
  await this.take(worktreeId4, "quiet");
80904
81573
  }
80905
81574
  /**
@@ -81074,11 +81743,11 @@ ${diff}`).digest("hex");
81074
81743
  const found = worktreeService.resolve(worktreeId4);
81075
81744
  if (!found) throw new CheckpointError("invalid", `Unknown worktree "${worktreeId4}"`);
81076
81745
  const { repo, worktree } = found;
81077
- const fromHostId = worktree.hostId ?? LOCAL_HOST_ID7;
81746
+ const fromHostId = worktree.hostId ?? LOCAL_HOST_ID8;
81078
81747
  if (fromHostId === toHostId) {
81079
81748
  throw new CheckpointError("invalid", `${worktreeId4} is already on ${toHostId}.`);
81080
81749
  }
81081
- if (toHostId === LOCAL_HOST_ID7) {
81750
+ if (toHostId === LOCAL_HOST_ID8) {
81082
81751
  throw new CheckpointError(
81083
81752
  "invalid",
81084
81753
  "A worktree can be restored on a worker, not on the hub's own machine."
@@ -81111,7 +81780,7 @@ ${diff}`).digest("hex");
81111
81780
  }
81112
81781
  }
81113
81782
  if (repo.worktrees.some(
81114
- (w2) => w2.name === worktree.name && (w2.hostId ?? LOCAL_HOST_ID7) === toHostId
81783
+ (w2) => w2.name === worktree.name && (w2.hostId ?? LOCAL_HOST_ID8) === toHostId
81115
81784
  )) {
81116
81785
  throw new CheckpointError(
81117
81786
  "conflict",
@@ -81250,11 +81919,11 @@ ${diff}`).digest("hex");
81250
81919
  }
81251
81920
  const stage = posix8.join(wipDir, "sessions", `${row.id}`);
81252
81921
  await host.fs.rm(stage, { recursive: true, force: true });
81253
- for (const file2 of files) {
81254
- if (!isSafeSessionFile(file2)) continue;
81255
- const target = posix8.join(stage, file2.root, file2.rel);
81922
+ for (const file3 of files) {
81923
+ if (!isSafeSessionFile(file3)) continue;
81924
+ const target = posix8.join(stage, file3.root, file3.rel);
81256
81925
  await host.fs.mkdir(posix8.dirname(target), { recursive: true });
81257
- await host.fs.writeFile(target, Buffer.from(file2.data, "base64"));
81926
+ await host.fs.writeFile(target, Buffer.from(file3.data, "base64"));
81258
81927
  }
81259
81928
  await rpc.call(METHOD_LIFECYCLE_IMPORT_SESSIONS, { dir: stage });
81260
81929
  }
@@ -81266,8 +81935,8 @@ ${diff}`).digest("hex");
81266
81935
  const { worktreeId: worktreeId4 } = input;
81267
81936
  const found = worktreeService.resolve(worktreeId4);
81268
81937
  if (!found) throw new CheckpointError("invalid", `Unknown worktree "${worktreeId4}"`);
81269
- const fromHostId = found.worktree.hostId ?? LOCAL_HOST_ID7;
81270
- if (fromHostId === LOCAL_HOST_ID7) {
81938
+ const fromHostId = found.worktree.hostId ?? LOCAL_HOST_ID8;
81939
+ if (fromHostId === LOCAL_HOST_ID8) {
81271
81940
  throw new CheckpointError("invalid", "Only worktrees on a worker can be moved.");
81272
81941
  }
81273
81942
  if (!this.sessions.get(fromHostId)?.attached) {
@@ -81335,7 +82004,7 @@ ${diff}`).digest("hex");
81335
82004
  for (const repo of loadState().repos) {
81336
82005
  const clone3 = hostRegistry.repoPathOn(repo.name, hostId2, "");
81337
82006
  for (const wt of repo.worktrees) {
81338
- if ((wt.hostId ?? LOCAL_HOST_ID7) !== hostId2 || wt.supersededBy) continue;
82007
+ if ((wt.hostId ?? LOCAL_HOST_ID8) !== hostId2 || wt.supersededBy) continue;
81339
82008
  if (clone3 && wt.path === clone3) continue;
81340
82009
  out.push({
81341
82010
  repo: repo.name,
@@ -81682,7 +82351,7 @@ init_token_service();
81682
82351
  init_worktree_script_service();
81683
82352
  init_worktree_service();
81684
82353
  var log51 = createLogger("follow");
81685
- var LOCAL_HOST_ID8 = "local";
82354
+ var LOCAL_HOST_ID9 = "local";
81686
82355
  var errorText4 = (err) => err instanceof Error ? err.message : String(err);
81687
82356
  function autoContinueMaxAgeMs() {
81688
82357
  const raw = Number(process.env.BAND_AUTO_CONTINUE_MAX_AGE_MS);
@@ -81697,7 +82366,7 @@ var FollowService = class {
81697
82366
  locate(worktreeId4) {
81698
82367
  const found = worktreeService.resolve(worktreeId4);
81699
82368
  if (!found) return null;
81700
- return { ...found, hostId: found.worktree.hostId ?? LOCAL_HOST_ID8 };
82369
+ return { ...found, hostId: found.worktree.hostId ?? LOCAL_HOST_ID9 };
81701
82370
  }
81702
82371
  /**
81703
82372
  * Whether the worktree is on a worker that is offline and is not asleep. A sleeping worktree
@@ -81706,7 +82375,7 @@ var FollowService = class {
81706
82375
  isOffline(worktreeId4) {
81707
82376
  const at = this.locate(worktreeId4);
81708
82377
  if (!at) return false;
81709
- if (at.hostId === LOCAL_HOST_ID8) return false;
82378
+ if (at.hostId === LOCAL_HOST_ID9) return false;
81710
82379
  if (at.worktree.supersededBy) return false;
81711
82380
  if (checkpointService.isHostOnline(at.hostId)) return false;
81712
82381
  if (ephemeralLifecycleService.stateOf(worktreeId4)) return false;
@@ -81761,7 +82430,7 @@ var FollowService = class {
81761
82430
  blocked: { reason, message },
81762
82431
  question: message
81763
82432
  });
81764
- if (hostId2 === LOCAL_HOST_ID8) {
82433
+ if (hostId2 === LOCAL_HOST_ID9) {
81765
82434
  return blocked("not-offline", "This worktree is on the hub's own machine, which is up.");
81766
82435
  }
81767
82436
  if (worktree.supersededBy) {
@@ -81793,12 +82462,12 @@ var FollowService = class {
81793
82462
  const needed = this.neededAgents(worktreeId4, opts);
81794
82463
  const clones = new Set(
81795
82464
  [...this.repos.allHostPaths().get(repo.name)?.keys() ?? []].filter(
81796
- (id) => id !== LOCAL_HOST_ID8
82465
+ (id) => id !== LOCAL_HOST_ID9
81797
82466
  )
81798
82467
  );
81799
82468
  const load = await this.loadByHost();
81800
82469
  const candidates = tokenService.listHosts(1e3).filter(
81801
- (h) => h.id !== LOCAL_HOST_ID8 && h.id !== hostId2 && h.status === "online" && checkpointService.isHostOnline(h.id) && !checkpointService.isHostEphemeral(h.id) && isHostAllowed(allowed2, h.id)
82470
+ (h) => h.id !== LOCAL_HOST_ID9 && h.id !== hostId2 && h.status === "online" && checkpointService.isHostOnline(h.id) && !checkpointService.isHostEphemeral(h.id) && isHostAllowed(allowed2, h.id)
81802
82471
  );
81803
82472
  if (candidates.length === 0) {
81804
82473
  return {
@@ -81867,7 +82536,7 @@ var FollowService = class {
81867
82536
  for (const repo of this.repos.loadAll()) {
81868
82537
  for (const wt of repo.worktrees) {
81869
82538
  if (wt.supersededBy) continue;
81870
- const id = wt.hostId ?? LOCAL_HOST_ID8;
82539
+ const id = wt.hostId ?? LOCAL_HOST_ID9;
81871
82540
  out.set(id, (out.get(id) ?? 0) + 1);
81872
82541
  }
81873
82542
  }
@@ -81883,7 +82552,7 @@ var FollowService = class {
81883
82552
  async gate(worktreeId4, opts = {}) {
81884
82553
  const stale = this.locate(worktreeId4);
81885
82554
  const next = stale?.worktree.supersededBy ? this.locate(stale.worktree.supersededBy) : null;
81886
- if (stale && next && stale.hostId !== LOCAL_HOST_ID8 && !checkpointService.isHostOnline(stale.hostId)) {
82555
+ if (stale && next && stale.hostId !== LOCAL_HOST_ID9 && !checkpointService.isHostOnline(stale.hostId)) {
81887
82556
  return { worktreeId: idOf(next) };
81888
82557
  }
81889
82558
  if (!this.isOffline(worktreeId4)) return { worktreeId: worktreeId4 };
@@ -82017,7 +82686,7 @@ var FollowService = class {
82017
82686
  async onHostReturned(hostId2) {
82018
82687
  for (const repo of this.repos.loadAll()) {
82019
82688
  for (const wt of repo.worktrees) {
82020
- if ((wt.hostId ?? LOCAL_HOST_ID8) !== hostId2 || !wt.supersededBy) continue;
82689
+ if ((wt.hostId ?? LOCAL_HOST_ID9) !== hostId2 || !wt.supersededBy) continue;
82021
82690
  const oldId = toWorktreeId(repo.name, wt.name, wt.hostId);
82022
82691
  if (wt.divergence?.state === "resolved") continue;
82023
82692
  try {
@@ -90379,17 +91048,17 @@ import { execFile as execFile13 } from "node:child_process";
90379
91048
  import {
90380
91049
  accessSync as accessSync3,
90381
91050
  constants as constants3,
90382
- existsSync as existsSync15,
91051
+ existsSync as existsSync16,
90383
91052
  lstatSync as lstatSync4,
90384
- mkdirSync as mkdirSync8,
91053
+ mkdirSync as mkdirSync9,
90385
91054
  readFileSync as readFileSync11,
90386
- realpathSync as realpathSync4,
91055
+ realpathSync as realpathSync5,
90387
91056
  symlinkSync,
90388
- unlinkSync as unlinkSync5,
91057
+ unlinkSync as unlinkSync6,
90389
91058
  writeFileSync as writeFileSync7
90390
91059
  } from "node:fs";
90391
91060
  import { homedir as homedir15, platform as platform2 } from "node:os";
90392
- import { basename as basename3, dirname as dirname12, join as join46 } from "node:path";
91061
+ import { basename as basename3, dirname as dirname13, join as join46 } from "node:path";
90393
91062
  function windowsBinDir() {
90394
91063
  const localAppData = process.env.LOCALAPPDATA || join46(homedir15(), "AppData", "Local");
90395
91064
  return join46(localAppData, "band", "bin");
@@ -90408,7 +91077,7 @@ function checkCliWindows() {
90408
91077
  if (!match) return "ConflictingBinary";
90409
91078
  const target = match[1];
90410
91079
  if (basename3(target).toLowerCase() !== "band.exe") return "ConflictingBinary";
90411
- return existsSync15(target) ? "Installed" : "NotInstalled";
91080
+ return existsSync16(target) ? "Installed" : "NotInstalled";
90412
91081
  }
90413
91082
  async function checkCli() {
90414
91083
  if (platform2() === "win32") {
@@ -90421,7 +91090,7 @@ async function checkCli() {
90421
91090
  }
90422
91091
  let target;
90423
91092
  try {
90424
- target = realpathSync4(SYMLINK_PATH);
91093
+ target = realpathSync5(SYMLINK_PATH);
90425
91094
  } catch {
90426
91095
  return "NotInstalled";
90427
91096
  }
@@ -90500,7 +91169,7 @@ async function addBinDirToUserPath(binDir) {
90500
91169
  }
90501
91170
  async function installCliWindows(binaryPath) {
90502
91171
  const binDir = windowsBinDir();
90503
- mkdirSync8(binDir, { recursive: true });
91172
+ mkdirSync9(binDir, { recursive: true });
90504
91173
  const shim = `@echo off\r
90505
91174
  "${binaryPath}" %*\r
90506
91175
  `;
@@ -90516,11 +91185,11 @@ async function installCli(_opts = {}) {
90516
91185
  await installCliWindows(binaryPath);
90517
91186
  return;
90518
91187
  }
90519
- const dir = dirname12(SYMLINK_PATH);
91188
+ const dir = dirname13(SYMLINK_PATH);
90520
91189
  if (isDirWritable(dir)) {
90521
91190
  try {
90522
91191
  lstatSync4(SYMLINK_PATH);
90523
- unlinkSync5(SYMLINK_PATH);
91192
+ unlinkSync6(SYMLINK_PATH);
90524
91193
  } catch (err) {
90525
91194
  const code = err.code;
90526
91195
  if (code !== "ENOENT") throw err;
@@ -90774,7 +91443,7 @@ function subscribeToFileChanges(worktreeId4, listener) {
90774
91443
  // src/server/services/formatter.ts
90775
91444
  init_src();
90776
91445
  init_registry();
90777
- import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as join47, resolve as resolvePath3 } from "node:path";
91446
+ import { basename as basename4, dirname as dirname14, isAbsolute as isAbsolute4, join as join47, resolve as resolvePath3 } from "node:path";
90778
91447
  import prettier from "prettier";
90779
91448
  var log56 = createLogger("formatter");
90780
91449
  var FormatterError = class extends Error {
@@ -90853,7 +91522,7 @@ async function formatFile(worktreePath, filePath, content, options = {}) {
90853
91522
  var JSON_CONFIGS = [".prettierrc", ".prettierrc.json"];
90854
91523
  async function resolveConfigOnHost(fs3, absFile, worktreePath) {
90855
91524
  const root = resolvePath3(worktreePath);
90856
- let dir = dirname13(absFile);
91525
+ let dir = dirname14(absFile);
90857
91526
  for (; ; ) {
90858
91527
  for (const name4 of JSON_CONFIGS) {
90859
91528
  const parsed = await readJson2(fs3, join47(dir, name4));
@@ -90863,8 +91532,8 @@ async function resolveConfigOnHost(fs3, absFile, worktreePath) {
90863
91532
  if (pkg && typeof pkg.prettier === "object" && pkg.prettier !== null) {
90864
91533
  return withoutPlugins(pkg.prettier);
90865
91534
  }
90866
- if (dir === root || dirname13(dir) === dir) return null;
90867
- dir = dirname13(dir);
91535
+ if (dir === root || dirname14(dir) === dir) return null;
91536
+ dir = dirname14(dir);
90868
91537
  }
90869
91538
  }
90870
91539
  function withoutPlugins(config2) {
@@ -90897,7 +91566,7 @@ async function isInsideWorktree(fs3, absFile, worktreePath) {
90897
91566
  realFile = await fs3.realpath(absFile);
90898
91567
  } catch {
90899
91568
  try {
90900
- realFile = join47(await fs3.realpath(dirname13(absFile)), basename4(absFile));
91569
+ realFile = join47(await fs3.realpath(dirname14(absFile)), basename4(absFile));
90901
91570
  } catch {
90902
91571
  realFile = absFile;
90903
91572
  }
@@ -91160,7 +91829,7 @@ init_environment_build_service();
91160
91829
  init_src4();
91161
91830
  init_dist();
91162
91831
  init_registry();
91163
- import { basename as basename5, dirname as dirname14, isAbsolute as isAbsolute6 } from "node:path";
91832
+ import { basename as basename5, dirname as dirname15, isAbsolute as isAbsolute6 } from "node:path";
91164
91833
  init_token_service();
91165
91834
  var EnvironmentService = class {
91166
91835
  /** The environment of a repo's main checkout, and which hosts meet its `requires`. */
@@ -91178,8 +91847,8 @@ var EnvironmentService = class {
91178
91847
  if (!isAbsolute6(path2)) {
91179
91848
  throw new TRPCError({ code: "BAD_REQUEST", message: "path must be absolute" });
91180
91849
  }
91181
- const isFile2 = basename5(path2) === "environment.json" && basename5(dirname14(path2)) === ".band";
91182
- return this.read(isFile2 ? dirname14(dirname14(path2)) : path2);
91850
+ const isFile2 = basename5(path2) === "environment.json" && basename5(dirname15(path2)) === ".band";
91851
+ return this.read(isFile2 ? dirname15(dirname15(path2)) : path2);
91183
91852
  }
91184
91853
  read(dir) {
91185
91854
  return hostRegistry.local.scripts.environment({ repoPath: dir, worktreePath: dir });
@@ -91571,26 +92240,83 @@ var historyRouter = t.router({
91571
92240
  })
91572
92241
  });
91573
92242
 
92243
+ // src/server/api/hooks/router.ts
92244
+ init_zod();
92245
+
91574
92246
  // src/server/services/hooks-service.ts
91575
92247
  init_registry();
91576
- function checkHooks2() {
91577
- return hostRegistry.local.agentEnv.hooksStatus();
92248
+ init_token_service();
92249
+ var LOCAL_HOST_ID10 = "local";
92250
+ function hostOf2(hostId2) {
92251
+ return hostRegistry.hostById(hostId2);
92252
+ }
92253
+ function checkHooks2(hostId2 = LOCAL_HOST_ID10) {
92254
+ return hostOf2(hostId2).agentEnv.hooksStatus();
91578
92255
  }
91579
- function installHooks2() {
91580
- return hostRegistry.local.agentEnv.installHooks();
92256
+ function installHooks2(hostId2 = LOCAL_HOST_ID10) {
92257
+ return hostOf2(hostId2).agentEnv.installHooks();
92258
+ }
92259
+ function removeHooks2(hostId2 = LOCAL_HOST_ID10) {
92260
+ return hostOf2(hostId2).agentEnv.removeHooks();
92261
+ }
92262
+ async function claudeCodeOn(hostId2, agents) {
92263
+ if (hostId2 !== LOCAL_HOST_ID10) return agents.includes("claude-code");
92264
+ try {
92265
+ return typeof await hostOf2(hostId2).acp.resolveLaunch({ type: "claude-code" }) !== "string";
92266
+ } catch {
92267
+ return false;
92268
+ }
92269
+ }
92270
+ async function listHostHooks() {
92271
+ const hosts2 = tokenService.listHosts().filter((h) => h.id !== LOCAL_HOST_ID10 || h.usable);
92272
+ return Promise.all(
92273
+ hosts2.map(async (h) => {
92274
+ const online = h.id === LOCAL_HOST_ID10 || h.status === "online";
92275
+ const view4 = {
92276
+ hostId: h.id,
92277
+ name: h.name,
92278
+ online,
92279
+ hasClaudeCode: await claudeCodeOn(h.id, h.agents),
92280
+ status: null
92281
+ };
92282
+ if (!online) return view4;
92283
+ try {
92284
+ view4.status = await checkHooks2(h.id);
92285
+ } catch (err) {
92286
+ view4.error = err instanceof Error ? err.message : String(err);
92287
+ }
92288
+ return view4;
92289
+ })
92290
+ );
91581
92291
  }
91582
92292
 
91583
92293
  // src/server/api/hooks/router.ts
92294
+ var hostInput = external_exports2.object({ hostId: external_exports2.string().optional() }).default({});
92295
+ function failure(err) {
92296
+ return new Error(err instanceof Error ? err.message : String(err));
92297
+ }
91584
92298
  var hooksRouter = t.router({
91585
- check: publicProcedure.query(async () => {
91586
- return await checkHooks2();
92299
+ /** The hub's own host. */
92300
+ check: adminProcedure.input(hostInput).query(async ({ input }) => {
92301
+ return await checkHooks2(input.hostId);
91587
92302
  }),
91588
- install: publicProcedure.mutation(async () => {
92303
+ /** Every host with its hooks status. */
92304
+ hosts: adminProcedure.query(async () => ({ hosts: await listHostHooks() })),
92305
+ install: adminProcedure.input(hostInput).mutation(async ({ input }) => {
91589
92306
  try {
91590
- await installHooks2();
92307
+ await installHooks2(input.hostId);
91591
92308
  return { ok: true };
91592
92309
  } catch (err) {
91593
- throw new Error(err instanceof Error ? err.message : String(err));
92310
+ throw failure(err);
92311
+ }
92312
+ }),
92313
+ /** Removes Band's entries and leaves every other hook and setting. */
92314
+ remove: adminProcedure.input(hostInput).mutation(async ({ input }) => {
92315
+ try {
92316
+ await removeHooks2(input.hostId);
92317
+ return { ok: true };
92318
+ } catch (err) {
92319
+ throw failure(err);
91594
92320
  }
91595
92321
  })
91596
92322
  });
@@ -91724,6 +92450,17 @@ var RELAY_PROCEDURES = /* @__PURE__ */ new Set([
91724
92450
  "statuses.notify",
91725
92451
  "statuses.clearNeedsAttention",
91726
92452
  "repos.list",
92453
+ "repos.add",
92454
+ "repos.addFromWorker",
92455
+ "repos.addByUrl",
92456
+ "repos.inspectFolder",
92457
+ "repos.resolveRemote",
92458
+ "repos.gitInit",
92459
+ "repos.promoteToGit",
92460
+ "repos.update",
92461
+ "repos.updateLabel",
92462
+ "repos.reorder",
92463
+ "repos.remove",
91727
92464
  "hosts.list",
91728
92465
  "hosts.forAgents",
91729
92466
  "cronjobs.list",
@@ -91736,7 +92473,13 @@ var RELAY_PROCEDURES = /* @__PURE__ */ new Set([
91736
92473
  "worktrees.create",
91737
92474
  "worktrees.remove"
91738
92475
  ]);
91739
- var UNBATCHABLE = /* @__PURE__ */ new Set(["repos.list", "hosts.list", "worktrees.create", "worktrees.remove"]);
92476
+ var UNBATCHABLE = /* @__PURE__ */ new Set([
92477
+ "repos.list",
92478
+ "hosts.list",
92479
+ "worktrees.create",
92480
+ "worktrees.remove",
92481
+ "statuses.notify"
92482
+ ]);
91740
92483
  function pinHost(input, workerId, keep) {
91741
92484
  const base = input !== null && typeof input === "object" ? input : {};
91742
92485
  return keep && typeof base.hostId === "string" ? base : { ...base, hostId: workerId };
@@ -91749,7 +92492,7 @@ function pinWorktreeHost(request, workerId) {
91749
92492
  } catch {
91750
92493
  return request;
91751
92494
  }
91752
- const isTrpc = call === "/trpc/worktrees.create" || call === "/trpc/worktrees.remove";
92495
+ const isTrpc = call === "/trpc/worktrees.create" || call === "/trpc/worktrees.remove" || call === "/trpc/statuses.notify";
91753
92496
  const isMcp = call === "/mcp";
91754
92497
  if (!isTrpc && !isMcp) return request;
91755
92498
  const parsed = parseJson(bodyText(request.body));
@@ -91903,7 +92646,7 @@ function checkNamed(input, workerId, lookups) {
91903
92646
  if (worktree === null || lookups.hostOfWorktree(worktree) !== workerId) return outside;
91904
92647
  }
91905
92648
  for (const cwd of named.cwds) {
91906
- const worktree = lookups.worktreeOfCwd(path.resolve(cwd));
92649
+ const worktree = lookups.worktreeOfCwd(path.resolve(cwd), workerId);
91907
92650
  if (worktree === null || lookups.hostOfWorktree(worktree) !== workerId) return outside;
91908
92651
  }
91909
92652
  for (const id of named.terminals) {
@@ -91929,8 +92672,40 @@ function checkChosenId(procedure, input, lookups) {
91929
92672
  }
91930
92673
  return { ok: true };
91931
92674
  }
92675
+ var REPO_BY_NAME_PROCEDURES = /* @__PURE__ */ new Set([
92676
+ "repos.update",
92677
+ "repos.updateLabel",
92678
+ "repos.remove",
92679
+ "repos.promoteToGit"
92680
+ ]);
92681
+ function checkRepoCall(procedure, input, workerId, lookups) {
92682
+ const record3 = input ?? {};
92683
+ if (REPO_BY_NAME_PROCEDURES.has(procedure)) {
92684
+ if (typeof record3.name !== "string") return deny(400, `${procedure} needs a repo name`);
92685
+ const refusal = lookups.repoHostRefusal(record3.name, workerId);
92686
+ return refusal === null ? { ok: true } : deny(403, `${procedure}: ${refusal}`);
92687
+ }
92688
+ if (procedure === "repos.addFromWorker") {
92689
+ const { hostId: hostId2, name: name4, allowedHostIds } = record3;
92690
+ if (typeof hostId2 !== "string") return deny(400, `${procedure} needs a hostId`);
92691
+ if (Array.isArray(allowedHostIds) && !allowedHostIds.includes(hostId2)) {
92692
+ return deny(
92693
+ 403,
92694
+ `${procedure}: The repo would be limited to hosts that exclude ${hostId2}, where the folder is`
92695
+ );
92696
+ }
92697
+ if (typeof name4 === "string") {
92698
+ const refusal = lookups.repoHostRefusal(name4, workerId);
92699
+ if (refusal !== null) return deny(403, `${procedure}: ${refusal}`);
92700
+ }
92701
+ }
92702
+ return { ok: true };
92703
+ }
91932
92704
  function checkCall(procedure, input, workerId, lookups) {
91933
92705
  if (!RELAY_PROCEDURES.has(procedure)) return deny(403, `${procedure} is not available to agents`);
92706
+ if (procedure.startsWith("repos.") && procedure !== "repos.list") {
92707
+ return checkRepoCall(procedure, input, workerId, lookups);
92708
+ }
91934
92709
  let named = input;
91935
92710
  if (CRONJOB_KEY_PROCEDURES.has(procedure)) {
91936
92711
  const { key: key2, ...rest } = input ?? {};
@@ -94768,7 +95543,7 @@ init_src();
94768
95543
  init_zod();
94769
95544
  init_registry();
94770
95545
  init_queued_message_store();
94771
- import { realpathSync as realpathSync5 } from "node:fs";
95546
+ import { realpathSync as realpathSync6 } from "node:fs";
94772
95547
  import { join as join49, resolve as resolve11, sep as sep7 } from "node:path";
94773
95548
  init_chat_service();
94774
95549
  init_state();
@@ -94786,8 +95561,8 @@ function isPathWithinUploadDir(p2) {
94786
95561
  return false;
94787
95562
  }
94788
95563
  try {
94789
- const canonicalUploadDir = realpathSync5(uploadDir);
94790
- const canonicalPath2 = realpathSync5(p2);
95564
+ const canonicalUploadDir = realpathSync6(uploadDir);
95565
+ const canonicalPath2 = realpathSync6(p2);
94791
95566
  return canonicalPath2 === canonicalUploadDir || canonicalPath2.startsWith(canonicalUploadDir + sep7);
94792
95567
  } catch {
94793
95568
  return true;
@@ -94800,38 +95575,38 @@ async function resolveQueuedFiles(chatId, worktreeId4, files) {
94800
95575
  const needsSave = [];
94801
95576
  const needsSaveIdx = [];
94802
95577
  for (let i = 0; i < files.length; i++) {
94803
- const file2 = files[i];
94804
- const derivedPath = file2.path ?? await uploadPathFromUrl(worktreeId4, file2.url) ?? void 0;
95578
+ const file3 = files[i];
95579
+ const derivedPath = file3.path ?? await uploadPathFromUrl(worktreeId4, file3.url) ?? void 0;
94805
95580
  if (derivedPath) {
94806
95581
  const inside = remote ? await isWithinUploads(worktreeId4, derivedPath) : isPathWithinUploadDir(derivedPath);
94807
95582
  if (!inside) {
94808
95583
  log57.warn(
94809
- { chatId, path: derivedPath, filename: file2.filename },
95584
+ { chatId, path: derivedPath, filename: file3.filename },
94810
95585
  "queue: dropping file with path outside uploads directory"
94811
95586
  );
94812
95587
  continue;
94813
95588
  }
94814
95589
  resolved.push({
94815
- mediaType: file2.mediaType,
94816
- url: file2.url,
95590
+ mediaType: file3.mediaType,
95591
+ url: file3.url,
94817
95592
  path: derivedPath,
94818
- ...file2.filename !== void 0 && { filename: file2.filename }
95593
+ ...file3.filename !== void 0 && { filename: file3.filename }
94819
95594
  });
94820
95595
  continue;
94821
95596
  }
94822
- if (file2.url.startsWith("data:")) {
94823
- needsSave.push(file2);
95597
+ if (file3.url.startsWith("data:")) {
95598
+ needsSave.push(file3);
94824
95599
  needsSaveIdx.push(resolved.length);
94825
95600
  resolved.push({
94826
- mediaType: file2.mediaType,
94827
- url: file2.url,
95601
+ mediaType: file3.mediaType,
95602
+ url: file3.url,
94828
95603
  path: "",
94829
- filename: file2.filename
95604
+ filename: file3.filename
94830
95605
  });
94831
95606
  continue;
94832
95607
  }
94833
95608
  log57.warn(
94834
- { chatId, url: file2.url, filename: file2.filename },
95609
+ { chatId, url: file3.url, filename: file3.filename },
94835
95610
  "queue: dropping file with no path and non-data URL \u2014 cannot recover disk path"
94836
95611
  );
94837
95612
  }
@@ -95153,7 +95928,7 @@ var UsageScannerService = class {
95153
95928
  async scanPair(ws, a) {
95154
95929
  let reader;
95155
95930
  try {
95156
- reader = await this.getUsageReader(a);
95931
+ reader = await this.getUsageReader(a, ws.hostId);
95157
95932
  } catch (err) {
95158
95933
  log58.debug({ err, agentId: a.agentId }, "failed to resolve usage reader for scan");
95159
95934
  return 0;
@@ -95184,6 +95959,7 @@ var UsageScannerService = class {
95184
95959
  { err, sessionId: s2.sessionId, agentType: a.agentType },
95185
95960
  "getSessionUsage failed"
95186
95961
  );
95962
+ if (err instanceof Error && err.name === "HostOfflineError") break;
95187
95963
  maxObserved = Math.max(maxObserved, s2.lastModified);
95188
95964
  continue;
95189
95965
  }
@@ -95239,7 +96015,8 @@ function defaultListWorktrees() {
95239
96015
  out.push({
95240
96016
  worktreeId: toWorktreeId(repo.name, worktree.name, worktree.hostId),
95241
96017
  repo: repo.name,
95242
- worktreePath: worktree.path
96018
+ worktreePath: worktree.path,
96019
+ hostId: worktree.hostId
95243
96020
  });
95244
96021
  }
95245
96022
  }
@@ -95254,8 +96031,9 @@ function defaultListAgents() {
95254
96031
  command: def.command
95255
96032
  }));
95256
96033
  }
95257
- function defaultGetUsageReader(agent) {
95258
- return hostRegistry.local.agentEnv.usageReader(agent);
96034
+ function defaultGetUsageReader(agent, hostId2) {
96035
+ const host = hostId2 ? hostRegistry.hostById(hostId2) : hostRegistry.local;
96036
+ return host.agentEnv.usageReader(agent);
95259
96037
  }
95260
96038
  function defaultIsPollingEnabled() {
95261
96039
  try {
@@ -96220,7 +96998,6 @@ var settingsRouter = t.router({
96220
96998
  init_zod();
96221
96999
  init_branch_status_poller();
96222
97000
  init_state();
96223
- init_task_service();
96224
97001
  init_watcher_service();
96225
97002
  var statusesRouter = t.router({
96226
97003
  get: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).query(({ input }) => {
@@ -96249,12 +97026,12 @@ var statusesRouter = t.router({
96249
97026
  refreshed: await branchStatusPoller.refreshWorktree(input.worktreeId)
96250
97027
  })),
96251
97028
  clearNeedsAttention: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).mutation(({ input }) => {
96252
- const status = taskService.acknowledgeAttention(input.worktreeId);
97029
+ const status = acknowledgeWorktreeAttention(input.worktreeId);
96253
97030
  if (status) emit({ kind: "update", status });
96254
97031
  return { ok: true };
96255
97032
  }),
96256
- resolve: publicProcedure.input(external_exports2.object({ cwd: external_exports2.string() })).query(({ input }) => {
96257
- return { worktreeId: resolveWorktreeIdByCwd(input.cwd) };
97033
+ resolve: publicProcedure.input(external_exports2.object({ cwd: external_exports2.string(), hostId: external_exports2.string().optional() })).query(({ input }) => {
97034
+ return { worktreeId: resolveWorktreeIdByCwd(input.cwd, input.hostId) };
96258
97035
  }),
96259
97036
  /**
96260
97037
  * Agent-agnostic entry point for coding-agent lifecycle notifications
@@ -96275,7 +97052,8 @@ var statusesRouter = t.router({
96275
97052
  payload: external_exports2.record(external_exports2.string(), external_exports2.unknown()),
96276
97053
  agent: external_exports2.string().max(64).optional(),
96277
97054
  dispatch: external_exports2.string().max(64).optional(),
96278
- terminalId: external_exports2.string().max(256).optional()
97055
+ terminalId: external_exports2.string().max(256).optional(),
97056
+ hostId: external_exports2.string().max(256).optional()
96279
97057
  })
96280
97058
  ).mutation(async ({ input }) => {
96281
97059
  const status = await applyHookNotification(input);
@@ -96472,6 +97250,91 @@ var subscriptionsRouter = t.router({
96472
97250
  init_src();
96473
97251
  init_dist();
96474
97252
  init_zod();
97253
+
97254
+ // package.json
97255
+ var package_default = {
97256
+ name: "@band-app/server",
97257
+ version: "0.48.1",
97258
+ type: "module",
97259
+ types: "src/index.ts",
97260
+ exports: {
97261
+ ".": "./src/index.ts",
97262
+ "./package.json": "./package.json"
97263
+ },
97264
+ bin: {
97265
+ "band-server": "./bin/band-server.mjs"
97266
+ },
97267
+ files: [
97268
+ "dist/client",
97269
+ "dist/start-server.mjs",
97270
+ "dist/terminal-daemon.mjs",
97271
+ "dist/openapi.json",
97272
+ "dist/migrations",
97273
+ "bin"
97274
+ ],
97275
+ engines: {
97276
+ node: ">=22.5.0"
97277
+ },
97278
+ os: [
97279
+ "darwin",
97280
+ "linux",
97281
+ "win32"
97282
+ ],
97283
+ scripts: {
97284
+ dev: "NODE_ENV=development tsx watch --exclude '../web/**' start-server.ts",
97285
+ build: "./scripts/build-server.sh",
97286
+ start: "node dist/start-server.mjs",
97287
+ prepack: "node scripts/copy-ui.mjs",
97288
+ postpack: "node scripts/copy-ui.mjs --clean",
97289
+ pretest: "pnpm --filter @band-app/web build && pnpm build",
97290
+ test: "NODE_OPTIONS='--no-warnings=ExperimentalWarning' vitest run",
97291
+ postinstall: `node -e "process.platform!=='win32'&&require('child_process').execSync('chmod +x node_modules/node-pty/prebuilds/*/spawn-helper 2>/dev/null || true',{stdio:'ignore'})"`
97292
+ },
97293
+ dependencies: {
97294
+ "@agentclientprotocol/claude-agent-acp": "0.81.2",
97295
+ "@agentclientprotocol/codex-acp": "1.13.1",
97296
+ "@agentclientprotocol/sdk": "1.5.0",
97297
+ "@vscode/ripgrep": "^1.18.0",
97298
+ fzf: "0.5.2",
97299
+ "node-pty": "^1.1.0",
97300
+ prettier: "^3.8.3",
97301
+ "typescript-language-server": "^5.1.3"
97302
+ },
97303
+ devDependencies: {
97304
+ "@band-app/coding-agent": "workspace:^",
97305
+ "@band-app/environment": "workspace:^",
97306
+ "@band-app/host-api": "workspace:^",
97307
+ "@band-app/host-local": "workspace:^",
97308
+ "@band-app/host-remote": "workspace:^",
97309
+ "@band-app/link": "workspace:^",
97310
+ "@band-app/logger": "workspace:^",
97311
+ "@band-app/plugin-api": "workspace:^",
97312
+ "@band-app/plugin-github": "workspace:^",
97313
+ "@band-app/shared": "workspace:^",
97314
+ "@modelcontextprotocol/sdk": "^1.29.0",
97315
+ "@trpc/client": "^11.12.0",
97316
+ "@trpc/openapi": "11.16.0-alpha",
97317
+ "@trpc/server": "^11.12.0",
97318
+ "@types/express": "^5.0.6",
97319
+ "@types/node": "^22.19.13",
97320
+ "@types/ws": "^8.18.0",
97321
+ "@xterm/headless": "6.1.0-beta.302",
97322
+ croner: "^10.0.1",
97323
+ "drizzle-kit": "1.0.0-rc.1",
97324
+ "drizzle-orm": "1.0.0-rc.1",
97325
+ esbuild: "^0.25.0",
97326
+ express: "^5.2.1",
97327
+ sirv: "^3.0.0",
97328
+ tsx: "^4.21.0",
97329
+ typescript: "^5.7.0",
97330
+ vite: "^7.0.0",
97331
+ vitest: "^4.0.18",
97332
+ ws: "^8.18.0",
97333
+ zod: "^3.25.67"
97334
+ }
97335
+ };
97336
+
97337
+ // src/server/api/system/router.ts
96475
97338
  init_branch_status_poller();
96476
97339
  init_state();
96477
97340
  init_tunnel_service();
@@ -96488,6 +97351,7 @@ var systemRouter = t.router({
96488
97351
  log62.debug({ tunnel }, "services.health: tunnel status");
96489
97352
  const result = {
96490
97353
  webserver: true,
97354
+ version: package_default.version,
96491
97355
  tunnel: tunnel.running,
96492
97356
  tunnel_url: tunnel.url
96493
97357
  };
@@ -97043,6 +97907,45 @@ var vaultRouter = t.router({
97043
97907
  init_dist();
97044
97908
  init_zod();
97045
97909
 
97910
+ // src/server/services/_utils/worktree-preview-link.ts
97911
+ init_state();
97912
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual8 } from "node:crypto";
97913
+ import { posix as posix10 } from "node:path";
97914
+ var WORKTREE_PREVIEW_PREFIX = "/api/worktree-preview/";
97915
+ var PREVIEW_TTL_MS = 60 * 60 * 1e3;
97916
+ function signingKey() {
97917
+ const token = resolveAdminToken(process.env.BAND_ADMIN_TOKEN).token;
97918
+ return createHmac2("sha256", token).update("worktree-preview-v1").digest();
97919
+ }
97920
+ function sign(worktreeId4, dir, expiresAt) {
97921
+ return createHmac2("sha256", signingKey()).update(`${worktreeId4}
97922
+ ${dir}
97923
+ ${expiresAt}`).digest("hex");
97924
+ }
97925
+ function worktreePreviewUrl(worktreeId4, path2, now = Date.now()) {
97926
+ const expiresAt = now + PREVIEW_TTL_MS;
97927
+ const dir = posix10.dirname(path2.replace(/^\/+/, ""));
97928
+ const encodedDir = Buffer.from(dir, "utf8").toString("base64url");
97929
+ const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
97930
+ const grant = `${expiresAt}.${encodedDir}.${sign(worktreeId4, dir, expiresAt)}`;
97931
+ return `${WORKTREE_PREVIEW_PREFIX}${encodeURIComponent(worktreeId4)}/${grant}/${encodedPath}`;
97932
+ }
97933
+ function verifyWorktreePreviewGrant(worktreeId4, grant, filePath, now = Date.now()) {
97934
+ if (filePath.includes("\0")) return false;
97935
+ const [expiry, encodedDir, signature, ...extra] = grant.split(".");
97936
+ const expiresAt = Number(expiry);
97937
+ if (extra.length > 0 || encodedDir === void 0 || !signature || !Number.isSafeInteger(expiresAt) || expiresAt < now) {
97938
+ return false;
97939
+ }
97940
+ const dir = Buffer.from(encodedDir, "base64url").toString("utf8");
97941
+ const expected = Buffer.from(sign(worktreeId4, dir, expiresAt), "hex");
97942
+ const given = Buffer.from(signature, "hex");
97943
+ if (given.length !== expected.length || !timingSafeEqual8(given, expected)) return false;
97944
+ const target = posix10.normalize(filePath.replace(/^\/+/, ""));
97945
+ if (target === ".." || target.startsWith("../")) return false;
97946
+ return dir === "." || target === dir || target.startsWith(`${dir}/`);
97947
+ }
97948
+
97046
97949
  // src/server/services/diff-service.ts
97047
97950
  init_src2();
97048
97951
  init_src();
@@ -97158,9 +98061,9 @@ function assertWorktreeRelative(cwd, filePath) {
97158
98061
  }
97159
98062
  return target;
97160
98063
  }
97161
- async function readUntrackedFileLines(host, cwd, file2) {
98064
+ async function readUntrackedFileLines(host, cwd, file3) {
97162
98065
  try {
97163
- const content = new TextDecoder().decode(await host.fs.readFile(join50(cwd, file2)));
98066
+ const content = new TextDecoder().decode(await host.fs.readFile(join50(cwd, file3)));
97164
98067
  const lines = content.split("\n");
97165
98068
  if (lines.length > 0 && lines[lines.length - 1] === "") {
97166
98069
  lines.pop();
@@ -97450,17 +98353,17 @@ var DiffService = class {
97450
98353
  const fileStatuses = parseFileStatuses(nameStatusOutput);
97451
98354
  const untrackedFiles = untrackedOutput.trim().split("\n").filter(Boolean);
97452
98355
  const untrackedLines = await Promise.all(
97453
- untrackedFiles.map((file2) => readUntrackedFileLines(worktree.host, cwd, file2))
98356
+ untrackedFiles.map((file3) => readUntrackedFileLines(worktree.host, cwd, file3))
97454
98357
  );
97455
98358
  for (let i = 0; i < untrackedFiles.length; i++) {
97456
- const file2 = untrackedFiles[i];
98359
+ const file3 = untrackedFiles[i];
97457
98360
  const lines = untrackedLines[i];
97458
98361
  if (lines === null) continue;
97459
- diff += `diff --git a/${file2} b/${file2}
98362
+ diff += `diff --git a/${file3} b/${file3}
97460
98363
  `;
97461
98364
  diff += "new file mode 100644\n";
97462
98365
  diff += "--- /dev/null\n";
97463
- diff += `+++ b/${file2}
98366
+ diff += `+++ b/${file3}
97464
98367
  `;
97465
98368
  diff += `@@ -0,0 +1,${lines.length} @@
97466
98369
  `;
@@ -97468,7 +98371,7 @@ var DiffService = class {
97468
98371
  diff += "\n";
97469
98372
  stats.filesChanged++;
97470
98373
  stats.insertions += lines.length;
97471
- fileStatuses[file2] = "U";
98374
+ fileStatuses[file3] = "U";
97472
98375
  }
97473
98376
  return {
97474
98377
  diff,
@@ -97696,7 +98599,7 @@ var diffService = new DiffService();
97696
98599
  init_errors6();
97697
98600
  init_ephemeral_lifecycle_service();
97698
98601
  init_worktree_service();
97699
- import { dirname as dirname15, extname as extname3, join as join51, resolve as resolve13, sep as sep9 } from "node:path";
98602
+ import { dirname as dirname16, extname as extname3, join as join51, resolve as resolve13, sep as sep9 } from "node:path";
97700
98603
  var MAX_FILE_SIZE2 = 1024 * 1024;
97701
98604
  var LANG_MAP2 = {
97702
98605
  ".js": "javascript",
@@ -97837,7 +98740,7 @@ var FilesService = class {
97837
98740
  if (await exists2(fs3, target)) {
97838
98741
  throw new Error("A file or directory already exists at this path");
97839
98742
  }
97840
- await assertParentDirectory(fs3, dirname15(target), "Parent");
98743
+ await assertParentDirectory(fs3, dirname16(target), "Parent");
97841
98744
  await fs3.writeFile(target, content, { exclusive: true });
97842
98745
  return { ok: true };
97843
98746
  }
@@ -97847,7 +98750,7 @@ var FilesService = class {
97847
98750
  if (await exists2(fs3, target)) {
97848
98751
  throw new Error("A file or directory already exists at this path");
97849
98752
  }
97850
- await assertParentDirectory(fs3, dirname15(target), "Parent");
98753
+ await assertParentDirectory(fs3, dirname16(target), "Parent");
97851
98754
  await fs3.mkdir(target);
97852
98755
  return { ok: true };
97853
98756
  }
@@ -97889,7 +98792,7 @@ var FilesService = class {
97889
98792
  if (await exists2(fs3, toTarget)) {
97890
98793
  throw new Error("A file or directory already exists at the destination");
97891
98794
  }
97892
- await assertParentDirectory(fs3, dirname15(toTarget), "Destination parent");
98795
+ await assertParentDirectory(fs3, dirname16(toTarget), "Destination parent");
97893
98796
  await fs3.rename(fromTarget, toTarget);
97894
98797
  return {
97895
98798
  ok: true,
@@ -97922,7 +98825,7 @@ var FilesService = class {
97922
98825
  if (await exists2(fs3, toTarget)) {
97923
98826
  throw new Error("A file or directory already exists at the destination");
97924
98827
  }
97925
- await assertParentDirectory(fs3, dirname15(toTarget), "Destination parent");
98828
+ await assertParentDirectory(fs3, dirname16(toTarget), "Destination parent");
97926
98829
  await fs3.copy(fromTarget, toTarget, { recursive: true, exclusive: true });
97927
98830
  return {
97928
98831
  ok: true,
@@ -99408,6 +100311,13 @@ var worktreeRouter = t.router({
99408
100311
  toPath: external_exports2.string().min(1)
99409
100312
  })
99410
100313
  ).mutation(({ input }) => filesService.copyPath(input.worktreeId, input.fromPath, input.toPath)),
100314
+ /** A signed, expiring URL for one worktree file, for a browser tab that cannot send the auth header. */
100315
+ previewUrl: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string(), path: external_exports2.string().min(1) })).query(({ input }) => {
100316
+ if (!worktreeService.resolve(input.worktreeId)) {
100317
+ throw new TRPCError({ code: "NOT_FOUND", message: "Worktree not found" });
100318
+ }
100319
+ return { url: worktreePreviewUrl(input.worktreeId, input.path) };
100320
+ }),
99411
100321
  searchFiles: publicProcedure.input(
99412
100322
  external_exports2.object({
99413
100323
  worktreeId: external_exports2.string(),
@@ -99491,6 +100401,13 @@ var worktreesRouter = t.router({
99491
100401
  throwAsTrpcError3(err);
99492
100402
  }
99493
100403
  }),
100404
+ setOrigin: publicProcedure.input(worktreeSetOriginInput).mutation(({ input }) => {
100405
+ try {
100406
+ return worktreeService.setOrigin(input);
100407
+ } catch (err) {
100408
+ throwAsTrpcError3(err);
100409
+ }
100410
+ }),
99494
100411
  gitPull: publicProcedure.input(worktreeGitInput).mutation(async ({ input }) => {
99495
100412
  try {
99496
100413
  return await worktreeService.gitPull(input);
@@ -99514,7 +100431,7 @@ var worktreesRouter = t.router({
99514
100431
  })
99515
100432
  });
99516
100433
  function throwAsTrpcError3(err) {
99517
- if (err instanceof PlainRepoError) {
100434
+ if (err instanceof PlainRepoError || err instanceof InvalidOriginError) {
99518
100435
  throw new TRPCError({ code: "BAD_REQUEST", message: err.message, cause: err });
99519
100436
  }
99520
100437
  if (err instanceof RepoHostNotAllowedError) {
@@ -99585,12 +100502,15 @@ function discoverProcedures() {
99585
100502
  for (const [path2, procedure] of Object.entries(procRecord)) {
99586
100503
  const type = procedure._def.type;
99587
100504
  if (type === "subscription") continue;
99588
- if (path2.startsWith("tokens.") || path2.startsWith("vault.") || path2.startsWith("checkpoints.") || path2.startsWith("follow.") || path2.startsWith("mcp.") || path2.startsWith("hosts.") && path2 !== "hosts.forAgents" || path2.startsWith("hostRequests.") || path2.startsWith("runners."))
100505
+ if (path2.startsWith("tokens.") || path2.startsWith("vault.") || path2.startsWith("checkpoints.") || path2.startsWith("follow.") || path2.startsWith("mcp.") || path2.startsWith("hosts.") && path2 !== "hosts.forAgents" || path2.startsWith("hostRequests.") || path2.startsWith("runners.") || // Writes ~/.claude/settings.json on a host; only a person's click may.
100506
+ path2.startsWith("hooks."))
99589
100507
  continue;
100508
+ if (path2 === "worktree.previewUrl") continue;
99590
100509
  if (path2 === "environment.validate") continue;
99591
100510
  if (path2 === "repos.addFromWorker") continue;
99592
100511
  if (path2 === "repos.addByUrl") continue;
99593
100512
  if (path2 === "repos.inspectFolder" || path2 === "repos.resolveRemote") continue;
100513
+ if (path2 === "worktrees.setOrigin") continue;
99594
100514
  if (path2 === "environment.build") continue;
99595
100515
  const toolName2 = `band_${path2.replace(/\./g, "_")}`;
99596
100516
  const inputs = procedure._def.inputs;
@@ -99764,6 +100684,11 @@ function crossHostRefusal(repo, targetHostId, callerHostId) {
99764
100684
  }
99765
100685
  return null;
99766
100686
  }
100687
+ function repoHostRefusal(repo, callerHostId) {
100688
+ const allowed2 = allowedHostsOf(repo);
100689
+ if (isHostAllowed(allowed2, callerHostId)) return null;
100690
+ return `${repo} is limited to other hosts (${hostNamesOf(allowed2 ?? []).join(", ")})`;
100691
+ }
99767
100692
 
99768
100693
  // src/server/api/mcp-proxy/band-tools.ts
99769
100694
  init_worktree_service();
@@ -100905,7 +101830,7 @@ async function handleOAuthCallback(req, res) {
100905
101830
  }
100906
101831
 
100907
101832
  // src/server/api/web-app-manifest.ts
100908
- import { createReadStream as createReadStream4, statSync as statSync12 } from "node:fs";
101833
+ import { createReadStream as createReadStream4, statSync as statSync14 } from "node:fs";
100909
101834
  import { join as join52 } from "node:path";
100910
101835
  var WEB_APP_MANIFEST_PATH = "/manifest.webmanifest";
100911
101836
  var ICON_FILES = ["band-192.png", "band-512.png", "apple-touch-icon.png"];
@@ -100929,7 +101854,7 @@ function handleWebAppManifest(req, res, publicDir) {
100929
101854
  }
100930
101855
  function sendFile(req, res, filePath, headers) {
100931
101856
  try {
100932
- const { size } = statSync12(filePath);
101857
+ const { size } = statSync14(filePath);
100933
101858
  res.writeHead(200, { ...headers, "Content-Length": size.toString() });
100934
101859
  if (req.method === "HEAD") res.end();
100935
101860
  else
@@ -101677,31 +102602,52 @@ init_src();
101677
102602
  init_registry();
101678
102603
  async function installSkills2(opts = {}) {
101679
102604
  const result = await hostRegistry.local.agentEnv.installSkills({ home: opts.home });
101680
- const log77 = opts.log;
101681
- if (log77) {
101682
- for (const warning of result.warnings) log77.warn(warning);
101683
- for (const path2 of result.written) log77.info("Installed skill at %s", path2);
102605
+ const log78 = opts.log;
102606
+ if (log78) {
102607
+ for (const warning of result.warnings) log78.warn(warning);
102608
+ for (const path2 of result.written) log78.info("Installed skill at %s", path2);
101684
102609
  for (const path2 of result.updated) {
101685
- log77.info("Updated skill at %s (content differed \u2014 shipped version reinstalled)", path2);
102610
+ log78.info("Updated skill at %s (content differed \u2014 shipped version reinstalled)", path2);
101686
102611
  }
101687
102612
  for (const conflict of result.conflicts) {
101688
- log77.warn("Skill symlink conflict \u2014 %s (left as-is; remove it manually to re-link)", conflict);
102613
+ log78.warn("Skill symlink conflict \u2014 %s (left as-is; remove it manually to re-link)", conflict);
101689
102614
  }
101690
102615
  }
101691
102616
  return result;
101692
102617
  }
101693
102618
 
101694
- // src/server/services/setup-service.ts
101695
- init_settings_service();
102619
+ // src/server/services/coding-agent-seed.ts
102620
+ init_src();
101696
102621
  init_state();
101697
- init_sync_service();
101698
- init_worktree_service();
101699
- var log73 = createLogger("setup");
102622
+ var log73 = createLogger("coding-agent-seed");
101700
102623
  var AGENT_CHECKS = [
101701
102624
  { id: "claude-code", type: "claude-code", label: "Claude Code", binary: "claude" },
101702
102625
  { id: "codex", type: "codex", label: "Codex", binary: "codex" },
101703
102626
  { id: "opencode", type: "opencode", label: "OpenCode", binary: "opencode" }
101704
102627
  ];
102628
+ function seedCodingAgentsFromTypes(types) {
102629
+ const settings3 = loadSettings();
102630
+ if (Array.isArray(settings3.codingAgents) && settings3.codingAgents.length > 0) return;
102631
+ const seeded = AGENT_CHECKS.filter((c) => types.includes(c.type)).map(
102632
+ ({ id, type, label: label2 }) => ({ id, type, label: label2 })
102633
+ );
102634
+ if (seeded.length === 0) return;
102635
+ settings3.codingAgents = seeded;
102636
+ if (!settings3.defaultCodingAgent) settings3.defaultCodingAgent = seeded[0].id;
102637
+ saveSettings(settings3);
102638
+ log73.info(
102639
+ "Enabled %d coding agent(s) from a worker; default = %s",
102640
+ seeded.length,
102641
+ settings3.defaultCodingAgent
102642
+ );
102643
+ }
102644
+
102645
+ // src/server/services/setup-service.ts
102646
+ init_settings_service();
102647
+ init_state();
102648
+ init_sync_service();
102649
+ init_worktree_service();
102650
+ var log74 = createLogger("setup");
101705
102651
  async function runFirstTimeSetup() {
101706
102652
  forgetLocalWorktrees();
101707
102653
  const repoSync = ensureRepoStateInSync().then(() => ensureRepoRemotes());
@@ -101709,12 +102655,11 @@ async function runFirstTimeSetup() {
101709
102655
  const results = await Promise.allSettled([
101710
102656
  repoSync,
101711
102657
  ensureSettingsDefaults(),
101712
- ensureClaudeHooks(),
101713
102658
  ensureSkillsInstalled()
101714
102659
  ]);
101715
102660
  for (const r of results) {
101716
102661
  if (r.status === "rejected") {
101717
- log73.warn(
102662
+ log74.warn(
101718
102663
  "Setup step failed: %s",
101719
102664
  r.reason instanceof Error ? r.reason.message : String(r.reason)
101720
102665
  );
@@ -101728,7 +102673,7 @@ async function refreshAgentModelsInBackground() {
101728
102673
  const ok = results.filter((r) => !r.error).length;
101729
102674
  const failed = results.length - ok;
101730
102675
  if (results.length > 0) {
101731
- log73.info(
102676
+ log74.info(
101732
102677
  "Background model refresh: %d/%d agents updated (%d failed)",
101733
102678
  ok,
101734
102679
  results.length,
@@ -101736,7 +102681,7 @@ async function refreshAgentModelsInBackground() {
101736
102681
  );
101737
102682
  }
101738
102683
  } catch (err) {
101739
- log73.warn(
102684
+ log74.warn(
101740
102685
  "Background model refresh failed: %s",
101741
102686
  err instanceof Error ? err.message : String(err)
101742
102687
  );
@@ -101751,7 +102696,7 @@ async function ensureRepoRemotes() {
101751
102696
  try {
101752
102697
  await repoService.backfillRemotes();
101753
102698
  } catch (err) {
101754
- log73.warn(
102699
+ log74.warn(
101755
102700
  "Failed to record repo remotes at boot: %s",
101756
102701
  err instanceof Error ? err.message : String(err)
101757
102702
  );
@@ -101761,7 +102706,7 @@ function forgetLocalWorktrees() {
101761
102706
  try {
101762
102707
  worktreeService.forgetLocalWorktrees();
101763
102708
  } catch (err) {
101764
- log73.warn(
102709
+ log74.warn(
101765
102710
  "Failed to forget local worktrees at boot: %s",
101766
102711
  err instanceof Error ? err.message : String(err)
101767
102712
  );
@@ -101771,7 +102716,7 @@ async function ensureRepoStateInSync() {
101771
102716
  try {
101772
102717
  await syncService.syncWorktrees();
101773
102718
  } catch (err) {
101774
- log73.warn(
102719
+ log74.warn(
101775
102720
  "Failed to sync repo state at boot: %s",
101776
102721
  err instanceof Error ? err.message : String(err)
101777
102722
  );
@@ -101782,22 +102727,22 @@ async function ensureCliInstalled() {
101782
102727
  try {
101783
102728
  cliStatus = await checkCli();
101784
102729
  } catch (err) {
101785
- log73.warn("Could not check CLI status: %s", err instanceof Error ? err.message : String(err));
102730
+ log74.warn("Could not check CLI status: %s", err instanceof Error ? err.message : String(err));
101786
102731
  return;
101787
102732
  }
101788
102733
  if (cliStatus === "Installed") {
101789
102734
  return;
101790
102735
  }
101791
102736
  if (cliStatus !== "NotInstalled") {
101792
- log73.warn("CLI not auto-installed (status: %s)", cliStatus);
102737
+ log74.warn("CLI not auto-installed (status: %s)", cliStatus);
101793
102738
  return;
101794
102739
  }
101795
- log73.info("Installing band CLI...");
102740
+ log74.info("Installing band CLI...");
101796
102741
  try {
101797
102742
  await installCli();
101798
- log73.info("CLI installed to %s", SYMLINK_PATH);
102743
+ log74.info("CLI installed to %s", SYMLINK_PATH);
101799
102744
  } catch (err) {
101800
- log73.warn("CLI installation failed: %s", err instanceof Error ? err.message : String(err));
102745
+ log74.warn("CLI installation failed: %s", err instanceof Error ? err.message : String(err));
101801
102746
  }
101802
102747
  }
101803
102748
  async function ensureDefaultCodingAgents() {
@@ -101806,17 +102751,17 @@ async function ensureDefaultCodingAgents() {
101806
102751
  if (Array.isArray(existing) && existing.length > 0) {
101807
102752
  return;
101808
102753
  }
101809
- log73.info("Detecting installed coding agents...");
102754
+ log74.info("Detecting installed coding agents...");
101810
102755
  const detected = [];
101811
102756
  for (const check2 of AGENT_CHECKS) {
101812
102757
  const path2 = await systemService.whichBinary(check2.binary);
101813
102758
  if (path2) {
101814
- log73.info("Detected coding agent: %s (%s)", check2.id, path2);
102759
+ log74.info("Detected coding agent: %s (%s)", check2.id, path2);
101815
102760
  detected.push({ id: check2.id, type: check2.type, label: check2.label });
101816
102761
  }
101817
102762
  }
101818
102763
  if (detected.length === 0) {
101819
- log73.info("No coding agent CLIs detected on PATH");
102764
+ log74.info("No coding agent CLIs detected on PATH");
101820
102765
  return;
101821
102766
  }
101822
102767
  const current3 = loadSettings();
@@ -101825,7 +102770,7 @@ async function ensureDefaultCodingAgents() {
101825
102770
  current3.defaultCodingAgent = detected[0].id;
101826
102771
  }
101827
102772
  saveSettings(current3);
101828
- log73.info("Enabled %d coding agent(s); default = %s", detected.length, current3.defaultCodingAgent);
102773
+ log74.info("Enabled %d coding agent(s); default = %s", detected.length, current3.defaultCodingAgent);
101829
102774
  }
101830
102775
  function ensureNotificationDefaults() {
101831
102776
  const settings3 = loadSettings();
@@ -101837,7 +102782,7 @@ function ensureNotificationDefaults() {
101837
102782
  ...settings3,
101838
102783
  notifications: { ...notifications, soundOnNeedsAttention: true }
101839
102784
  });
101840
- log73.info("Set default notifications.soundOnNeedsAttention = true");
102785
+ log74.info("Set default notifications.soundOnNeedsAttention = true");
101841
102786
  }
101842
102787
  function migrateAgentDefaultMode() {
101843
102788
  const settings3 = loadSettings();
@@ -101845,30 +102790,15 @@ function migrateAgentDefaultMode() {
101845
102790
  const mode = agentModeFromVia(settings3.cli?.defaultVia);
101846
102791
  if (!mode) return;
101847
102792
  saveSettings({ ...settings3, agents: { ...settings3.agents, defaultMode: mode } });
101848
- log73.info("Migrated cli.defaultVia to agents.defaultMode = %s", mode);
101849
- }
101850
- async function ensureClaudeHooks() {
101851
- try {
101852
- const status = await checkHooks2();
101853
- if (status.installed) {
101854
- return;
101855
- }
101856
- await installHooks2();
101857
- log73.info("Installed Claude Code hooks");
101858
- } catch (err) {
101859
- log73.warn(
101860
- "Failed to install Claude Code hooks: %s",
101861
- err instanceof Error ? err.message : String(err)
101862
- );
101863
- }
102793
+ log74.info("Migrated cli.defaultVia to agents.defaultMode = %s", mode);
101864
102794
  }
101865
102795
  async function ensureSkillsInstalled() {
101866
102796
  try {
101867
- const result = await installSkills2({ log: log73 });
102797
+ const result = await installSkills2({ log: log74 });
101868
102798
  const wrote = result.written.length + result.updated.length;
101869
102799
  const linkChange = result.linked.length;
101870
102800
  if (wrote > 0 || linkChange > 0 || result.conflicts.length > 0) {
101871
- log73.info(
102801
+ log74.info(
101872
102802
  "Synced CLI skills (shared: %d written, %d updated, %d unchanged; symlinks: %d created, %d already-linked, %d conflicts, %d skipped)",
101873
102803
  result.written.length,
101874
102804
  result.updated.length,
@@ -101880,7 +102810,7 @@ async function ensureSkillsInstalled() {
101880
102810
  );
101881
102811
  }
101882
102812
  } catch (err) {
101883
- log73.warn("Failed to sync CLI skills: %s", err instanceof Error ? err.message : String(err));
102813
+ log74.warn("Failed to sync CLI skills: %s", err instanceof Error ? err.message : String(err));
101884
102814
  }
101885
102815
  }
101886
102816
  var SetupService = class {
@@ -101919,7 +102849,7 @@ init_src();
101919
102849
  import { createHash as createHash12 } from "node:crypto";
101920
102850
  import { readFile as readFile9 } from "node:fs/promises";
101921
102851
  import { join as join54 } from "node:path";
101922
- var log74 = createLogger("worker-cli");
102852
+ var log75 = createLogger("worker-cli");
101923
102853
  var RPC_INVALID_PARAMS2 = -32602;
101924
102854
  var PLATFORM_NAME = /^[a-z0-9_]{1,16}$/;
101925
102855
  function candidatesFor(platform3, arch) {
@@ -101955,7 +102885,7 @@ var WorkerCliService = class {
101955
102885
  }
101956
102886
  const sha2562 = createHash12("sha256").update(data).digest("hex");
101957
102887
  if (have === sha2562) return { available: true, sha256: sha2562, size: data.length };
101958
- log74.info(
102888
+ log75.info(
101959
102889
  `sending the band CLI (${platform3}-${arch}, ${data.length} bytes) to ${session.workerId}`
101960
102890
  );
101961
102891
  const ch = session.openChannel("cli.body", { platform: platform3, arch });
@@ -101997,7 +102927,7 @@ init_chat_service();
101997
102927
  init_state();
101998
102928
  init_subscription_service();
101999
102929
  init_terminal_service();
102000
- var log75 = createLogger("worker-relay");
102930
+ var log76 = createLogger("worker-relay");
102001
102931
  var REPLY_HEADERS2 = ["content-type", "cache-control", "mcp-session-id"];
102002
102932
  var FORWARDED_HEADERS = [
102003
102933
  "content-type",
@@ -102013,11 +102943,12 @@ var defaultLookups = {
102013
102943
  const chat = chatService.get(id);
102014
102944
  return chat?.worktreeId ?? null;
102015
102945
  },
102016
- worktreeOfCwd: (cwd) => resolveWorktreeIdByCwd(cwd),
102946
+ worktreeOfCwd: (cwd, hostId2) => resolveWorktreeIdByCwd(cwd, hostId2),
102017
102947
  hostOfTerminal: (id) => terminalService.hostIdOf(id),
102018
102948
  worktreeOfBrowser: (id) => browserService.get(id)?.worktreeId ?? null,
102019
102949
  worktreeOfSubscription: (id) => subscriptionService.list().find((s2) => s2.id === id)?.worktreeId ?? null,
102020
- crossHostRefusal
102950
+ crossHostRefusal,
102951
+ repoHostRefusal
102021
102952
  };
102022
102953
  var WorkerRelayService = class {
102023
102954
  constructor(lookups = defaultLookups) {
@@ -102036,7 +102967,7 @@ var WorkerRelayService = class {
102036
102967
  let request = parseRequest(params);
102037
102968
  const verdict = checkRelayRequest(request, session.workerId, this.lookups);
102038
102969
  if (!verdict.ok) {
102039
- log75.warn(
102970
+ log76.warn(
102040
102971
  `refused ${request.method} ${request.path.split("?")[0]} from ${session.workerId}: ${verdict.reason}`
102041
102972
  );
102042
102973
  return this.answer(session, verdict.status, refusalBody(request, verdict));
@@ -102071,7 +103002,7 @@ var WorkerRelayService = class {
102071
103002
  redirect: "manual"
102072
103003
  });
102073
103004
  } catch (err) {
102074
- log75.warn(`relay call failed: ${err instanceof Error ? err.message : err}`);
103005
+ log76.warn(`relay call failed: ${err instanceof Error ? err.message : err}`);
102075
103006
  return this.answer(session, 502, { error: "The hub could not answer" });
102076
103007
  }
102077
103008
  const replyHeaders = {};
@@ -102089,7 +103020,7 @@ var WorkerRelayService = class {
102089
103020
  const filtered = filterReposReply(await upstream.json(), session.workerId);
102090
103021
  return this.answer(session, upstream.status, filtered);
102091
103022
  } catch (err) {
102092
- log75.warn(`relay call failed: ${err instanceof Error ? err.message : err}`);
103023
+ log76.warn(`relay call failed: ${err instanceof Error ? err.message : err}`);
102093
103024
  return this.answer(session, 502, { error: "The hub could not answer" });
102094
103025
  }
102095
103026
  }
@@ -102097,7 +103028,7 @@ var WorkerRelayService = class {
102097
103028
  try {
102098
103029
  return this.answer(session, upstream.status, redactHostsReply(await upstream.json()));
102099
103030
  } catch (err) {
102100
- log75.warn(`relay call failed: ${err instanceof Error ? err.message : err}`);
103031
+ log76.warn(`relay call failed: ${err instanceof Error ? err.message : err}`);
102101
103032
  return this.answer(session, 502, { error: "The hub could not answer" });
102102
103033
  }
102103
103034
  }
@@ -102150,9 +103081,9 @@ var workerRelayService = new WorkerRelayService();
102150
103081
 
102151
103082
  // src/server/services/worker-link-service.ts
102152
103083
  init_worktree_service();
102153
- var log76 = createLogger("worker-link");
103084
+ var log77 = createLogger("worker-link");
102154
103085
  var repoQueries5 = new RepoQueries();
102155
- var LOCAL_HOST_ID9 = "local";
103086
+ var LOCAL_HOST_ID11 = "local";
102156
103087
  var CAPABILITY_REFRESH_MS = 3 * 6e4;
102157
103088
  var WORKER_CONNECT_PATH = "/api/workers/connect";
102158
103089
  var WORKER_EXCHANGE_PATH = "/api/workers/exchange";
@@ -102195,10 +103126,10 @@ var WorkerLinkService = class {
102195
103126
  /** Makes every known worker resolvable, so a worktree on an offline host still finds its host. */
102196
103127
  start() {
102197
103128
  for (const row of this.queries.listHosts(1e3)) {
102198
- if (row.id !== LOCAL_HOST_ID9) this.remoteHost(row.id);
103129
+ if (row.id !== LOCAL_HOST_ID11) this.remoteHost(row.id);
102199
103130
  }
102200
103131
  for (const row of this.queries.listHosts(1e3)) {
102201
- if (row.id !== LOCAL_HOST_ID9 && row.status === "online") {
103132
+ if (row.id !== LOCAL_HOST_ID11 && row.status === "online") {
102202
103133
  this.queries.setHostStatus(row.id, "offline");
102203
103134
  }
102204
103135
  }
@@ -102210,7 +103141,7 @@ var WorkerLinkService = class {
102210
103141
  this.refreshTimer.unref();
102211
103142
  this.unsubscribeRevoke = this.tokens.onRevoked((row) => {
102212
103143
  if (row.kind === "worker_session" && row.hostId) {
102213
- if (this.server.drop(row.hostId, "token revoked")) log76.info(`cut link of ${row.hostId}`);
103144
+ if (this.server.drop(row.hostId, "token revoked")) log77.info(`cut link of ${row.hostId}`);
102214
103145
  }
102215
103146
  });
102216
103147
  }
@@ -102224,7 +103155,7 @@ var WorkerLinkService = class {
102224
103155
  this.refreshing = true;
102225
103156
  try {
102226
103157
  await Promise.allSettled(
102227
- this.queries.listHosts(1e3).filter((row) => row.id !== LOCAL_HOST_ID9 && row.status === "online").map((row) => this.refreshReport(row))
103158
+ this.queries.listHosts(1e3).filter((row) => row.id !== LOCAL_HOST_ID11 && row.status === "online").map((row) => this.refreshReport(row))
102228
103159
  );
102229
103160
  } finally {
102230
103161
  this.refreshing = false;
@@ -102240,7 +103171,7 @@ var WorkerLinkService = class {
102240
103171
  this.queries.setHostInfo(row.id, stored);
102241
103172
  if (changed) this.publish(row.id, "online");
102242
103173
  } catch (err) {
102243
- log76.debug(
103174
+ log77.debug(
102244
103175
  `could not refresh the report of ${row.id}: ${err instanceof Error ? err.message : err}`
102245
103176
  );
102246
103177
  }
@@ -102282,7 +103213,7 @@ var WorkerLinkService = class {
102282
103213
  try {
102283
103214
  info = await this.remoteHost(workerId).info();
102284
103215
  } catch (err) {
102285
- log76.warn(
103216
+ log77.warn(
102286
103217
  `could not read host info from ${workerId}: ${err instanceof Error ? err.message : err}`
102287
103218
  );
102288
103219
  }
@@ -102299,27 +103230,32 @@ var WorkerLinkService = class {
102299
103230
  mode: hello.mode
102300
103231
  };
102301
103232
  this.queries.markHostOnline(workerId, at, { info: stored, version: hello.buildId });
102302
- log76.info(`worker ${workerId} is online`);
103233
+ log77.info(`worker ${workerId} is online`);
103234
+ try {
103235
+ seedCodingAgentsFromTypes(hello.agents);
103236
+ } catch (err) {
103237
+ log77.warn(`could not seed coding agents: ${err instanceof Error ? err.message : err}`);
103238
+ }
102303
103239
  this.publish(workerId, "online");
102304
103240
  void ephemeralLifecycleService.onConnected(session);
102305
103241
  void followService.onHostReturned(workerId).catch((err) => {
102306
- log76.warn(`follow check on ${workerId}: ${err instanceof Error ? err.message : err}`);
103242
+ log77.warn(`follow check on ${workerId}: ${err instanceof Error ? err.message : err}`);
102307
103243
  });
102308
103244
  placementService.onHostOnline(workerId);
102309
103245
  void worktreeService.finishPendingRemovals(workerId).catch((err) => {
102310
- log76.warn(`pending removals on ${workerId}: ${err instanceof Error ? err.message : err}`);
103246
+ log77.warn(`pending removals on ${workerId}: ${err instanceof Error ? err.message : err}`);
102311
103247
  });
102312
103248
  }
102313
103249
  setStatus(workerId, status, lastSeenAt) {
102314
103250
  try {
102315
103251
  this.queries.setHostStatus(workerId, status, lastSeenAt);
102316
103252
  } catch (err) {
102317
- log76.warn(
103253
+ log77.warn(
102318
103254
  `could not record ${workerId} as ${status}: ${err instanceof Error ? err.message : err}`
102319
103255
  );
102320
103256
  return;
102321
103257
  }
102322
- log76.info(`worker ${workerId} is ${status}`);
103258
+ log77.info(`worker ${workerId} is ${status}`);
102323
103259
  this.publish(workerId, status);
102324
103260
  }
102325
103261
  publish(hostId2, hostStatus) {
@@ -102364,7 +103300,7 @@ var WorkerLinkService = class {
102364
103300
  send2(200, out);
102365
103301
  } catch (err) {
102366
103302
  if (!(err instanceof TokenExchangeError)) throw err;
102367
- log76.warn(`bootstrap exchange refused: ${err.reason}`);
103303
+ log77.warn(`bootstrap exchange refused: ${err.reason}`);
102368
103304
  send2(401, { error: "invalid or expired token" });
102369
103305
  }
102370
103306
  }
@@ -102383,7 +103319,7 @@ function reportKey(report2) {
102383
103319
  init_worktree_service();
102384
103320
  function logCrash(message) {
102385
103321
  try {
102386
- mkdirSync9(bandHome(), { recursive: true });
103322
+ mkdirSync10(bandHome(), { recursive: true });
102387
103323
  appendFileSync2(join55(bandHome(), "server.log"), message, "utf-8");
102388
103324
  } catch {
102389
103325
  }
@@ -102426,7 +103362,7 @@ function resolveUiDir() {
102426
103362
  resolve14(SERVER_ROOT, "..", "..", "web", "dist", "client"),
102427
103363
  join55(SERVER_ROOT, "client")
102428
103364
  ];
102429
- return candidates.find((dir) => existsSync16(join55(dir, "_shell.html"))) ?? candidates[1];
103365
+ return candidates.find((dir) => existsSync17(join55(dir, "_shell.html"))) ?? candidates[1];
102430
103366
  }
102431
103367
  var clientDir = resolveUiDir();
102432
103368
  var uiRoot = process.env.BAND_UI_ROOT ? resolve14(process.env.BAND_UI_ROOT) : resolve14(SERVER_ROOT, "..", "web");
@@ -102511,7 +103447,7 @@ function serveStaticFile(res, root, subdir, rawFilename) {
102511
103447
  }
102512
103448
  const filePath = join55(root, subdir, filename);
102513
103449
  try {
102514
- const fileStat = statSync13(filePath);
103450
+ const fileStat = statSync15(filePath);
102515
103451
  const contentType = mimeTypeFromFilename(filename);
102516
103452
  res.writeHead(200, {
102517
103453
  "Content-Type": contentType,
@@ -102526,26 +103462,26 @@ function serveStaticFile(res, root, subdir, rawFilename) {
102526
103462
  }
102527
103463
  async function serveHostedFile(res, kind, worktreeId4, rawName) {
102528
103464
  if (hostRegistry.hostFor(worktreeId4).id === hostRegistry.local.id) return false;
102529
- const file2 = await openHostedFile(kind, worktreeId4, rawName);
102530
- if (!file2) {
103465
+ const file3 = await openHostedFile(kind, worktreeId4, rawName);
103466
+ if (!file3) {
102531
103467
  res.writeHead(404);
102532
103468
  res.end("Not found");
102533
103469
  return true;
102534
103470
  }
102535
103471
  res.writeHead(200, {
102536
103472
  "Content-Type": mimeTypeFromFilename(basename7(decodeURIComponent(rawName))),
102537
- "Content-Length": file2.size.toString(),
103473
+ "Content-Length": file3.size.toString(),
102538
103474
  "Cache-Control": "private, max-age=86400"
102539
103475
  });
102540
103476
  pipeline(
102541
- Readable2.from(file2.stream, { objectMode: false, highWaterMark: 64 * 1024 }),
103477
+ Readable2.from(file3.stream, { objectMode: false, highWaterMark: 64 * 1024 }),
102542
103478
  res,
102543
103479
  () => {
102544
103480
  }
102545
103481
  );
102546
103482
  return true;
102547
103483
  }
102548
- async function serveWorktreeFile(res, worktreeId4, rawPath) {
103484
+ async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}) {
102549
103485
  const worktree = worktreeService.resolve(worktreeId4);
102550
103486
  if (!worktree) {
102551
103487
  res.writeHead(404);
@@ -102561,12 +103497,25 @@ async function serveWorktreeFile(res, worktreeId4, rawPath) {
102561
103497
  }
102562
103498
  try {
102563
103499
  const { fs: fs3 } = worktree.host;
103500
+ const [realRoot, realTarget] = await Promise.all([fs3.realpath(root), fs3.realpath(target)]);
103501
+ if (realTarget !== realRoot && !realTarget.startsWith(realRoot + sep10)) {
103502
+ res.writeHead(400);
103503
+ res.end("Bad request");
103504
+ return;
103505
+ }
102564
103506
  const fileStat = await fs3.stat(target, { followSymlinks: true });
102565
103507
  const contentType = mimeTypeFromFilename(basename7(target));
103508
+ const isHtml = contentType.startsWith("text/html");
102566
103509
  res.writeHead(200, {
102567
103510
  "Content-Type": contentType,
102568
103511
  "Content-Length": fileStat.size.toString(),
102569
- "Cache-Control": "private, no-cache"
103512
+ "Cache-Control": "private, no-cache",
103513
+ // HTML from a repo must not run with the hub's origin.
103514
+ ...isHtml ? {
103515
+ "Content-Security-Policy": "sandbox allow-scripts",
103516
+ "X-Content-Type-Options": "nosniff"
103517
+ } : {},
103518
+ ...extraHeaders
102570
103519
  });
102571
103520
  pipeline(
102572
103521
  Readable2.from(fs3.readStream(target), { objectMode: false, highWaterMark: 64 * 1024 }),
@@ -102579,6 +103528,41 @@ async function serveWorktreeFile(res, worktreeId4, rawPath) {
102579
103528
  res.end("Not found");
102580
103529
  }
102581
103530
  }
103531
+ async function handleWorktreePreview(req, res) {
103532
+ const rest = (req.url ?? "").slice(WORKTREE_PREVIEW_PREFIX.length).split("?")[0];
103533
+ const [rawId, grant, ...pathParts] = rest.split("/");
103534
+ let wId;
103535
+ let filePath;
103536
+ try {
103537
+ wId = decodeURIComponent(rawId ?? "");
103538
+ filePath = decodeURIComponent(pathParts.join("/"));
103539
+ } catch {
103540
+ res.writeHead(400);
103541
+ res.end("Bad request");
103542
+ return;
103543
+ }
103544
+ if (!wId || !grant || !filePath) {
103545
+ res.writeHead(400);
103546
+ res.end("Bad request");
103547
+ return;
103548
+ }
103549
+ if (!verifyWorktreePreviewGrant(wId, grant, filePath)) {
103550
+ res.writeHead(403);
103551
+ res.end("Forbidden");
103552
+ return;
103553
+ }
103554
+ await serveWorktreeFile(res, wId, filePath, {
103555
+ // The page runs repo-controlled scripts on the hub's origin. `sandbox`
103556
+ // without `allow-same-origin` gives it an opaque origin, so it cannot use
103557
+ // the hub's cookies, storage or tRPC. Its own assets are plain GETs under
103558
+ // the grant, and the wildcard CORS header lets scripts fetch them.
103559
+ "Content-Security-Policy": "sandbox allow-scripts allow-forms allow-popups allow-modals",
103560
+ "X-Content-Type-Options": "nosniff",
103561
+ "Referrer-Policy": "no-referrer",
103562
+ "Access-Control-Allow-Origin": "*",
103563
+ "Cache-Control": "private, no-store"
103564
+ });
103565
+ }
102582
103566
  var MAX_WEBHOOK_BODY_BYTES = 256 * 1024;
102583
103567
  var MAX_GITHUB_HOOK_BODY_BYTES = 2 * 1024 * 1024;
102584
103568
  async function handleGithubWebhook(req, res) {
@@ -102815,9 +103799,13 @@ async function main() {
102815
103799
  await handleSubscriptionWebhook(req, res, subscriptionId);
102816
103800
  return;
102817
103801
  }
103802
+ if (req.method === "GET" && req.url?.startsWith(WORKTREE_PREVIEW_PREFIX)) {
103803
+ await handleWorktreePreview(req, res);
103804
+ return;
103805
+ }
102818
103806
  if (handleAuth(req, res)) return;
102819
103807
  if (req.url?.startsWith("/api/uploads/")) {
102820
- const rest = req.url.slice("/api/uploads/".length);
103808
+ const rest = req.url.slice("/api/uploads/".length).split("?")[0];
102821
103809
  const slashIdx = rest.indexOf("/");
102822
103810
  if (slashIdx !== -1) {
102823
103811
  let wId;
@@ -102864,7 +103852,7 @@ async function main() {
102864
103852
  return;
102865
103853
  }
102866
103854
  if (req.url?.startsWith("/api/shared/")) {
102867
- const rest = req.url.slice("/api/shared/".length);
103855
+ const rest = req.url.slice("/api/shared/".length).split("?")[0];
102868
103856
  const slashIdx = rest.indexOf("/");
102869
103857
  if (slashIdx === -1) {
102870
103858
  res.writeHead(400);
@@ -102882,21 +103870,33 @@ async function main() {
102882
103870
  return;
102883
103871
  }
102884
103872
  if (req.url?.startsWith("/api/worktree-file/")) {
102885
- const rest = req.url.slice("/api/worktree-file/".length);
103873
+ const queryIdx = req.url.indexOf("?");
103874
+ const rest = req.url.slice(
103875
+ "/api/worktree-file/".length,
103876
+ queryIdx === -1 ? void 0 : queryIdx
103877
+ );
102886
103878
  const slashIdx = rest.indexOf("/");
102887
103879
  if (slashIdx === -1) {
102888
103880
  res.writeHead(400);
102889
103881
  res.end("Bad request");
102890
103882
  return;
102891
103883
  }
102892
- const wId = decodeURIComponent(rest.slice(0, slashIdx));
102893
- const filePath = rest.slice(slashIdx + 1);
103884
+ let wId;
103885
+ let filePath;
103886
+ try {
103887
+ wId = decodeURIComponent(rest.slice(0, slashIdx));
103888
+ filePath = decodeURIComponent(rest.slice(slashIdx + 1));
103889
+ } catch {
103890
+ res.writeHead(400);
103891
+ res.end("Bad request");
103892
+ return;
103893
+ }
102894
103894
  if (!wId || !filePath) {
102895
103895
  res.writeHead(400);
102896
103896
  res.end("Bad request");
102897
103897
  return;
102898
103898
  }
102899
- await serveWorktreeFile(res, wId, decodeURIComponent(filePath));
103899
+ await serveWorktreeFile(res, wId, filePath);
102900
103900
  return;
102901
103901
  }
102902
103902
  if (req.url?.startsWith("/api/cdp/tabs")) {
@@ -103087,7 +104087,7 @@ async function main() {
103087
104087
  }
103088
104088
  });
103089
104089
  const shellPath2 = join55(clientDir, "_shell.html");
103090
- if (!existsSync16(shellPath2)) {
104090
+ if (!existsSync17(shellPath2)) {
103091
104091
  throw new Error(
103092
104092
  `UI build not found at ${clientDir} (no _shell.html). Run \`pnpm build\` or pass --ui-dir <path>.`
103093
104093
  );